Merge branch 'main' of gitea.haiyihy.com:hy/hyapp-server
This commit is contained in:
commit
4a4e40bc7d
@ -14,7 +14,7 @@ HY Voice Room 是语音房后端 monorepo。当前架构不再自研 IM 长连
|
|||||||
- `user-service` 当前承接登录、三方注册、密码、refresh session、用户主数据、默认短号和临时靓号基础能力。
|
- `user-service` 当前承接登录、三方注册、密码、refresh session、用户主数据、默认短号和临时靓号基础能力。
|
||||||
- `activity-service` 当前承接每日任务、活动/系统消息基础能力和房间事件 consumer 边界;`room_outbox` 到 activity/audit 的实际消费链路按独立消费位点接入。
|
- `activity-service` 当前承接每日任务、活动/系统消息基础能力和房间事件 consumer 边界;`room_outbox` 到 activity/audit 的实际消费链路按独立消费位点接入。
|
||||||
- `cron-service` 当前负责后台调度、任务运行记录,以及通过内部 gRPC 触发 owner service 批处理;它不直接拥有房间、账务、用户或活动业务状态。
|
- `cron-service` 当前负责后台调度、任务运行记录,以及通过内部 gRPC 触发 owner service 批处理;它不直接拥有房间、账务、用户或活动业务状态。
|
||||||
- `notice-service` 当前消费 `wallet_outbox` 的余额变更事件,异步投递腾讯云 IM 单聊私有通知;后续 push、站内 inbox 和其它 owner outbox 消费按模块继续扩展。
|
- `notice-service` 当前消费 `wallet_outbox` 的余额变更事件和 `room_outbox` 的踢人事件,异步投递腾讯云 IM 单聊私有通知;房间群消息和群成员控制仍由 `room-service` 的房间 IM bridge 处理。
|
||||||
- 腾讯云 IM 承担客户端长连接、群消息、公屏、单聊、离线/漫游和全球接入能力;本仓库不再部署 `im-service`。
|
- 腾讯云 IM 承担客户端长连接、群消息、公屏、单聊、离线/漫游和全球接入能力;本仓库不再部署 `im-service`。
|
||||||
- 多 App 共用同一套后端服务,入口由 `gateway-service` 通过包名或 `app_code` 解析租户;所有业务库表按 `app_code` 隔离,设计细节见 `docs/多App租户架构.md`。
|
- 多 App 共用同一套后端服务,入口由 `gateway-service` 通过包名或 `app_code` 解析租户;所有业务库表按 `app_code` 隔离,设计细节见 `docs/多App租户架构.md`。
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ Authorization: Bearer <access_token>
|
|||||||
- `X-Request-ID`/`request_id` 由后端生成,只做链路追踪;写操作的 `command_id` 由前端按用户动作生成,用于服务端幂等。
|
- `X-Request-ID`/`request_id` 由后端生成,只做链路追踪;写操作的 `command_id` 由前端按用户动作生成,用于服务端幂等。
|
||||||
- `room-service` 在 `CreateRoom` 成功后写 `room_outbox`,由 outbox worker 异步创建或补偿同名腾讯云 IM 群组。
|
- `room-service` 在 `CreateRoom` 成功后写 `room_outbox`,由 outbox worker 异步创建或补偿同名腾讯云 IM 群组。
|
||||||
- `room-service` 在上下麦、禁言、踢人、送礼等命令提交后只写房间状态、command log 和 outbox;腾讯云 IM 房间系统自定义消息由 outbox worker 异步投递。
|
- `room-service` 在上下麦、禁言、踢人、送礼等命令提交后只写房间状态、command log 和 outbox;腾讯云 IM 房间系统自定义消息由 outbox worker 异步投递。
|
||||||
- `notice-service` 消费钱包余额变更 outbox,向单个用户发送 `wallet_notice` 私有自定义消息;钱包不会直接调用腾讯云 IM。
|
- `notice-service` 消费钱包余额变更 outbox 和房间踢人 outbox,向单个用户发送 `wallet_notice`、`room_notice` 私有自定义消息;owner service 不直接调用腾讯云 IM 单聊。
|
||||||
- `gateway-service` 已提供 `/api/v1/tencent-im/callback`,支持腾讯云 IM 入群前和发言前回调,并分别回查 `VerifyRoomPresence` 和 `CheckSpeakPermission`。
|
- `gateway-service` 已提供 `/api/v1/tencent-im/callback`,支持腾讯云 IM 入群前和发言前回调,并分别回查 `VerifyRoomPresence` 和 `CheckSpeakPermission`。
|
||||||
|
|
||||||
腾讯云 IM 配置分别在:
|
腾讯云 IM 配置分别在:
|
||||||
@ -197,7 +197,7 @@ services/notice-service/ async user notice delivery workers
|
|||||||
- 腾讯云 IM 回调入口已经落在 gateway;线上仍必须在腾讯云 IM 控制台启用 `Group.CallbackBeforeApplyJoinGroup` 和 `Group.CallbackBeforeSendMsg`,并配置公网 callback URL 与鉴权 token。
|
- 腾讯云 IM 回调入口已经落在 gateway;线上仍必须在腾讯云 IM 控制台启用 `Group.CallbackBeforeApplyJoinGroup` 和 `Group.CallbackBeforeSendMsg`,并配置公网 callback URL 与鉴权 token。
|
||||||
- 前端对同一次用户写动作的 HTTP 重试必须复用同一个 `command_id`;`X-Request-ID` 不需要也不应该由前端传入。
|
- 前端对同一次用户写动作的 HTTP 重试必须复用同一个 `command_id`;`X-Request-ID` 不需要也不应该由前端传入。
|
||||||
- `wallet-service` App 运行路径使用 MySQL 扣费事务;本地需要先通过 compose initdb 建好 `hyapp_wallet`。
|
- `wallet-service` App 运行路径使用 MySQL 扣费事务;本地需要先通过 compose initdb 建好 `hyapp_wallet`。
|
||||||
- `notice-service` 本地默认不启用腾讯云 IM worker;线上启用 `wallet_notice_worker.enabled=true` 时必须同时启用 `tencent_im.enabled=true`。
|
- `notice-service` 本地默认不启用腾讯云 IM worker;线上启用 `wallet_notice_worker.enabled=true` 或 `room_notice_worker.enabled=true` 时必须同时启用 `tencent_im.enabled=true`。
|
||||||
- `activity-service` 已承接每日任务和消息/活动基础能力;`room_outbox` 到 activity/audit 的实际消费链路仍需按独立消费位点补齐。
|
- `activity-service` 已承接每日任务和消息/活动基础能力;`room_outbox` 到 activity/audit 的实际消费链路仍需按独立消费位点补齐。
|
||||||
- `room-service` App 已启动 `room_outbox` 补偿 worker;该状态只表示腾讯云 IM 系统消息补偿结果,activity/audit 后续必须使用独立消费位点。
|
- `room-service` App 已启动 `room_outbox` 补偿 worker;该状态只表示腾讯云 IM 系统消息补偿结果,activity/audit 后续必须使用独立消费位点。
|
||||||
- JWT、腾讯云 IM secret 等本地配置只能用开发值,线上必须走安全配置源。
|
- JWT、腾讯云 IM secret 等本地配置只能用开发值,线上必须走安全配置源。
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.5.1
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
// - protoc v5.29.3
|
// - protoc v5.29.2
|
||||||
// source: proto/activity/v1/activity.proto
|
// source: proto/activity/v1/activity.proto
|
||||||
|
|
||||||
package activityv1
|
package activityv1
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.35.1
|
// protoc-gen-go v1.34.2
|
||||||
// protoc v5.29.3
|
// protoc v5.29.2
|
||||||
// source: proto/events/room/v1/events.proto
|
// source: proto/events/room/v1/events.proto
|
||||||
|
|
||||||
package roomeventsv1
|
package roomeventsv1
|
||||||
@ -38,9 +38,11 @@ type EventEnvelope struct {
|
|||||||
|
|
||||||
func (x *EventEnvelope) Reset() {
|
func (x *EventEnvelope) Reset() {
|
||||||
*x = EventEnvelope{}
|
*x = EventEnvelope{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[0]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[0]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *EventEnvelope) String() string {
|
func (x *EventEnvelope) String() string {
|
||||||
@ -51,7 +53,7 @@ func (*EventEnvelope) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *EventEnvelope) ProtoReflect() protoreflect.Message {
|
func (x *EventEnvelope) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[0]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[0]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -132,9 +134,11 @@ type RoomCreated struct {
|
|||||||
|
|
||||||
func (x *RoomCreated) Reset() {
|
func (x *RoomCreated) Reset() {
|
||||||
*x = RoomCreated{}
|
*x = RoomCreated{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[1]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[1]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomCreated) String() string {
|
func (x *RoomCreated) String() string {
|
||||||
@ -145,7 +149,7 @@ func (*RoomCreated) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RoomCreated) ProtoReflect() protoreflect.Message {
|
func (x *RoomCreated) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[1]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[1]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -217,9 +221,11 @@ type RoomProfileUpdated struct {
|
|||||||
|
|
||||||
func (x *RoomProfileUpdated) Reset() {
|
func (x *RoomProfileUpdated) Reset() {
|
||||||
*x = RoomProfileUpdated{}
|
*x = RoomProfileUpdated{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomProfileUpdated) String() string {
|
func (x *RoomProfileUpdated) String() string {
|
||||||
@ -230,7 +236,7 @@ func (*RoomProfileUpdated) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RoomProfileUpdated) ProtoReflect() protoreflect.Message {
|
func (x *RoomProfileUpdated) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -292,9 +298,11 @@ type RoomUserJoined struct {
|
|||||||
|
|
||||||
func (x *RoomUserJoined) Reset() {
|
func (x *RoomUserJoined) Reset() {
|
||||||
*x = RoomUserJoined{}
|
*x = RoomUserJoined{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomUserJoined) String() string {
|
func (x *RoomUserJoined) String() string {
|
||||||
@ -305,7 +313,7 @@ func (*RoomUserJoined) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RoomUserJoined) ProtoReflect() protoreflect.Message {
|
func (x *RoomUserJoined) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -345,9 +353,11 @@ type RoomUserLeft struct {
|
|||||||
|
|
||||||
func (x *RoomUserLeft) Reset() {
|
func (x *RoomUserLeft) Reset() {
|
||||||
*x = RoomUserLeft{}
|
*x = RoomUserLeft{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomUserLeft) String() string {
|
func (x *RoomUserLeft) String() string {
|
||||||
@ -358,7 +368,7 @@ func (*RoomUserLeft) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RoomUserLeft) ProtoReflect() protoreflect.Message {
|
func (x *RoomUserLeft) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -392,9 +402,11 @@ type RoomClosed struct {
|
|||||||
|
|
||||||
func (x *RoomClosed) Reset() {
|
func (x *RoomClosed) Reset() {
|
||||||
*x = RoomClosed{}
|
*x = RoomClosed{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomClosed) String() string {
|
func (x *RoomClosed) String() string {
|
||||||
@ -405,7 +417,7 @@ func (*RoomClosed) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RoomClosed) ProtoReflect() protoreflect.Message {
|
func (x *RoomClosed) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -458,9 +470,11 @@ type RoomMicChanged struct {
|
|||||||
|
|
||||||
func (x *RoomMicChanged) Reset() {
|
func (x *RoomMicChanged) Reset() {
|
||||||
*x = RoomMicChanged{}
|
*x = RoomMicChanged{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomMicChanged) String() string {
|
func (x *RoomMicChanged) String() string {
|
||||||
@ -471,7 +485,7 @@ func (*RoomMicChanged) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RoomMicChanged) ProtoReflect() protoreflect.Message {
|
func (x *RoomMicChanged) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -576,9 +590,11 @@ type RoomMicSeatLocked struct {
|
|||||||
|
|
||||||
func (x *RoomMicSeatLocked) Reset() {
|
func (x *RoomMicSeatLocked) Reset() {
|
||||||
*x = RoomMicSeatLocked{}
|
*x = RoomMicSeatLocked{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomMicSeatLocked) String() string {
|
func (x *RoomMicSeatLocked) String() string {
|
||||||
@ -589,7 +605,7 @@ func (*RoomMicSeatLocked) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message {
|
func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -637,9 +653,11 @@ type RoomChatEnabledChanged struct {
|
|||||||
|
|
||||||
func (x *RoomChatEnabledChanged) Reset() {
|
func (x *RoomChatEnabledChanged) Reset() {
|
||||||
*x = RoomChatEnabledChanged{}
|
*x = RoomChatEnabledChanged{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomChatEnabledChanged) String() string {
|
func (x *RoomChatEnabledChanged) String() string {
|
||||||
@ -650,7 +668,7 @@ func (*RoomChatEnabledChanged) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message {
|
func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -679,6 +697,62 @@ func (x *RoomChatEnabledChanged) GetEnabled() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RoomPasswordChanged 表达房间入房密码状态变更,不携带明文或哈希。
|
||||||
|
type RoomPasswordChanged struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
|
||||||
|
Locked bool `protobuf:"varint,2,opt,name=locked,proto3" json:"locked,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RoomPasswordChanged) Reset() {
|
||||||
|
*x = RoomPasswordChanged{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RoomPasswordChanged) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*RoomPasswordChanged) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *RoomPasswordChanged) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use RoomPasswordChanged.ProtoReflect.Descriptor instead.
|
||||||
|
func (*RoomPasswordChanged) Descriptor() ([]byte, []int) {
|
||||||
|
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{9}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RoomPasswordChanged) GetActorUserId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ActorUserId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *RoomPasswordChanged) GetLocked() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Locked
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// RoomAdminChanged 表达房间管理员集合变更。
|
// RoomAdminChanged 表达房间管理员集合变更。
|
||||||
type RoomAdminChanged struct {
|
type RoomAdminChanged struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
@ -692,9 +766,11 @@ type RoomAdminChanged struct {
|
|||||||
|
|
||||||
func (x *RoomAdminChanged) Reset() {
|
func (x *RoomAdminChanged) Reset() {
|
||||||
*x = RoomAdminChanged{}
|
*x = RoomAdminChanged{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomAdminChanged) String() string {
|
func (x *RoomAdminChanged) String() string {
|
||||||
@ -704,8 +780,8 @@ func (x *RoomAdminChanged) String() string {
|
|||||||
func (*RoomAdminChanged) ProtoMessage() {}
|
func (*RoomAdminChanged) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
|
func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -717,7 +793,7 @@ func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RoomAdminChanged.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RoomAdminChanged.ProtoReflect.Descriptor instead.
|
||||||
func (*RoomAdminChanged) Descriptor() ([]byte, []int) {
|
func (*RoomAdminChanged) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{9}
|
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{10}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomAdminChanged) GetActorUserId() int64 {
|
func (x *RoomAdminChanged) GetActorUserId() int64 {
|
||||||
@ -754,9 +830,11 @@ type RoomUserMuted struct {
|
|||||||
|
|
||||||
func (x *RoomUserMuted) Reset() {
|
func (x *RoomUserMuted) Reset() {
|
||||||
*x = RoomUserMuted{}
|
*x = RoomUserMuted{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomUserMuted) String() string {
|
func (x *RoomUserMuted) String() string {
|
||||||
@ -766,8 +844,8 @@ func (x *RoomUserMuted) String() string {
|
|||||||
func (*RoomUserMuted) ProtoMessage() {}
|
func (*RoomUserMuted) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
|
func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -779,7 +857,7 @@ func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RoomUserMuted.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RoomUserMuted.ProtoReflect.Descriptor instead.
|
||||||
func (*RoomUserMuted) Descriptor() ([]byte, []int) {
|
func (*RoomUserMuted) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{10}
|
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{11}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomUserMuted) GetActorUserId() int64 {
|
func (x *RoomUserMuted) GetActorUserId() int64 {
|
||||||
@ -815,9 +893,11 @@ type RoomUserKicked struct {
|
|||||||
|
|
||||||
func (x *RoomUserKicked) Reset() {
|
func (x *RoomUserKicked) Reset() {
|
||||||
*x = RoomUserKicked{}
|
*x = RoomUserKicked{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomUserKicked) String() string {
|
func (x *RoomUserKicked) String() string {
|
||||||
@ -827,8 +907,8 @@ func (x *RoomUserKicked) String() string {
|
|||||||
func (*RoomUserKicked) ProtoMessage() {}
|
func (*RoomUserKicked) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
|
func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -840,7 +920,7 @@ func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RoomUserKicked.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RoomUserKicked.ProtoReflect.Descriptor instead.
|
||||||
func (*RoomUserKicked) Descriptor() ([]byte, []int) {
|
func (*RoomUserKicked) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{11}
|
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{12}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomUserKicked) GetActorUserId() int64 {
|
func (x *RoomUserKicked) GetActorUserId() int64 {
|
||||||
@ -869,9 +949,11 @@ type RoomUserUnbanned struct {
|
|||||||
|
|
||||||
func (x *RoomUserUnbanned) Reset() {
|
func (x *RoomUserUnbanned) Reset() {
|
||||||
*x = RoomUserUnbanned{}
|
*x = RoomUserUnbanned{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomUserUnbanned) String() string {
|
func (x *RoomUserUnbanned) String() string {
|
||||||
@ -881,8 +963,8 @@ func (x *RoomUserUnbanned) String() string {
|
|||||||
func (*RoomUserUnbanned) ProtoMessage() {}
|
func (*RoomUserUnbanned) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
|
func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -894,7 +976,7 @@ func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RoomUserUnbanned.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RoomUserUnbanned.ProtoReflect.Descriptor instead.
|
||||||
func (*RoomUserUnbanned) Descriptor() ([]byte, []int) {
|
func (*RoomUserUnbanned) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{12}
|
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{13}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomUserUnbanned) GetActorUserId() int64 {
|
func (x *RoomUserUnbanned) GetActorUserId() int64 {
|
||||||
@ -929,9 +1011,11 @@ type RoomGiftSent struct {
|
|||||||
|
|
||||||
func (x *RoomGiftSent) Reset() {
|
func (x *RoomGiftSent) Reset() {
|
||||||
*x = RoomGiftSent{}
|
*x = RoomGiftSent{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomGiftSent) String() string {
|
func (x *RoomGiftSent) String() string {
|
||||||
@ -941,8 +1025,8 @@ func (x *RoomGiftSent) String() string {
|
|||||||
func (*RoomGiftSent) ProtoMessage() {}
|
func (*RoomGiftSent) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
|
func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -954,7 +1038,7 @@ func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RoomGiftSent.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RoomGiftSent.ProtoReflect.Descriptor instead.
|
||||||
func (*RoomGiftSent) Descriptor() ([]byte, []int) {
|
func (*RoomGiftSent) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{13}
|
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{14}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomGiftSent) GetSenderUserId() int64 {
|
func (x *RoomGiftSent) GetSenderUserId() int64 {
|
||||||
@ -1025,9 +1109,11 @@ type RoomHeatChanged struct {
|
|||||||
|
|
||||||
func (x *RoomHeatChanged) Reset() {
|
func (x *RoomHeatChanged) Reset() {
|
||||||
*x = RoomHeatChanged{}
|
*x = RoomHeatChanged{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomHeatChanged) String() string {
|
func (x *RoomHeatChanged) String() string {
|
||||||
@ -1037,8 +1123,8 @@ func (x *RoomHeatChanged) String() string {
|
|||||||
func (*RoomHeatChanged) ProtoMessage() {}
|
func (*RoomHeatChanged) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
|
func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1050,7 +1136,7 @@ func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RoomHeatChanged.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RoomHeatChanged.ProtoReflect.Descriptor instead.
|
||||||
func (*RoomHeatChanged) Descriptor() ([]byte, []int) {
|
func (*RoomHeatChanged) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{14}
|
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{15}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomHeatChanged) GetDelta() int64 {
|
func (x *RoomHeatChanged) GetDelta() int64 {
|
||||||
@ -1080,9 +1166,11 @@ type RoomRankChanged struct {
|
|||||||
|
|
||||||
func (x *RoomRankChanged) Reset() {
|
func (x *RoomRankChanged) Reset() {
|
||||||
*x = RoomRankChanged{}
|
*x = RoomRankChanged{}
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomRankChanged) String() string {
|
func (x *RoomRankChanged) String() string {
|
||||||
@ -1092,8 +1180,8 @@ func (x *RoomRankChanged) String() string {
|
|||||||
func (*RoomRankChanged) ProtoMessage() {}
|
func (*RoomRankChanged) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
|
func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1105,7 +1193,7 @@ func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use RoomRankChanged.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RoomRankChanged.ProtoReflect.Descriptor instead.
|
||||||
func (*RoomRankChanged) Descriptor() ([]byte, []int) {
|
func (*RoomRankChanged) Descriptor() ([]byte, []int) {
|
||||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{15}
|
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{16}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomRankChanged) GetUserId() int64 {
|
func (x *RoomRankChanged) GetUserId() int64 {
|
||||||
@ -1222,67 +1310,72 @@ var file_proto_events_room_v1_events_proto_rawDesc = []byte{
|
|||||||
0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01,
|
0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01,
|
||||||
0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49,
|
0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49,
|
||||||
0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01,
|
0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01,
|
||||||
0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x76, 0x0a, 0x10, 0x52,
|
0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x51, 0x0a, 0x13, 0x52,
|
||||||
0x6f, 0x6f, 0x6d, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12,
|
0x6f, 0x6f, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67,
|
||||||
0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64,
|
0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72,
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65,
|
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72,
|
||||||
0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73,
|
0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64,
|
||||||
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72,
|
0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x22, 0x76,
|
||||||
0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61,
|
0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67,
|
||||||
0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62,
|
0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72,
|
||||||
0x6c, 0x65, 0x64, 0x22, 0x6f, 0x0a, 0x0d, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x4d,
|
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72,
|
||||||
0x75, 0x74, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73,
|
0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74,
|
||||||
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74,
|
0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c,
|
||||||
0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
|
0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07,
|
||||||
|
0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65,
|
||||||
|
0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x6f, 0x0a, 0x0d, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73,
|
||||||
|
0x65, 0x72, 0x4d, 0x75, 0x74, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72,
|
||||||
|
0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
|
||||||
|
0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74,
|
||||||
|
0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
|
||||||
|
0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49,
|
||||||
|
0x64, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
|
||||||
|
0x52, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x22, 0x5a, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d, 0x55,
|
||||||
|
0x73, 0x65, 0x72, 0x4b, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74,
|
||||||
|
0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
|
||||||
|
0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a,
|
||||||
|
0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
|
||||||
|
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65,
|
||||||
|
0x72, 0x49, 0x64, 0x22, 0x5c, 0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x55,
|
||||||
|
0x6e, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72,
|
||||||
|
0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
|
||||||
|
0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74,
|
||||||
|
0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
|
||||||
|
0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49,
|
||||||
|
0x64, 0x22, 0xaa, 0x02, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65,
|
||||||
|
0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65,
|
||||||
|
0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64,
|
||||||
|
0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
|
||||||
0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
|
0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
|
||||||
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14,
|
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17,
|
||||||
0x0a, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x6d,
|
0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x75, 0x74, 0x65, 0x64, 0x22, 0x5a, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72,
|
0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f,
|
||||||
0x4b, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f,
|
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66,
|
||||||
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61,
|
0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76,
|
||||||
0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61,
|
0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74,
|
||||||
0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
|
0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67,
|
||||||
0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64,
|
0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28,
|
||||||
0x22, 0x5c, 0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x55, 0x6e, 0x62, 0x61,
|
0x09, 0x52, 0x10, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70,
|
||||||
0x6e, 0x6e, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73,
|
0x74, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72,
|
||||||
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74,
|
0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f,
|
||||||
0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
|
0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12,
|
||||||
0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
|
0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20,
|
||||||
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xaa,
|
0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x22, 0x4a,
|
||||||
0x02, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x6e, 0x74, 0x12,
|
0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
|
||||||
0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
|
0x64, 0x12, 0x14, 0x0a, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
|
||||||
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x55,
|
0x52, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65,
|
||||||
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f,
|
0x6e, 0x74, 0x5f, 0x68, 0x65, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63,
|
||||||
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74,
|
0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x65, 0x61, 0x74, 0x22, 0x5f, 0x0a, 0x0f, 0x52, 0x6f,
|
||||||
0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67,
|
0x6f, 0x6d, 0x52, 0x61, 0x6e, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x17, 0x0a,
|
||||||
0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69,
|
0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06,
|
||||||
0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75,
|
0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18,
|
||||||
0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f,
|
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a,
|
||||||
0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75,
|
0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
|
||||||
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c,
|
0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x68,
|
||||||
0x75, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x65,
|
0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70,
|
||||||
0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10,
|
0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x72, 0x6f, 0x6f, 0x6d,
|
||||||
0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x49, 0x64,
|
0x2f, 0x76, 0x31, 0x3b, 0x72, 0x6f, 0x6f, 0x6d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x76, 0x31,
|
||||||
0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69,
|
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73,
|
|
||||||
0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a,
|
|
||||||
0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09,
|
|
||||||
0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x22, 0x4a, 0x0a, 0x0f, 0x52,
|
|
||||||
0x6f, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x14,
|
|
||||||
0x0a, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x64,
|
|
||||||
0x65, 0x6c, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f,
|
|
||||||
0x68, 0x65, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72,
|
|
||||||
0x65, 0x6e, 0x74, 0x48, 0x65, 0x61, 0x74, 0x22, 0x5f, 0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x6d, 0x52,
|
|
||||||
0x61, 0x6e, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73,
|
|
||||||
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65,
|
|
||||||
0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01,
|
|
||||||
0x28, 0x03, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66,
|
|
||||||
0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67,
|
|
||||||
0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x68, 0x79, 0x61, 0x70,
|
|
||||||
0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
|
||||||
0x6f, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x72, 0x6f, 0x6f, 0x6d, 0x2f, 0x76, 0x31,
|
|
||||||
0x3b, 0x72, 0x6f, 0x6f, 0x6d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70,
|
|
||||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -1297,7 +1390,7 @@ func file_proto_events_room_v1_events_proto_rawDescGZIP() []byte {
|
|||||||
return file_proto_events_room_v1_events_proto_rawDescData
|
return file_proto_events_room_v1_events_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
|
var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
|
||||||
var file_proto_events_room_v1_events_proto_goTypes = []any{
|
var file_proto_events_room_v1_events_proto_goTypes = []any{
|
||||||
(*EventEnvelope)(nil), // 0: hyapp.events.room.v1.EventEnvelope
|
(*EventEnvelope)(nil), // 0: hyapp.events.room.v1.EventEnvelope
|
||||||
(*RoomCreated)(nil), // 1: hyapp.events.room.v1.RoomCreated
|
(*RoomCreated)(nil), // 1: hyapp.events.room.v1.RoomCreated
|
||||||
@ -1308,13 +1401,14 @@ var file_proto_events_room_v1_events_proto_goTypes = []any{
|
|||||||
(*RoomMicChanged)(nil), // 6: hyapp.events.room.v1.RoomMicChanged
|
(*RoomMicChanged)(nil), // 6: hyapp.events.room.v1.RoomMicChanged
|
||||||
(*RoomMicSeatLocked)(nil), // 7: hyapp.events.room.v1.RoomMicSeatLocked
|
(*RoomMicSeatLocked)(nil), // 7: hyapp.events.room.v1.RoomMicSeatLocked
|
||||||
(*RoomChatEnabledChanged)(nil), // 8: hyapp.events.room.v1.RoomChatEnabledChanged
|
(*RoomChatEnabledChanged)(nil), // 8: hyapp.events.room.v1.RoomChatEnabledChanged
|
||||||
(*RoomAdminChanged)(nil), // 9: hyapp.events.room.v1.RoomAdminChanged
|
(*RoomPasswordChanged)(nil), // 9: hyapp.events.room.v1.RoomPasswordChanged
|
||||||
(*RoomUserMuted)(nil), // 10: hyapp.events.room.v1.RoomUserMuted
|
(*RoomAdminChanged)(nil), // 10: hyapp.events.room.v1.RoomAdminChanged
|
||||||
(*RoomUserKicked)(nil), // 11: hyapp.events.room.v1.RoomUserKicked
|
(*RoomUserMuted)(nil), // 11: hyapp.events.room.v1.RoomUserMuted
|
||||||
(*RoomUserUnbanned)(nil), // 12: hyapp.events.room.v1.RoomUserUnbanned
|
(*RoomUserKicked)(nil), // 12: hyapp.events.room.v1.RoomUserKicked
|
||||||
(*RoomGiftSent)(nil), // 13: hyapp.events.room.v1.RoomGiftSent
|
(*RoomUserUnbanned)(nil), // 13: hyapp.events.room.v1.RoomUserUnbanned
|
||||||
(*RoomHeatChanged)(nil), // 14: hyapp.events.room.v1.RoomHeatChanged
|
(*RoomGiftSent)(nil), // 14: hyapp.events.room.v1.RoomGiftSent
|
||||||
(*RoomRankChanged)(nil), // 15: hyapp.events.room.v1.RoomRankChanged
|
(*RoomHeatChanged)(nil), // 15: hyapp.events.room.v1.RoomHeatChanged
|
||||||
|
(*RoomRankChanged)(nil), // 16: hyapp.events.room.v1.RoomRankChanged
|
||||||
}
|
}
|
||||||
var file_proto_events_room_v1_events_proto_depIdxs = []int32{
|
var file_proto_events_room_v1_events_proto_depIdxs = []int32{
|
||||||
0, // [0:0] is the sub-list for method output_type
|
0, // [0:0] is the sub-list for method output_type
|
||||||
@ -1329,13 +1423,219 @@ func file_proto_events_room_v1_events_proto_init() {
|
|||||||
if File_proto_events_room_v1_events_proto != nil {
|
if File_proto_events_room_v1_events_proto != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*EventEnvelope); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomCreated); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomProfileUpdated); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomUserJoined); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomUserLeft); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomClosed); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomMicChanged); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomMicSeatLocked); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomChatEnabledChanged); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomPasswordChanged); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomAdminChanged); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[11].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomUserMuted); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[12].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomUserKicked); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[13].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomUserUnbanned); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[14].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomGiftSent); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[15].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomHeatChanged); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_events_room_v1_events_proto_msgTypes[16].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RoomRankChanged); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: file_proto_events_room_v1_events_proto_rawDesc,
|
RawDescriptor: file_proto_events_room_v1_events_proto_rawDesc,
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 16,
|
NumMessages: 17,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 0,
|
NumServices: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -84,6 +84,12 @@ message RoomChatEnabledChanged {
|
|||||||
bool enabled = 2;
|
bool enabled = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RoomPasswordChanged 表达房间入房密码状态变更,不携带明文或哈希。
|
||||||
|
message RoomPasswordChanged {
|
||||||
|
int64 actor_user_id = 1;
|
||||||
|
bool locked = 2;
|
||||||
|
}
|
||||||
|
|
||||||
// RoomAdminChanged 表达房间管理员集合变更。
|
// RoomAdminChanged 表达房间管理员集合变更。
|
||||||
message RoomAdminChanged {
|
message RoomAdminChanged {
|
||||||
int64 actor_user_id = 1;
|
int64 actor_user_id = 1;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.35.1
|
// protoc-gen-go v1.34.2
|
||||||
// protoc v5.29.3
|
// protoc v5.29.2
|
||||||
// source: proto/game/v1/game.proto
|
// source: proto/game/v1/game.proto
|
||||||
|
|
||||||
package gamev1
|
package gamev1
|
||||||
@ -36,9 +36,11 @@ type RequestMeta struct {
|
|||||||
|
|
||||||
func (x *RequestMeta) Reset() {
|
func (x *RequestMeta) Reset() {
|
||||||
*x = RequestMeta{}
|
*x = RequestMeta{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[0]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[0]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RequestMeta) String() string {
|
func (x *RequestMeta) String() string {
|
||||||
@ -49,7 +51,7 @@ func (*RequestMeta) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RequestMeta) ProtoReflect() protoreflect.Message {
|
func (x *RequestMeta) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[0]
|
mi := &file_proto_game_v1_game_proto_msgTypes[0]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -123,9 +125,11 @@ type GamePlatform struct {
|
|||||||
|
|
||||||
func (x *GamePlatform) Reset() {
|
func (x *GamePlatform) Reset() {
|
||||||
*x = GamePlatform{}
|
*x = GamePlatform{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[1]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[1]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GamePlatform) String() string {
|
func (x *GamePlatform) String() string {
|
||||||
@ -136,7 +140,7 @@ func (*GamePlatform) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *GamePlatform) ProtoReflect() protoreflect.Message {
|
func (x *GamePlatform) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[1]
|
mi := &file_proto_game_v1_game_proto_msgTypes[1]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -232,9 +236,11 @@ type GameCatalogItem struct {
|
|||||||
|
|
||||||
func (x *GameCatalogItem) Reset() {
|
func (x *GameCatalogItem) Reset() {
|
||||||
*x = GameCatalogItem{}
|
*x = GameCatalogItem{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[2]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[2]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GameCatalogItem) String() string {
|
func (x *GameCatalogItem) String() string {
|
||||||
@ -245,7 +251,7 @@ func (*GameCatalogItem) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *GameCatalogItem) ProtoReflect() protoreflect.Message {
|
func (x *GameCatalogItem) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[2]
|
mi := &file_proto_game_v1_game_proto_msgTypes[2]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -394,9 +400,11 @@ type AppGame struct {
|
|||||||
|
|
||||||
func (x *AppGame) Reset() {
|
func (x *AppGame) Reset() {
|
||||||
*x = AppGame{}
|
*x = AppGame{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[3]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[3]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *AppGame) String() string {
|
func (x *AppGame) String() string {
|
||||||
@ -407,7 +415,7 @@ func (*AppGame) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *AppGame) ProtoReflect() protoreflect.Message {
|
func (x *AppGame) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[3]
|
mi := &file_proto_game_v1_game_proto_msgTypes[3]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -529,9 +537,11 @@ type ListGamesRequest struct {
|
|||||||
|
|
||||||
func (x *ListGamesRequest) Reset() {
|
func (x *ListGamesRequest) Reset() {
|
||||||
*x = ListGamesRequest{}
|
*x = ListGamesRequest{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[4]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[4]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListGamesRequest) String() string {
|
func (x *ListGamesRequest) String() string {
|
||||||
@ -542,7 +552,7 @@ func (*ListGamesRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *ListGamesRequest) ProtoReflect() protoreflect.Message {
|
func (x *ListGamesRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[4]
|
mi := &file_proto_game_v1_game_proto_msgTypes[4]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -617,9 +627,11 @@ type ListGamesResponse struct {
|
|||||||
|
|
||||||
func (x *ListGamesResponse) Reset() {
|
func (x *ListGamesResponse) Reset() {
|
||||||
*x = ListGamesResponse{}
|
*x = ListGamesResponse{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[5]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[5]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListGamesResponse) String() string {
|
func (x *ListGamesResponse) String() string {
|
||||||
@ -630,7 +642,7 @@ func (*ListGamesResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *ListGamesResponse) ProtoReflect() protoreflect.Message {
|
func (x *ListGamesResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[5]
|
mi := &file_proto_game_v1_game_proto_msgTypes[5]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -674,9 +686,11 @@ type LaunchGameRequest struct {
|
|||||||
|
|
||||||
func (x *LaunchGameRequest) Reset() {
|
func (x *LaunchGameRequest) Reset() {
|
||||||
*x = LaunchGameRequest{}
|
*x = LaunchGameRequest{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[6]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[6]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *LaunchGameRequest) String() string {
|
func (x *LaunchGameRequest) String() string {
|
||||||
@ -687,7 +701,7 @@ func (*LaunchGameRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *LaunchGameRequest) ProtoReflect() protoreflect.Message {
|
func (x *LaunchGameRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[6]
|
mi := &file_proto_game_v1_game_proto_msgTypes[6]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -758,9 +772,11 @@ type LaunchGameResponse struct {
|
|||||||
|
|
||||||
func (x *LaunchGameResponse) Reset() {
|
func (x *LaunchGameResponse) Reset() {
|
||||||
*x = LaunchGameResponse{}
|
*x = LaunchGameResponse{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[7]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[7]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *LaunchGameResponse) String() string {
|
func (x *LaunchGameResponse) String() string {
|
||||||
@ -771,7 +787,7 @@ func (*LaunchGameResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *LaunchGameResponse) ProtoReflect() protoreflect.Message {
|
func (x *LaunchGameResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[7]
|
mi := &file_proto_game_v1_game_proto_msgTypes[7]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -837,9 +853,11 @@ type CallbackRequest struct {
|
|||||||
|
|
||||||
func (x *CallbackRequest) Reset() {
|
func (x *CallbackRequest) Reset() {
|
||||||
*x = CallbackRequest{}
|
*x = CallbackRequest{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[8]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[8]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CallbackRequest) String() string {
|
func (x *CallbackRequest) String() string {
|
||||||
@ -850,7 +868,7 @@ func (*CallbackRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *CallbackRequest) ProtoReflect() protoreflect.Message {
|
func (x *CallbackRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[8]
|
mi := &file_proto_game_v1_game_proto_msgTypes[8]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -925,9 +943,11 @@ type CallbackResponse struct {
|
|||||||
|
|
||||||
func (x *CallbackResponse) Reset() {
|
func (x *CallbackResponse) Reset() {
|
||||||
*x = CallbackResponse{}
|
*x = CallbackResponse{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[9]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[9]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CallbackResponse) String() string {
|
func (x *CallbackResponse) String() string {
|
||||||
@ -938,7 +958,7 @@ func (*CallbackResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *CallbackResponse) ProtoReflect() protoreflect.Message {
|
func (x *CallbackResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[9]
|
mi := &file_proto_game_v1_game_proto_msgTypes[9]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -978,9 +998,11 @@ type ListPlatformsRequest struct {
|
|||||||
|
|
||||||
func (x *ListPlatformsRequest) Reset() {
|
func (x *ListPlatformsRequest) Reset() {
|
||||||
*x = ListPlatformsRequest{}
|
*x = ListPlatformsRequest{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[10]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[10]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListPlatformsRequest) String() string {
|
func (x *ListPlatformsRequest) String() string {
|
||||||
@ -991,7 +1013,7 @@ func (*ListPlatformsRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *ListPlatformsRequest) ProtoReflect() protoreflect.Message {
|
func (x *ListPlatformsRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[10]
|
mi := &file_proto_game_v1_game_proto_msgTypes[10]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1031,9 +1053,11 @@ type ListPlatformsResponse struct {
|
|||||||
|
|
||||||
func (x *ListPlatformsResponse) Reset() {
|
func (x *ListPlatformsResponse) Reset() {
|
||||||
*x = ListPlatformsResponse{}
|
*x = ListPlatformsResponse{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[11]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[11]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListPlatformsResponse) String() string {
|
func (x *ListPlatformsResponse) String() string {
|
||||||
@ -1044,7 +1068,7 @@ func (*ListPlatformsResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *ListPlatformsResponse) ProtoReflect() protoreflect.Message {
|
func (x *ListPlatformsResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[11]
|
mi := &file_proto_game_v1_game_proto_msgTypes[11]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1084,9 +1108,11 @@ type UpsertPlatformRequest struct {
|
|||||||
|
|
||||||
func (x *UpsertPlatformRequest) Reset() {
|
func (x *UpsertPlatformRequest) Reset() {
|
||||||
*x = UpsertPlatformRequest{}
|
*x = UpsertPlatformRequest{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[12]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[12]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *UpsertPlatformRequest) String() string {
|
func (x *UpsertPlatformRequest) String() string {
|
||||||
@ -1097,7 +1123,7 @@ func (*UpsertPlatformRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *UpsertPlatformRequest) ProtoReflect() protoreflect.Message {
|
func (x *UpsertPlatformRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[12]
|
mi := &file_proto_game_v1_game_proto_msgTypes[12]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1137,9 +1163,11 @@ type PlatformResponse struct {
|
|||||||
|
|
||||||
func (x *PlatformResponse) Reset() {
|
func (x *PlatformResponse) Reset() {
|
||||||
*x = PlatformResponse{}
|
*x = PlatformResponse{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[13]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[13]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *PlatformResponse) String() string {
|
func (x *PlatformResponse) String() string {
|
||||||
@ -1150,7 +1178,7 @@ func (*PlatformResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *PlatformResponse) ProtoReflect() protoreflect.Message {
|
func (x *PlatformResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[13]
|
mi := &file_proto_game_v1_game_proto_msgTypes[13]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1193,9 +1221,11 @@ type ListCatalogRequest struct {
|
|||||||
|
|
||||||
func (x *ListCatalogRequest) Reset() {
|
func (x *ListCatalogRequest) Reset() {
|
||||||
*x = ListCatalogRequest{}
|
*x = ListCatalogRequest{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[14]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[14]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListCatalogRequest) String() string {
|
func (x *ListCatalogRequest) String() string {
|
||||||
@ -1206,7 +1236,7 @@ func (*ListCatalogRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *ListCatalogRequest) ProtoReflect() protoreflect.Message {
|
func (x *ListCatalogRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[14]
|
mi := &file_proto_game_v1_game_proto_msgTypes[14]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1268,9 +1298,11 @@ type ListCatalogResponse struct {
|
|||||||
|
|
||||||
func (x *ListCatalogResponse) Reset() {
|
func (x *ListCatalogResponse) Reset() {
|
||||||
*x = ListCatalogResponse{}
|
*x = ListCatalogResponse{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[15]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[15]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ListCatalogResponse) String() string {
|
func (x *ListCatalogResponse) String() string {
|
||||||
@ -1281,7 +1313,7 @@ func (*ListCatalogResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *ListCatalogResponse) ProtoReflect() protoreflect.Message {
|
func (x *ListCatalogResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[15]
|
mi := &file_proto_game_v1_game_proto_msgTypes[15]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1328,9 +1360,11 @@ type UpsertCatalogRequest struct {
|
|||||||
|
|
||||||
func (x *UpsertCatalogRequest) Reset() {
|
func (x *UpsertCatalogRequest) Reset() {
|
||||||
*x = UpsertCatalogRequest{}
|
*x = UpsertCatalogRequest{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[16]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[16]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *UpsertCatalogRequest) String() string {
|
func (x *UpsertCatalogRequest) String() string {
|
||||||
@ -1341,7 +1375,7 @@ func (*UpsertCatalogRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *UpsertCatalogRequest) ProtoReflect() protoreflect.Message {
|
func (x *UpsertCatalogRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[16]
|
mi := &file_proto_game_v1_game_proto_msgTypes[16]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1381,9 +1415,11 @@ type CatalogResponse struct {
|
|||||||
|
|
||||||
func (x *CatalogResponse) Reset() {
|
func (x *CatalogResponse) Reset() {
|
||||||
*x = CatalogResponse{}
|
*x = CatalogResponse{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[17]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[17]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CatalogResponse) String() string {
|
func (x *CatalogResponse) String() string {
|
||||||
@ -1394,7 +1430,7 @@ func (*CatalogResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *CatalogResponse) ProtoReflect() protoreflect.Message {
|
func (x *CatalogResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[17]
|
mi := &file_proto_game_v1_game_proto_msgTypes[17]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1435,9 +1471,11 @@ type SetGameStatusRequest struct {
|
|||||||
|
|
||||||
func (x *SetGameStatusRequest) Reset() {
|
func (x *SetGameStatusRequest) Reset() {
|
||||||
*x = SetGameStatusRequest{}
|
*x = SetGameStatusRequest{}
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[18]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_game_v1_game_proto_msgTypes[18]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SetGameStatusRequest) String() string {
|
func (x *SetGameStatusRequest) String() string {
|
||||||
@ -1448,7 +1486,7 @@ func (*SetGameStatusRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *SetGameStatusRequest) ProtoReflect() protoreflect.Message {
|
func (x *SetGameStatusRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_game_v1_game_proto_msgTypes[18]
|
mi := &file_proto_game_v1_game_proto_msgTypes[18]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1862,6 +1900,236 @@ func file_proto_game_v1_game_proto_init() {
|
|||||||
if File_proto_game_v1_game_proto != nil {
|
if File_proto_game_v1_game_proto != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RequestMeta); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*GamePlatform); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*GameCatalogItem); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*AppGame); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ListGamesRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ListGamesResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*LaunchGameRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*LaunchGameResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*CallbackRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*CallbackResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ListPlatformsRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[11].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ListPlatformsResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[12].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*UpsertPlatformRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[13].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*PlatformResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[14].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ListCatalogRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[15].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*ListCatalogResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[16].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*UpsertCatalogRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[17].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*CatalogResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_game_v1_game_proto_msgTypes[18].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*SetGameStatusRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.5.1
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
// - protoc v5.29.3
|
// - protoc v5.29.2
|
||||||
// source: proto/game/v1/game.proto
|
// source: proto/game/v1/game.proto
|
||||||
|
|
||||||
package gamev1
|
package gamev1
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -80,6 +80,8 @@ message RoomSnapshot {
|
|||||||
string app_code = 16;
|
string app_code = 16;
|
||||||
string room_short_id = 17;
|
string room_short_id = 17;
|
||||||
int64 visible_region_id = 18;
|
int64 visible_region_id = 18;
|
||||||
|
// locked 只表达房间是否需要入房密码;具体密码哈希不对外映射到 HTTP JSON。
|
||||||
|
bool locked = 19;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRoomRequest 创建一个新的房间执行单元。
|
// CreateRoomRequest 创建一个新的房间执行单元。
|
||||||
@ -129,6 +131,8 @@ message UpdateRoomProfileResponse {
|
|||||||
message JoinRoomRequest {
|
message JoinRoomRequest {
|
||||||
RequestMeta meta = 1;
|
RequestMeta meta = 1;
|
||||||
string role = 2;
|
string role = 2;
|
||||||
|
// password 只用于本次入房校验;room-service 不把明文写入 command log 或快照。
|
||||||
|
string password = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinRoomResponse 返回加入后的房间快照。
|
// JoinRoomResponse 返回加入后的房间快照。
|
||||||
@ -295,6 +299,20 @@ message SetChatEnabledResponse {
|
|||||||
RoomSnapshot room = 2;
|
RoomSnapshot room = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetRoomPasswordRequest 设置或清空房间入房密码。
|
||||||
|
// locked=true 时 password 必须非空;locked=false 表示清空密码并解除锁房。
|
||||||
|
message SetRoomPasswordRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
bool locked = 2;
|
||||||
|
string password = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRoomPasswordResponse 返回锁房状态变更后的最新房间快照。
|
||||||
|
message SetRoomPasswordResponse {
|
||||||
|
CommandResult result = 1;
|
||||||
|
RoomSnapshot room = 2;
|
||||||
|
}
|
||||||
|
|
||||||
// SetRoomAdminRequest 添加或移除房间管理员。
|
// SetRoomAdminRequest 添加或移除房间管理员。
|
||||||
message SetRoomAdminRequest {
|
message SetRoomAdminRequest {
|
||||||
RequestMeta meta = 1;
|
RequestMeta meta = 1;
|
||||||
@ -331,6 +349,10 @@ message KickUserRequest {
|
|||||||
message KickUserResponse {
|
message KickUserResponse {
|
||||||
CommandResult result = 1;
|
CommandResult result = 1;
|
||||||
RoomSnapshot room = 2;
|
RoomSnapshot room = 2;
|
||||||
|
// rtc_kicked 表示 room-service 已通过 TRTC 服务端接口移除目标用户;失败不回滚房间踢人事实。
|
||||||
|
bool rtc_kicked = 3;
|
||||||
|
// rtc_kick_error 仅用于调用方排障和告警,客户端展示仍以统一错误码和房间状态为准。
|
||||||
|
string rtc_kick_error = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnbanUserRequest 解除房间内 ban,用户仍需重新 JoinRoom。
|
// UnbanUserRequest 解除房间内 ban,用户仍需重新 JoinRoom。
|
||||||
@ -429,7 +451,7 @@ message ListRoomsRequest {
|
|||||||
string query = 7;
|
string query = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListRoomFeedsRequest 查询 Mine 页 visited/friend/following 用户房间流。
|
// ListRoomFeedsRequest 查询 Mine 页 visited/friend/following/followed 房间流。
|
||||||
// 它和公共房间发现列表分离,避免把用户关系流误建模成房间全集过滤。
|
// 它和公共房间发现列表分离,避免把用户关系流误建模成房间全集过滤。
|
||||||
message ListRoomFeedsRequest {
|
message ListRoomFeedsRequest {
|
||||||
RequestMeta meta = 1;
|
RequestMeta meta = 1;
|
||||||
@ -464,6 +486,7 @@ message RoomListItem {
|
|||||||
int64 visible_region_id = 12;
|
int64 visible_region_id = 12;
|
||||||
string app_code = 13;
|
string app_code = 13;
|
||||||
string room_short_id = 14;
|
string room_short_id = 14;
|
||||||
|
bool locked = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListRoomsResponse 返回一页房间卡片和下一页不透明 cursor。
|
// ListRoomsResponse 返回一页房间卡片和下一页不透明 cursor。
|
||||||
@ -520,6 +543,8 @@ message GetRoomSnapshotRequest {
|
|||||||
message GetRoomSnapshotResponse {
|
message GetRoomSnapshotResponse {
|
||||||
RoomSnapshot room = 1;
|
RoomSnapshot room = 1;
|
||||||
int64 server_time_ms = 2;
|
int64 server_time_ms = 2;
|
||||||
|
// is_followed 表达 viewer 是否已经关注该房间;它来自 room_follows 关系表,不属于 Room Cell 核心快照。
|
||||||
|
bool is_followed = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListRoomOnlineUsersRequest 分页查询房间在线用户,不让客户端拉完整 RoomSnapshot 做列表分页。
|
// ListRoomOnlineUsersRequest 分页查询房间在线用户,不让客户端拉完整 RoomSnapshot 做列表分页。
|
||||||
@ -539,6 +564,34 @@ message ListRoomOnlineUsersResponse {
|
|||||||
int64 server_time_ms = 5;
|
int64 server_time_ms = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FollowRoomRequest 建立当前用户对房间的关注关系。
|
||||||
|
// 该关系是 room-service 的用户-房间低频关系事实,不进入 Room Cell command log。
|
||||||
|
message FollowRoomRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
string room_id = 2;
|
||||||
|
int64 user_id = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FollowRoomResponse {
|
||||||
|
string room_id = 1;
|
||||||
|
bool followed = 2;
|
||||||
|
int64 followed_at_ms = 3;
|
||||||
|
int64 server_time_ms = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnfollowRoomRequest 取消当前用户对房间的关注关系;未关注时按幂等成功处理。
|
||||||
|
message UnfollowRoomRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
string room_id = 2;
|
||||||
|
int64 user_id = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UnfollowRoomResponse {
|
||||||
|
string room_id = 1;
|
||||||
|
bool followed = 2;
|
||||||
|
int64 server_time_ms = 3;
|
||||||
|
}
|
||||||
|
|
||||||
// RoomCommandService 承载所有会改变房间状态的命令。
|
// RoomCommandService 承载所有会改变房间状态的命令。
|
||||||
service RoomCommandService {
|
service RoomCommandService {
|
||||||
rpc CreateRoom(CreateRoomRequest) returns (CreateRoomResponse);
|
rpc CreateRoom(CreateRoomRequest) returns (CreateRoomResponse);
|
||||||
@ -555,12 +608,15 @@ service RoomCommandService {
|
|||||||
rpc ApplyRTCEvent(ApplyRTCEventRequest) returns (ApplyRTCEventResponse);
|
rpc ApplyRTCEvent(ApplyRTCEventRequest) returns (ApplyRTCEventResponse);
|
||||||
rpc SetMicSeatLock(SetMicSeatLockRequest) returns (SetMicSeatLockResponse);
|
rpc SetMicSeatLock(SetMicSeatLockRequest) returns (SetMicSeatLockResponse);
|
||||||
rpc SetChatEnabled(SetChatEnabledRequest) returns (SetChatEnabledResponse);
|
rpc SetChatEnabled(SetChatEnabledRequest) returns (SetChatEnabledResponse);
|
||||||
|
rpc SetRoomPassword(SetRoomPasswordRequest) returns (SetRoomPasswordResponse);
|
||||||
rpc SetRoomAdmin(SetRoomAdminRequest) returns (SetRoomAdminResponse);
|
rpc SetRoomAdmin(SetRoomAdminRequest) returns (SetRoomAdminResponse);
|
||||||
rpc MuteUser(MuteUserRequest) returns (MuteUserResponse);
|
rpc MuteUser(MuteUserRequest) returns (MuteUserResponse);
|
||||||
rpc KickUser(KickUserRequest) returns (KickUserResponse);
|
rpc KickUser(KickUserRequest) returns (KickUserResponse);
|
||||||
rpc UnbanUser(UnbanUserRequest) returns (UnbanUserResponse);
|
rpc UnbanUser(UnbanUserRequest) returns (UnbanUserResponse);
|
||||||
rpc SystemEvictUser(SystemEvictUserRequest) returns (SystemEvictUserResponse);
|
rpc SystemEvictUser(SystemEvictUserRequest) returns (SystemEvictUserResponse);
|
||||||
rpc SendGift(SendGiftRequest) returns (SendGiftResponse);
|
rpc SendGift(SendGiftRequest) returns (SendGiftResponse);
|
||||||
|
rpc FollowRoom(FollowRoomRequest) returns (FollowRoomResponse);
|
||||||
|
rpc UnfollowRoom(UnfollowRoomRequest) returns (UnfollowRoomResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomGuardService 提供给腾讯云 IM 回调或 gateway 做实时守卫查询。
|
// RoomGuardService 提供给腾讯云 IM 回调或 gateway 做实时守卫查询。
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.5.1
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
// - protoc v5.29.3
|
// - protoc v5.29.2
|
||||||
// source: proto/room/v1/room.proto
|
// source: proto/room/v1/room.proto
|
||||||
|
|
||||||
package roomv1
|
package roomv1
|
||||||
@ -33,12 +33,15 @@ const (
|
|||||||
RoomCommandService_ApplyRTCEvent_FullMethodName = "/hyapp.room.v1.RoomCommandService/ApplyRTCEvent"
|
RoomCommandService_ApplyRTCEvent_FullMethodName = "/hyapp.room.v1.RoomCommandService/ApplyRTCEvent"
|
||||||
RoomCommandService_SetMicSeatLock_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicSeatLock"
|
RoomCommandService_SetMicSeatLock_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicSeatLock"
|
||||||
RoomCommandService_SetChatEnabled_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetChatEnabled"
|
RoomCommandService_SetChatEnabled_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetChatEnabled"
|
||||||
|
RoomCommandService_SetRoomPassword_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomPassword"
|
||||||
RoomCommandService_SetRoomAdmin_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomAdmin"
|
RoomCommandService_SetRoomAdmin_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomAdmin"
|
||||||
RoomCommandService_MuteUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/MuteUser"
|
RoomCommandService_MuteUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/MuteUser"
|
||||||
RoomCommandService_KickUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/KickUser"
|
RoomCommandService_KickUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/KickUser"
|
||||||
RoomCommandService_UnbanUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/UnbanUser"
|
RoomCommandService_UnbanUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/UnbanUser"
|
||||||
RoomCommandService_SystemEvictUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/SystemEvictUser"
|
RoomCommandService_SystemEvictUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/SystemEvictUser"
|
||||||
RoomCommandService_SendGift_FullMethodName = "/hyapp.room.v1.RoomCommandService/SendGift"
|
RoomCommandService_SendGift_FullMethodName = "/hyapp.room.v1.RoomCommandService/SendGift"
|
||||||
|
RoomCommandService_FollowRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/FollowRoom"
|
||||||
|
RoomCommandService_UnfollowRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/UnfollowRoom"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RoomCommandServiceClient is the client API for RoomCommandService service.
|
// RoomCommandServiceClient is the client API for RoomCommandService service.
|
||||||
@ -61,12 +64,15 @@ type RoomCommandServiceClient interface {
|
|||||||
ApplyRTCEvent(ctx context.Context, in *ApplyRTCEventRequest, opts ...grpc.CallOption) (*ApplyRTCEventResponse, error)
|
ApplyRTCEvent(ctx context.Context, in *ApplyRTCEventRequest, opts ...grpc.CallOption) (*ApplyRTCEventResponse, error)
|
||||||
SetMicSeatLock(ctx context.Context, in *SetMicSeatLockRequest, opts ...grpc.CallOption) (*SetMicSeatLockResponse, error)
|
SetMicSeatLock(ctx context.Context, in *SetMicSeatLockRequest, opts ...grpc.CallOption) (*SetMicSeatLockResponse, error)
|
||||||
SetChatEnabled(ctx context.Context, in *SetChatEnabledRequest, opts ...grpc.CallOption) (*SetChatEnabledResponse, error)
|
SetChatEnabled(ctx context.Context, in *SetChatEnabledRequest, opts ...grpc.CallOption) (*SetChatEnabledResponse, error)
|
||||||
|
SetRoomPassword(ctx context.Context, in *SetRoomPasswordRequest, opts ...grpc.CallOption) (*SetRoomPasswordResponse, error)
|
||||||
SetRoomAdmin(ctx context.Context, in *SetRoomAdminRequest, opts ...grpc.CallOption) (*SetRoomAdminResponse, error)
|
SetRoomAdmin(ctx context.Context, in *SetRoomAdminRequest, opts ...grpc.CallOption) (*SetRoomAdminResponse, error)
|
||||||
MuteUser(ctx context.Context, in *MuteUserRequest, opts ...grpc.CallOption) (*MuteUserResponse, error)
|
MuteUser(ctx context.Context, in *MuteUserRequest, opts ...grpc.CallOption) (*MuteUserResponse, error)
|
||||||
KickUser(ctx context.Context, in *KickUserRequest, opts ...grpc.CallOption) (*KickUserResponse, error)
|
KickUser(ctx context.Context, in *KickUserRequest, opts ...grpc.CallOption) (*KickUserResponse, error)
|
||||||
UnbanUser(ctx context.Context, in *UnbanUserRequest, opts ...grpc.CallOption) (*UnbanUserResponse, error)
|
UnbanUser(ctx context.Context, in *UnbanUserRequest, opts ...grpc.CallOption) (*UnbanUserResponse, error)
|
||||||
SystemEvictUser(ctx context.Context, in *SystemEvictUserRequest, opts ...grpc.CallOption) (*SystemEvictUserResponse, error)
|
SystemEvictUser(ctx context.Context, in *SystemEvictUserRequest, opts ...grpc.CallOption) (*SystemEvictUserResponse, error)
|
||||||
SendGift(ctx context.Context, in *SendGiftRequest, opts ...grpc.CallOption) (*SendGiftResponse, error)
|
SendGift(ctx context.Context, in *SendGiftRequest, opts ...grpc.CallOption) (*SendGiftResponse, error)
|
||||||
|
FollowRoom(ctx context.Context, in *FollowRoomRequest, opts ...grpc.CallOption) (*FollowRoomResponse, error)
|
||||||
|
UnfollowRoom(ctx context.Context, in *UnfollowRoomRequest, opts ...grpc.CallOption) (*UnfollowRoomResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type roomCommandServiceClient struct {
|
type roomCommandServiceClient struct {
|
||||||
@ -217,6 +223,16 @@ func (c *roomCommandServiceClient) SetChatEnabled(ctx context.Context, in *SetCh
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *roomCommandServiceClient) SetRoomPassword(ctx context.Context, in *SetRoomPasswordRequest, opts ...grpc.CallOption) (*SetRoomPasswordResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(SetRoomPasswordResponse)
|
||||||
|
err := c.cc.Invoke(ctx, RoomCommandService_SetRoomPassword_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *roomCommandServiceClient) SetRoomAdmin(ctx context.Context, in *SetRoomAdminRequest, opts ...grpc.CallOption) (*SetRoomAdminResponse, error) {
|
func (c *roomCommandServiceClient) SetRoomAdmin(ctx context.Context, in *SetRoomAdminRequest, opts ...grpc.CallOption) (*SetRoomAdminResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(SetRoomAdminResponse)
|
out := new(SetRoomAdminResponse)
|
||||||
@ -277,6 +293,26 @@ func (c *roomCommandServiceClient) SendGift(ctx context.Context, in *SendGiftReq
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *roomCommandServiceClient) FollowRoom(ctx context.Context, in *FollowRoomRequest, opts ...grpc.CallOption) (*FollowRoomResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(FollowRoomResponse)
|
||||||
|
err := c.cc.Invoke(ctx, RoomCommandService_FollowRoom_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *roomCommandServiceClient) UnfollowRoom(ctx context.Context, in *UnfollowRoomRequest, opts ...grpc.CallOption) (*UnfollowRoomResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(UnfollowRoomResponse)
|
||||||
|
err := c.cc.Invoke(ctx, RoomCommandService_UnfollowRoom_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RoomCommandServiceServer is the server API for RoomCommandService service.
|
// RoomCommandServiceServer is the server API for RoomCommandService service.
|
||||||
// All implementations must embed UnimplementedRoomCommandServiceServer
|
// All implementations must embed UnimplementedRoomCommandServiceServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@ -297,12 +333,15 @@ type RoomCommandServiceServer interface {
|
|||||||
ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error)
|
ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error)
|
||||||
SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error)
|
SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error)
|
||||||
SetChatEnabled(context.Context, *SetChatEnabledRequest) (*SetChatEnabledResponse, error)
|
SetChatEnabled(context.Context, *SetChatEnabledRequest) (*SetChatEnabledResponse, error)
|
||||||
|
SetRoomPassword(context.Context, *SetRoomPasswordRequest) (*SetRoomPasswordResponse, error)
|
||||||
SetRoomAdmin(context.Context, *SetRoomAdminRequest) (*SetRoomAdminResponse, error)
|
SetRoomAdmin(context.Context, *SetRoomAdminRequest) (*SetRoomAdminResponse, error)
|
||||||
MuteUser(context.Context, *MuteUserRequest) (*MuteUserResponse, error)
|
MuteUser(context.Context, *MuteUserRequest) (*MuteUserResponse, error)
|
||||||
KickUser(context.Context, *KickUserRequest) (*KickUserResponse, error)
|
KickUser(context.Context, *KickUserRequest) (*KickUserResponse, error)
|
||||||
UnbanUser(context.Context, *UnbanUserRequest) (*UnbanUserResponse, error)
|
UnbanUser(context.Context, *UnbanUserRequest) (*UnbanUserResponse, error)
|
||||||
SystemEvictUser(context.Context, *SystemEvictUserRequest) (*SystemEvictUserResponse, error)
|
SystemEvictUser(context.Context, *SystemEvictUserRequest) (*SystemEvictUserResponse, error)
|
||||||
SendGift(context.Context, *SendGiftRequest) (*SendGiftResponse, error)
|
SendGift(context.Context, *SendGiftRequest) (*SendGiftResponse, error)
|
||||||
|
FollowRoom(context.Context, *FollowRoomRequest) (*FollowRoomResponse, error)
|
||||||
|
UnfollowRoom(context.Context, *UnfollowRoomRequest) (*UnfollowRoomResponse, error)
|
||||||
mustEmbedUnimplementedRoomCommandServiceServer()
|
mustEmbedUnimplementedRoomCommandServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,6 +394,9 @@ func (UnimplementedRoomCommandServiceServer) SetMicSeatLock(context.Context, *Se
|
|||||||
func (UnimplementedRoomCommandServiceServer) SetChatEnabled(context.Context, *SetChatEnabledRequest) (*SetChatEnabledResponse, error) {
|
func (UnimplementedRoomCommandServiceServer) SetChatEnabled(context.Context, *SetChatEnabledRequest) (*SetChatEnabledResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method SetChatEnabled not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method SetChatEnabled not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedRoomCommandServiceServer) SetRoomPassword(context.Context, *SetRoomPasswordRequest) (*SetRoomPasswordResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method SetRoomPassword not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedRoomCommandServiceServer) SetRoomAdmin(context.Context, *SetRoomAdminRequest) (*SetRoomAdminResponse, error) {
|
func (UnimplementedRoomCommandServiceServer) SetRoomAdmin(context.Context, *SetRoomAdminRequest) (*SetRoomAdminResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method SetRoomAdmin not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method SetRoomAdmin not implemented")
|
||||||
}
|
}
|
||||||
@ -373,6 +415,12 @@ func (UnimplementedRoomCommandServiceServer) SystemEvictUser(context.Context, *S
|
|||||||
func (UnimplementedRoomCommandServiceServer) SendGift(context.Context, *SendGiftRequest) (*SendGiftResponse, error) {
|
func (UnimplementedRoomCommandServiceServer) SendGift(context.Context, *SendGiftRequest) (*SendGiftResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method SendGift not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method SendGift not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedRoomCommandServiceServer) FollowRoom(context.Context, *FollowRoomRequest) (*FollowRoomResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method FollowRoom not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedRoomCommandServiceServer) UnfollowRoom(context.Context, *UnfollowRoomRequest) (*UnfollowRoomResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method UnfollowRoom not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedRoomCommandServiceServer) mustEmbedUnimplementedRoomCommandServiceServer() {}
|
func (UnimplementedRoomCommandServiceServer) mustEmbedUnimplementedRoomCommandServiceServer() {}
|
||||||
func (UnimplementedRoomCommandServiceServer) testEmbeddedByValue() {}
|
func (UnimplementedRoomCommandServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@ -646,6 +694,24 @@ func _RoomCommandService_SetChatEnabled_Handler(srv interface{}, ctx context.Con
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _RoomCommandService_SetRoomPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(SetRoomPasswordRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(RoomCommandServiceServer).SetRoomPassword(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: RoomCommandService_SetRoomPassword_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(RoomCommandServiceServer).SetRoomPassword(ctx, req.(*SetRoomPasswordRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _RoomCommandService_SetRoomAdmin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _RoomCommandService_SetRoomAdmin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(SetRoomAdminRequest)
|
in := new(SetRoomAdminRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -754,6 +820,42 @@ func _RoomCommandService_SendGift_Handler(srv interface{}, ctx context.Context,
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _RoomCommandService_FollowRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(FollowRoomRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(RoomCommandServiceServer).FollowRoom(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: RoomCommandService_FollowRoom_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(RoomCommandServiceServer).FollowRoom(ctx, req.(*FollowRoomRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _RoomCommandService_UnfollowRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UnfollowRoomRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(RoomCommandServiceServer).UnfollowRoom(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: RoomCommandService_UnfollowRoom_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(RoomCommandServiceServer).UnfollowRoom(ctx, req.(*UnfollowRoomRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// RoomCommandService_ServiceDesc is the grpc.ServiceDesc for RoomCommandService service.
|
// RoomCommandService_ServiceDesc is the grpc.ServiceDesc for RoomCommandService service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@ -817,6 +919,10 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "SetChatEnabled",
|
MethodName: "SetChatEnabled",
|
||||||
Handler: _RoomCommandService_SetChatEnabled_Handler,
|
Handler: _RoomCommandService_SetChatEnabled_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "SetRoomPassword",
|
||||||
|
Handler: _RoomCommandService_SetRoomPassword_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "SetRoomAdmin",
|
MethodName: "SetRoomAdmin",
|
||||||
Handler: _RoomCommandService_SetRoomAdmin_Handler,
|
Handler: _RoomCommandService_SetRoomAdmin_Handler,
|
||||||
@ -841,6 +947,14 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "SendGift",
|
MethodName: "SendGift",
|
||||||
Handler: _RoomCommandService_SendGift_Handler,
|
Handler: _RoomCommandService_SendGift_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "FollowRoom",
|
||||||
|
Handler: _RoomCommandService_FollowRoom_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "UnfollowRoom",
|
||||||
|
Handler: _RoomCommandService_UnfollowRoom_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "proto/room/v1/room.proto",
|
Metadata: "proto/room/v1/room.proto",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.35.1
|
// protoc-gen-go v1.34.2
|
||||||
// protoc v5.29.3
|
// protoc v5.29.2
|
||||||
// source: proto/user/v1/auth.proto
|
// source: proto/user/v1/auth.proto
|
||||||
|
|
||||||
package userv1
|
package userv1
|
||||||
@ -33,9 +33,11 @@ type LoginPasswordRequest struct {
|
|||||||
|
|
||||||
func (x *LoginPasswordRequest) Reset() {
|
func (x *LoginPasswordRequest) Reset() {
|
||||||
*x = LoginPasswordRequest{}
|
*x = LoginPasswordRequest{}
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[0]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_user_v1_auth_proto_msgTypes[0]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *LoginPasswordRequest) String() string {
|
func (x *LoginPasswordRequest) String() string {
|
||||||
@ -46,7 +48,7 @@ func (*LoginPasswordRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *LoginPasswordRequest) ProtoReflect() protoreflect.Message {
|
func (x *LoginPasswordRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[0]
|
mi := &file_proto_user_v1_auth_proto_msgTypes[0]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -112,9 +114,11 @@ type LoginThirdPartyRequest struct {
|
|||||||
|
|
||||||
func (x *LoginThirdPartyRequest) Reset() {
|
func (x *LoginThirdPartyRequest) Reset() {
|
||||||
*x = LoginThirdPartyRequest{}
|
*x = LoginThirdPartyRequest{}
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[1]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_user_v1_auth_proto_msgTypes[1]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *LoginThirdPartyRequest) String() string {
|
func (x *LoginThirdPartyRequest) String() string {
|
||||||
@ -125,7 +129,7 @@ func (*LoginThirdPartyRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *LoginThirdPartyRequest) ProtoReflect() protoreflect.Message {
|
func (x *LoginThirdPartyRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[1]
|
mi := &file_proto_user_v1_auth_proto_msgTypes[1]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -294,9 +298,11 @@ type AuthResponse struct {
|
|||||||
|
|
||||||
func (x *AuthResponse) Reset() {
|
func (x *AuthResponse) Reset() {
|
||||||
*x = AuthResponse{}
|
*x = AuthResponse{}
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[2]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_user_v1_auth_proto_msgTypes[2]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *AuthResponse) String() string {
|
func (x *AuthResponse) String() string {
|
||||||
@ -307,7 +313,7 @@ func (*AuthResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *AuthResponse) ProtoReflect() protoreflect.Message {
|
func (x *AuthResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[2]
|
mi := &file_proto_user_v1_auth_proto_msgTypes[2]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -363,9 +369,11 @@ type SetPasswordRequest struct {
|
|||||||
|
|
||||||
func (x *SetPasswordRequest) Reset() {
|
func (x *SetPasswordRequest) Reset() {
|
||||||
*x = SetPasswordRequest{}
|
*x = SetPasswordRequest{}
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[3]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_user_v1_auth_proto_msgTypes[3]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SetPasswordRequest) String() string {
|
func (x *SetPasswordRequest) String() string {
|
||||||
@ -376,7 +384,7 @@ func (*SetPasswordRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *SetPasswordRequest) ProtoReflect() protoreflect.Message {
|
func (x *SetPasswordRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[3]
|
mi := &file_proto_user_v1_auth_proto_msgTypes[3]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -423,9 +431,11 @@ type SetPasswordResponse struct {
|
|||||||
|
|
||||||
func (x *SetPasswordResponse) Reset() {
|
func (x *SetPasswordResponse) Reset() {
|
||||||
*x = SetPasswordResponse{}
|
*x = SetPasswordResponse{}
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[4]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_user_v1_auth_proto_msgTypes[4]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SetPasswordResponse) String() string {
|
func (x *SetPasswordResponse) String() string {
|
||||||
@ -436,7 +446,7 @@ func (*SetPasswordResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *SetPasswordResponse) ProtoReflect() protoreflect.Message {
|
func (x *SetPasswordResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[4]
|
mi := &file_proto_user_v1_auth_proto_msgTypes[4]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -470,9 +480,11 @@ type RefreshTokenRequest struct {
|
|||||||
|
|
||||||
func (x *RefreshTokenRequest) Reset() {
|
func (x *RefreshTokenRequest) Reset() {
|
||||||
*x = RefreshTokenRequest{}
|
*x = RefreshTokenRequest{}
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[5]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_user_v1_auth_proto_msgTypes[5]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RefreshTokenRequest) String() string {
|
func (x *RefreshTokenRequest) String() string {
|
||||||
@ -483,7 +495,7 @@ func (*RefreshTokenRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RefreshTokenRequest) ProtoReflect() protoreflect.Message {
|
func (x *RefreshTokenRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[5]
|
mi := &file_proto_user_v1_auth_proto_msgTypes[5]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -523,9 +535,11 @@ type RefreshTokenResponse struct {
|
|||||||
|
|
||||||
func (x *RefreshTokenResponse) Reset() {
|
func (x *RefreshTokenResponse) Reset() {
|
||||||
*x = RefreshTokenResponse{}
|
*x = RefreshTokenResponse{}
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[6]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_user_v1_auth_proto_msgTypes[6]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RefreshTokenResponse) String() string {
|
func (x *RefreshTokenResponse) String() string {
|
||||||
@ -536,7 +550,7 @@ func (*RefreshTokenResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RefreshTokenResponse) ProtoReflect() protoreflect.Message {
|
func (x *RefreshTokenResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[6]
|
mi := &file_proto_user_v1_auth_proto_msgTypes[6]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -571,9 +585,11 @@ type LogoutRequest struct {
|
|||||||
|
|
||||||
func (x *LogoutRequest) Reset() {
|
func (x *LogoutRequest) Reset() {
|
||||||
*x = LogoutRequest{}
|
*x = LogoutRequest{}
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[7]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_user_v1_auth_proto_msgTypes[7]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *LogoutRequest) String() string {
|
func (x *LogoutRequest) String() string {
|
||||||
@ -584,7 +600,7 @@ func (*LogoutRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
|
func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[7]
|
mi := &file_proto_user_v1_auth_proto_msgTypes[7]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -631,9 +647,11 @@ type LogoutResponse struct {
|
|||||||
|
|
||||||
func (x *LogoutResponse) Reset() {
|
func (x *LogoutResponse) Reset() {
|
||||||
*x = LogoutResponse{}
|
*x = LogoutResponse{}
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[8]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_user_v1_auth_proto_msgTypes[8]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *LogoutResponse) String() string {
|
func (x *LogoutResponse) String() string {
|
||||||
@ -644,7 +662,7 @@ func (*LogoutResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *LogoutResponse) ProtoReflect() protoreflect.Message {
|
func (x *LogoutResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[8]
|
mi := &file_proto_user_v1_auth_proto_msgTypes[8]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -682,9 +700,11 @@ type RecordLoginBlockedRequest struct {
|
|||||||
|
|
||||||
func (x *RecordLoginBlockedRequest) Reset() {
|
func (x *RecordLoginBlockedRequest) Reset() {
|
||||||
*x = RecordLoginBlockedRequest{}
|
*x = RecordLoginBlockedRequest{}
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[9]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_user_v1_auth_proto_msgTypes[9]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RecordLoginBlockedRequest) String() string {
|
func (x *RecordLoginBlockedRequest) String() string {
|
||||||
@ -695,7 +715,7 @@ func (*RecordLoginBlockedRequest) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RecordLoginBlockedRequest) ProtoReflect() protoreflect.Message {
|
func (x *RecordLoginBlockedRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[9]
|
mi := &file_proto_user_v1_auth_proto_msgTypes[9]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -763,9 +783,11 @@ type RecordLoginBlockedResponse struct {
|
|||||||
|
|
||||||
func (x *RecordLoginBlockedResponse) Reset() {
|
func (x *RecordLoginBlockedResponse) Reset() {
|
||||||
*x = RecordLoginBlockedResponse{}
|
*x = RecordLoginBlockedResponse{}
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[10]
|
if protoimpl.UnsafeEnabled {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
mi := &file_proto_user_v1_auth_proto_msgTypes[10]
|
||||||
ms.StoreMessageInfo(mi)
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RecordLoginBlockedResponse) String() string {
|
func (x *RecordLoginBlockedResponse) String() string {
|
||||||
@ -776,7 +798,7 @@ func (*RecordLoginBlockedResponse) ProtoMessage() {}
|
|||||||
|
|
||||||
func (x *RecordLoginBlockedResponse) ProtoReflect() protoreflect.Message {
|
func (x *RecordLoginBlockedResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proto_user_v1_auth_proto_msgTypes[10]
|
mi := &file_proto_user_v1_auth_proto_msgTypes[10]
|
||||||
if x != nil {
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
@ -1017,6 +1039,140 @@ func file_proto_user_v1_auth_proto_init() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
file_proto_user_v1_user_proto_init()
|
file_proto_user_v1_user_proto_init()
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_proto_user_v1_auth_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*LoginPasswordRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_user_v1_auth_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*LoginThirdPartyRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_user_v1_auth_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*AuthResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_user_v1_auth_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*SetPasswordRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_user_v1_auth_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*SetPasswordResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_user_v1_auth_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RefreshTokenRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_user_v1_auth_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RefreshTokenResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_user_v1_auth_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*LogoutRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_user_v1_auth_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*LogoutResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_user_v1_auth_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RecordLoginBlockedRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_proto_user_v1_auth_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*RecordLoginBlockedResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.5.1
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
// - protoc v5.29.3
|
// - protoc v5.29.2
|
||||||
// source: proto/user/v1/auth.proto
|
// source: proto/user/v1/auth.proto
|
||||||
|
|
||||||
package userv1
|
package userv1
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.5.1
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
// - protoc v5.29.3
|
// - protoc v5.29.2
|
||||||
// source: proto/user/v1/host.proto
|
// source: proto/user/v1/host.proto
|
||||||
|
|
||||||
package userv1
|
package userv1
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.5.1
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
// - protoc v5.29.3
|
// - protoc v5.29.2
|
||||||
// source: proto/user/v1/user.proto
|
// source: proto/user/v1/user.proto
|
||||||
|
|
||||||
package userv1
|
package userv1
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.5.1
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
// - protoc v5.29.3
|
// - protoc v5.29.2
|
||||||
// source: proto/wallet/v1/wallet.proto
|
// source: proto/wallet/v1/wallet.proto
|
||||||
|
|
||||||
package walletv1
|
package walletv1
|
||||||
|
|||||||
125
docs/flutter对接/房间关注接口Flutter对接.md
Normal file
125
docs/flutter对接/房间关注接口Flutter对接.md
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# 房间关注接口 Flutter 对接
|
||||||
|
|
||||||
|
## 接口地址
|
||||||
|
|
||||||
|
`POST /api/v1/rooms/{room_id}/follow`
|
||||||
|
|
||||||
|
`DELETE /api/v1/rooms/{room_id}/follow`
|
||||||
|
|
||||||
|
本地开发地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:13000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 请求方法
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `POST` | 关注房间 |
|
||||||
|
| `DELETE` | 取消关注房间 |
|
||||||
|
|
||||||
|
## 请求头
|
||||||
|
|
||||||
|
| Header | 必填 | 模拟值 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `Authorization` | 是 | `Bearer <access_token>` | 登录 access token;服务端从 token 读取当前用户 ID。 |
|
||||||
|
| `X-App-Code` | 否 | `lalu` | App 编码;登录态接口最终以 token 内的 `app_code` 为准。 |
|
||||||
|
|
||||||
|
## 参数
|
||||||
|
|
||||||
|
Path 参数:
|
||||||
|
|
||||||
|
| 参数 | 必填 | 类型 | 模拟值 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `room_id` | 是 | string | `room_10001` | 房间 ID;只允许字母、数字、下划线、短横线,最长 48 位。 |
|
||||||
|
|
||||||
|
Query 参数:无。
|
||||||
|
|
||||||
|
请求体:无。不要传 `user_id`,服务端只使用 token 中的当前用户。
|
||||||
|
|
||||||
|
关注请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/rooms/room_10001/follow
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
X-App-Code: lalu
|
||||||
|
```
|
||||||
|
|
||||||
|
取消关注请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
DELETE /api/v1/rooms/room_10001/follow
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
X-App-Code: lalu
|
||||||
|
```
|
||||||
|
|
||||||
|
## 返回体模拟数据
|
||||||
|
|
||||||
|
关注成功:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": {
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"is_followed": true,
|
||||||
|
"followed_at_ms": 1710000000123,
|
||||||
|
"server_time_ms": 1710000000456
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
取消关注成功:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": {
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"is_followed": false,
|
||||||
|
"server_time_ms": 1710000000789
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段说明:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `room_id` | string | 本次操作的房间 ID。 |
|
||||||
|
| `is_followed` | bool | 当前登录用户是否已关注该房间。 |
|
||||||
|
| `followed_at_ms` | int64 | 关注成功时间,Unix epoch milliseconds;取消关注返回体不带该字段。 |
|
||||||
|
| `server_time_ms` | int64 | 服务端当前时间,Unix epoch milliseconds。 |
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
失败返回仍使用统一 envelope:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "INVALID_ARGUMENT",
|
||||||
|
"message": "invalid argument",
|
||||||
|
"request_id": "req_abc"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| HTTP 状态码 | `code` | 处理方式 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `400` | `INVALID_ARGUMENT` | `room_id` 为空或格式非法,客户端不要重试,检查传参。 |
|
||||||
|
| `401` | `UNAUTHORIZED` | token 缺失、无效、过期或会话失效,重新登录。 |
|
||||||
|
| `403` | `PROFILE_REQUIRED` | 用户资料未完成,跳转资料补全流程。 |
|
||||||
|
| `404` | `NOT_FOUND` | 关注时房间不存在,刷新房间状态或退出当前房间页。 |
|
||||||
|
| `409` | `ROOM_CLOSED` | 关注时房间已关闭,刷新房间状态或退出当前房间页。 |
|
||||||
|
| `502` | `UPSTREAM_ERROR` | 内部服务暂不可用,可提示稍后重试。 |
|
||||||
|
|
||||||
|
幂等规则:
|
||||||
|
|
||||||
|
| 场景 | 返回 |
|
||||||
|
| --- | --- |
|
||||||
|
| 重复关注已关注房间 | `200 OK`,`is_followed=true`,不刷新原 `followed_at_ms`。 |
|
||||||
|
| 取消未关注房间 | `200 OK`,`is_followed=false`。 |
|
||||||
322
docs/flutter对接/房间踢人接口Flutter对接.md
Normal file
322
docs/flutter对接/房间踢人接口Flutter对接.md
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
# 房间踢人接口 Flutter 对接
|
||||||
|
|
||||||
|
本文描述 Flutter App 对接房间踢人能力的 HTTP 接口,以及踢人后腾讯 IM 和腾讯 RTC 的客户端处理方式。踢人事实由 `room-service` Room Cell 提交;客户端不能只靠本地 UI 状态判断用户是否仍在房间。
|
||||||
|
|
||||||
|
## 接口地址
|
||||||
|
|
||||||
|
`POST /api/v1/rooms/user/kick`
|
||||||
|
|
||||||
|
`POST /api/v1/rooms/user/unban`
|
||||||
|
|
||||||
|
本地开发地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:13000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 请求方法
|
||||||
|
|
||||||
|
| 方法 | 接口 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `POST` | `/api/v1/rooms/user/kick` | 踢出房间用户,并写入房间 ban 集合。 |
|
||||||
|
| `POST` | `/api/v1/rooms/user/unban` | 解除房间 ban,允许用户后续重新 JoinRoom。 |
|
||||||
|
|
||||||
|
## 请求头
|
||||||
|
|
||||||
|
| Header | 必填 | 模拟值 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `Authorization` | 是 | `Bearer <access_token>` | 登录 access token;服务端从 token 读取当前用户 ID。 |
|
||||||
|
| `X-App-Code` | 否 | `lalu` | App 编码;登录态接口最终以 token 内的 `app_code` 为准。 |
|
||||||
|
|
||||||
|
## 踢出房间用户
|
||||||
|
|
||||||
|
房主可以踢出房间内用户。当前后端权限矩阵也允许房管管理普通用户;端上如果只开放房主入口,后端仍兼容。
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
||||||
|
| `command_id` | 是 | string | `cmd_kick_room_10001_20001_1710000000` | 房间命令幂等键;同一次踢人重试必须复用。 |
|
||||||
|
| `target_user_id` | 是 | int64 | `20001` | 被踢用户长 ID。 |
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/rooms/user/kick
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
X-App-Code: lalu
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"command_id": "cmd_kick_room_10001_20001_1710000000",
|
||||||
|
"target_user_id": 20001
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": {
|
||||||
|
"result": {
|
||||||
|
"applied": true,
|
||||||
|
"room_version": 18,
|
||||||
|
"server_time_ms": 1710000000456
|
||||||
|
},
|
||||||
|
"room": {
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"status": "active",
|
||||||
|
"ban_user_ids": [20001],
|
||||||
|
"online_users": [],
|
||||||
|
"mic_seats": [],
|
||||||
|
"version": 18
|
||||||
|
},
|
||||||
|
"rtc_kicked": true,
|
||||||
|
"rtc_kick_error": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段说明:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `data.result.applied` | bool | 本次命令是否产生状态变更;重复踢已 ban 且无残留状态的用户可能为 `false`。 |
|
||||||
|
| `data.room.ban_user_ids` | int64[] | 当前房间 ban 用户集合;被踢用户会进入该集合。 |
|
||||||
|
| `data.room.online_users` | array | 当前业务 presence;被踢用户会被移除。 |
|
||||||
|
| `data.room.mic_seats` | array | 如果被踢用户在麦上,对应麦位会被释放。 |
|
||||||
|
| `data.rtc_kicked` | bool | 服务端是否已调用 TRTC `RemoveUserByStrRoomId` 移除目标用户。 |
|
||||||
|
| `data.rtc_kick_error` | string | TRTC 服务端踢人失败原因;踢人事实已生效,客户端仍按被踢处理。 |
|
||||||
|
|
||||||
|
## 解除房间 ban
|
||||||
|
|
||||||
|
解除 ban 只恢复用户后续重新进房资格,不恢复历史 presence、麦位、管理员身份或 RTC 连接。
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/rooms/user/unban
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
X-App-Code: lalu
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"command_id": "cmd_unban_room_10001_20001_1710000100",
|
||||||
|
"target_user_id": 20001
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应字段和踢人一致,`room.ban_user_ids` 中不再包含目标用户。
|
||||||
|
|
||||||
|
## IM 和 RTC 交互
|
||||||
|
|
||||||
|
踢人成功后有三层效果,客户端都要按幂等方式处理:
|
||||||
|
|
||||||
|
| 层 | 服务端行为 | Flutter 处理 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| room-service | 移除目标 presence、释放麦位、写入 ban,后续 JoinRoom、RTC token、IM 进群和发言守卫都会拒绝目标用户。 | 被踢用户所有房间接口如果返回 `PERMISSION_DENIED`,立即退出房间页并清理房间态。 |
|
||||||
|
| 腾讯 IM 群 | room-service outbox worker 先调用 IM REST 把目标用户移出房间群,再发布 `room_user_kicked` 系统消息给房间内其他用户。 | 普通成员收到 `room_user_kicked` 后刷新在线列表和麦位;被踢用户要监听 IM 群被移除或退群回调作为主要退出信号。 |
|
||||||
|
| 腾讯 IM 单聊 | notice-service 监听 `room_outbox.RoomUserKicked`,给被踢用户发送 C2C `room_notice` 私有通知。 | 被踢用户收到 `room_notice` 时执行同一套本地退出逻辑;按 `event_id` 去重,避免和群消息或接口 403 重复弹窗。 |
|
||||||
|
| 腾讯 RTC | room-service 在踢人事实提交后尽力调用 TRTC `RemoveUserByStrRoomId`。 | 被踢用户收到 RTC 退房或连接断开回调时调用同一套本地退出逻辑;如果先收到 IM 回调,也要主动 `exitRoom`。 |
|
||||||
|
|
||||||
|
客户端退出房间的本地动作必须可重复调用:
|
||||||
|
|
||||||
|
1. 停止本地音频上行。
|
||||||
|
2. 调用 TRTC `exitRoom`。
|
||||||
|
3. 退出或清理当前房间 IM 群会话。
|
||||||
|
4. 停止房间 heartbeat。
|
||||||
|
5. 清空当前房间页状态,回到房间列表或 Mine 页面。
|
||||||
|
6. 弹出被踢提示。
|
||||||
|
|
||||||
|
被踢用户不一定能收到房间群里的 `room_user_kicked` 自定义消息,因为服务端会优先把他移出 IM 群。端上必须同时处理 notice-service 的 C2C `room_notice`、IM SDK 的群移除回调、RTC 退房回调和后续接口 403。
|
||||||
|
|
||||||
|
## IM 自定义消息
|
||||||
|
|
||||||
|
房间内其他用户会收到群 `TIMCustomElem`,核心字段如下:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_id": "evt_abc",
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"event_type": "room_user_kicked",
|
||||||
|
"actor_user_id": 10001,
|
||||||
|
"target_user_id": 20001,
|
||||||
|
"room_version": 18,
|
||||||
|
"attributes": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
处理规则:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `event_type` | 固定 `room_user_kicked`。 |
|
||||||
|
| `actor_user_id` | 操作者用户 ID;系统驱逐时可能为 `0`。 |
|
||||||
|
| `target_user_id` | 被踢用户 ID。 |
|
||||||
|
| `room_version` | 房间版本;本地已有更高版本时丢弃旧消息。 |
|
||||||
|
|
||||||
|
如果 `target_user_id` 是当前登录用户,直接执行本地退出房间动作;如果不是当前用户,只刷新在线列表、麦位和管理面 UI。
|
||||||
|
|
||||||
|
被踢用户会收到 notice-service 发送的 C2C `TIMCustomElem`,`Ext=room_notice`,核心字段如下:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_id": "evt_abc",
|
||||||
|
"event_type": "room_user_kicked",
|
||||||
|
"source_event_type": "RoomUserKicked",
|
||||||
|
"app_code": "lalu",
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"room_version": 18,
|
||||||
|
"actor_user_id": 10001,
|
||||||
|
"target_user_id": 20001,
|
||||||
|
"occurred_at_ms": 1710000000456,
|
||||||
|
"source_created_at_ms": 1710000000456
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
C2C 通知和房间群消息使用同一个 `event_id`。Flutter 侧把 `room_user_kicked`、IM 群移除、RTC 退房和接口 403 都归一到同一个 `handleKickedFromRoom(roomId, eventId)` 流程;同一个 `event_id` 或同一个房间的短时间重复退出信号只处理一次。
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
失败返回仍使用统一 envelope:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "PERMISSION_DENIED",
|
||||||
|
"message": "permission denied",
|
||||||
|
"request_id": "req_abc"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| HTTP 状态码 | `code` | 典型场景 | 处理方式 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `400` | `INVALID_ARGUMENT` | `room_id`、`command_id` 或 `target_user_id` 缺失;操作者踢自己。 | 客户端修正参数,不自动重试。 |
|
||||||
|
| `401` | `UNAUTHORIZED` | token 缺失、无效、过期或会话失效。 | 重新登录。 |
|
||||||
|
| `403` | `PROFILE_REQUIRED` | 用户资料未完成。 | 跳转资料补全流程。 |
|
||||||
|
| `403` | `PERMISSION_DENIED` | 非管理角色踢人;房管踢房主;操作者不在房间。 | 隐藏入口或提示无权限。 |
|
||||||
|
| `404` | `NOT_FOUND` | 房间不存在。 | 刷新列表或退出房间页。 |
|
||||||
|
| `409` | `ROOM_CLOSED` | 房间已关闭。 | 刷新列表或退出房间页。 |
|
||||||
|
| `409` | `CONFLICT` | 同一个 `command_id` 被不同请求体复用。 | 为新的用户动作生成新的 `command_id`。 |
|
||||||
|
| `502` | `UPSTREAM_ERROR` | 内部服务暂不可用。 | 可提示稍后重试。 |
|
||||||
|
|
||||||
|
幂等规则:
|
||||||
|
|
||||||
|
| 场景 | 返回 |
|
||||||
|
| --- | --- |
|
||||||
|
| 同一个 `command_id` 重试同一次踢人 | 返回同一命令结果,不重复变更房间。 |
|
||||||
|
| 同一个 `command_id` 换另一个 `target_user_id` | `409 CONFLICT`。 |
|
||||||
|
| 重复踢已 ban 且无残留状态的用户 | `200 OK`,`applied=false`。 |
|
||||||
|
| 解封未 ban 用户 | `200 OK`,`applied=false`。 |
|
||||||
|
|
||||||
|
## Flutter 解析示例
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class RoomCommandResult {
|
||||||
|
RoomCommandResult({
|
||||||
|
required this.applied,
|
||||||
|
required this.roomVersion,
|
||||||
|
required this.serverTimeMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool applied;
|
||||||
|
final int roomVersion;
|
||||||
|
final int serverTimeMs;
|
||||||
|
|
||||||
|
factory RoomCommandResult.fromJson(Map<String, dynamic> json) {
|
||||||
|
return RoomCommandResult(
|
||||||
|
applied: json['applied'] as bool? ?? false,
|
||||||
|
roomVersion: (json['room_version'] as num?)?.toInt() ?? 0,
|
||||||
|
serverTimeMs: (json['server_time_ms'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KickUserResult {
|
||||||
|
KickUserResult({
|
||||||
|
required this.result,
|
||||||
|
required this.rtcKicked,
|
||||||
|
required this.rtcKickError,
|
||||||
|
required this.banUserIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
final RoomCommandResult result;
|
||||||
|
final bool rtcKicked;
|
||||||
|
final String rtcKickError;
|
||||||
|
final List<int> banUserIds;
|
||||||
|
|
||||||
|
factory KickUserResult.fromJson(Map<String, dynamic> json) {
|
||||||
|
final room = json['room'] as Map<String, dynamic>? ?? const {};
|
||||||
|
return KickUserResult(
|
||||||
|
result: RoomCommandResult.fromJson(
|
||||||
|
json['result'] as Map<String, dynamic>? ?? const {},
|
||||||
|
),
|
||||||
|
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())
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoomKickedEvent {
|
||||||
|
RoomKickedEvent({
|
||||||
|
required this.roomId,
|
||||||
|
required this.actorUserId,
|
||||||
|
required this.targetUserId,
|
||||||
|
required this.roomVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String roomId;
|
||||||
|
final int actorUserId;
|
||||||
|
final int targetUserId;
|
||||||
|
final int roomVersion;
|
||||||
|
|
||||||
|
bool isSelf(int currentUserId) => targetUserId == currentUserId;
|
||||||
|
|
||||||
|
factory RoomKickedEvent.fromJson(Map<String, dynamic> json) {
|
||||||
|
return RoomKickedEvent(
|
||||||
|
roomId: json['room_id'] as String? ?? '',
|
||||||
|
actorUserId: (json['actor_user_id'] as num?)?.toInt() ?? 0,
|
||||||
|
targetUserId: (json['target_user_id'] as num?)?.toInt() ?? 0,
|
||||||
|
roomVersion: (json['room_version'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoomKickNotice {
|
||||||
|
RoomKickNotice({
|
||||||
|
required this.eventId,
|
||||||
|
required this.roomId,
|
||||||
|
required this.actorUserId,
|
||||||
|
required this.targetUserId,
|
||||||
|
required this.roomVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String eventId;
|
||||||
|
final String roomId;
|
||||||
|
final int actorUserId;
|
||||||
|
final int targetUserId;
|
||||||
|
final int roomVersion;
|
||||||
|
|
||||||
|
factory RoomKickNotice.fromJson(Map<String, dynamic> json) {
|
||||||
|
return RoomKickNotice(
|
||||||
|
eventId: json['event_id'] as String? ?? '',
|
||||||
|
roomId: json['room_id'] as String? ?? '',
|
||||||
|
actorUserId: (json['actor_user_id'] as num?)?.toInt() ?? 0,
|
||||||
|
targetUserId: (json['target_user_id'] as num?)?.toInt() ?? 0,
|
||||||
|
roomVersion: (json['room_version'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
调用 HTTP 接口时先解析外层 envelope:只有 `code == "OK"` 时读取 `data`;其他 `code` 按登录态、参数错误、权限错误或服务错误处理,并把 `request_id` 写入客户端日志。
|
||||||
|
|
||||||
|
客户端生成 `command_id` 时按一次用户动作生成一次,同一次 HTTP 重试复用原 `command_id`;用户再次点击踢人时生成新的 `command_id`。
|
||||||
383
docs/flutter对接/房间锁房接口Flutter对接.md
Normal file
383
docs/flutter对接/房间锁房接口Flutter对接.md
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
# 房间锁房接口 Flutter 对接
|
||||||
|
|
||||||
|
本文描述 Flutter App 对接房间锁房能力的 HTTP 接口。房间密码由 `room-service` 只保存 bcrypt 哈希;客户端只提交明文用于本次设置或进房校验,任何列表、搜索、详情、房间消息和响应体都不会返回明文或哈希。
|
||||||
|
|
||||||
|
## 接口地址
|
||||||
|
|
||||||
|
`POST /api/v1/rooms/password`
|
||||||
|
|
||||||
|
`POST /api/v1/rooms/join`
|
||||||
|
|
||||||
|
`GET /api/v1/rooms`
|
||||||
|
|
||||||
|
`GET /api/v1/rooms/feeds`
|
||||||
|
|
||||||
|
本地开发地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:13000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 请求方法
|
||||||
|
|
||||||
|
| 方法 | 接口 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `POST` | `/api/v1/rooms/password` | 房主设置或清空房间入房密码。 |
|
||||||
|
| `POST` | `/api/v1/rooms/join` | 进入房间;带锁房间需要传 `password`。 |
|
||||||
|
| `GET` | `/api/v1/rooms` | 公共房间列表和搜索;房间卡片返回 `locked`。 |
|
||||||
|
| `GET` | `/api/v1/rooms/feeds` | Mine 页房间流;房间卡片返回 `locked`。 |
|
||||||
|
|
||||||
|
## 请求头
|
||||||
|
|
||||||
|
| Header | 必填 | 模拟值 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `Authorization` | 是 | `Bearer <access_token>` | 登录 access token;服务端从 token 读取当前用户 ID。 |
|
||||||
|
| `X-App-Code` | 否 | `lalu` | App 编码;登录态接口最终以 token 内的 `app_code` 为准。 |
|
||||||
|
|
||||||
|
## 设置或清空房间密码
|
||||||
|
|
||||||
|
只有房主可以调用。房管、主持人和普通观众调用会返回 `PERMISSION_DENIED`。
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
||||||
|
| `command_id` | 是 | string | `cmd_room_password_room_10001_1710000000` | 房间命令幂等键;同一次点击重试必须复用。 |
|
||||||
|
| `locked` | 是 | bool | `true` | `true` 表示设置密码,`false` 表示清空密码。 |
|
||||||
|
| `password` | `locked=true` 时必填 | string | `1234` | 入房密码;服务端 trim 后最长 64 个字符。`locked=false` 时忽略。 |
|
||||||
|
|
||||||
|
设置密码请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/rooms/password
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
X-App-Code: lalu
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"command_id": "cmd_room_password_room_10001_1710000000",
|
||||||
|
"locked": true,
|
||||||
|
"password": "1234"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
清空密码请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/rooms/password
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
X-App-Code: lalu
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"command_id": "cmd_room_password_room_10001_1710000100",
|
||||||
|
"locked": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": {
|
||||||
|
"result": {
|
||||||
|
"applied": true,
|
||||||
|
"room_version": 12,
|
||||||
|
"server_time_ms": 1710000000456
|
||||||
|
},
|
||||||
|
"room": {
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"im_group_id": "room_10001",
|
||||||
|
"room_short_id": "100001",
|
||||||
|
"title": "Live Room",
|
||||||
|
"cover_url": "https://cdn.example/room.png",
|
||||||
|
"owner_user_id": "10001",
|
||||||
|
"mode": "voice",
|
||||||
|
"status": "active",
|
||||||
|
"chat_enabled": true,
|
||||||
|
"locked": true,
|
||||||
|
"heat": 1000,
|
||||||
|
"online_count": 12,
|
||||||
|
"seat_count": 15,
|
||||||
|
"occupied_seat_count": 2,
|
||||||
|
"version": 12
|
||||||
|
},
|
||||||
|
"seats": [],
|
||||||
|
"server_time_ms": 1710000000456
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段说明:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `data.result.applied` | bool | 本次命令是否产生状态变更;重复提交相同命令可能为 `false`。 |
|
||||||
|
| `data.result.room_version` | int64 | 命令完成后房间版本。 |
|
||||||
|
| `data.room.locked` | bool | 最新锁房状态;`true` 表示后续新用户进房需要密码。 |
|
||||||
|
| `data.room.version` | int64 | 最新房间快照版本;客户端本地房间状态以更大版本覆盖旧状态。 |
|
||||||
|
| `data.seats` | array | 当前座位列表;锁房接口成功后可直接复用该房间快照刷新页面。 |
|
||||||
|
|
||||||
|
## 带密码进入房间
|
||||||
|
|
||||||
|
客户端进入房间前先看房间卡片或详情里的 `locked`:
|
||||||
|
|
||||||
|
| `locked` | 客户端行为 |
|
||||||
|
| --- | --- |
|
||||||
|
| `false` | 直接调用 JoinRoom,`password` 可不传或传空字符串。 |
|
||||||
|
| `true` | 先弹出密码输入,再调用 JoinRoom 并传 `password`。 |
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
||||||
|
| `command_id` | 是 | string | `cmd_join_room_10001_20001_1710000000` | 进房命令幂等键;同一次进房重试必须复用。 |
|
||||||
|
| `role` | 否 | string | `audience` | 当前固定传 `audience`。 |
|
||||||
|
| `password` | 锁房时必填 | string | `1234` | 只用于本次进房校验,服务端不保存明文。 |
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/rooms/join
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
X-App-Code: lalu
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"command_id": "cmd_join_room_10001_20001_1710000000",
|
||||||
|
"role": "audience",
|
||||||
|
"password": "1234"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应只展示锁房相关字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": {
|
||||||
|
"result": {
|
||||||
|
"applied": true,
|
||||||
|
"room_version": 13,
|
||||||
|
"server_time_ms": 1710000001456
|
||||||
|
},
|
||||||
|
"room": {
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"im_group_id": "room_10001",
|
||||||
|
"locked": true,
|
||||||
|
"version": 13
|
||||||
|
},
|
||||||
|
"viewer": {
|
||||||
|
"user_id": "20001",
|
||||||
|
"role": "audience",
|
||||||
|
"joined_at_ms": 1710000001456,
|
||||||
|
"last_seen_at_ms": 1710000001456
|
||||||
|
},
|
||||||
|
"im": {
|
||||||
|
"group_id": "room_10001",
|
||||||
|
"need_join_group": true
|
||||||
|
},
|
||||||
|
"rtc": {
|
||||||
|
"need_token": true,
|
||||||
|
"available": true
|
||||||
|
},
|
||||||
|
"server_time_ms": 1710000001456
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
进房规则:
|
||||||
|
|
||||||
|
| 场景 | 结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| 房间未锁 | 不校验 `password`。 |
|
||||||
|
| 房间已锁且密码正确 | JoinRoom 成功,客户端再调用腾讯 IM `joinGroup` 和 RTC `enterRoom`。 |
|
||||||
|
| 房间已锁且密码错误或缺失 | 返回 `403 PERMISSION_DENIED`,不会创建 presence,不会返回 RTC token。 |
|
||||||
|
| 房主进入自己的房间 | 不需要密码。 |
|
||||||
|
| 用户已经在房间内 | 再次 JoinRoom 按幂等恢复当前房间态,不会因为后续锁房被踢出。 |
|
||||||
|
|
||||||
|
## 房间列表和搜索 locked 字段
|
||||||
|
|
||||||
|
公共列表和搜索使用同一个接口:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/rooms?tab=hot&query=100001
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
X-App-Code: lalu
|
||||||
|
```
|
||||||
|
|
||||||
|
Mine 页房间流:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/rooms/feeds?tab=followed
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
X-App-Code: lalu
|
||||||
|
```
|
||||||
|
|
||||||
|
列表响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": {
|
||||||
|
"rooms": [
|
||||||
|
{
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"im_group_id": "room_10001",
|
||||||
|
"owner_user_id": "10001",
|
||||||
|
"title": "Live Room",
|
||||||
|
"cover_url": "https://cdn.example/room.png",
|
||||||
|
"mode": "voice",
|
||||||
|
"status": "active",
|
||||||
|
"locked": true,
|
||||||
|
"heat": 1000,
|
||||||
|
"online_count": 12,
|
||||||
|
"seat_count": 15,
|
||||||
|
"occupied_seat_count": 2,
|
||||||
|
"visible_region_id": 1,
|
||||||
|
"app_code": "lalu",
|
||||||
|
"room_short_id": "100001"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"next_cursor": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段说明:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `rooms[].locked` | bool | 房间是否需要入房密码;搜索结果和普通列表一致。 |
|
||||||
|
| `rooms[].im_group_id` | string | 腾讯 IM 群 ID;只用于 JoinRoom 成功后的 `joinGroup`,不能绕过 JoinRoom。 |
|
||||||
|
| `next_cursor` | string | 下一页游标;为空表示没有下一页。 |
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
失败返回仍使用统一 envelope:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "PERMISSION_DENIED",
|
||||||
|
"message": "permission denied",
|
||||||
|
"request_id": "req_abc"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| HTTP 状态码 | `code` | 典型场景 | 处理方式 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `400` | `INVALID_ARGUMENT` | `room_id`、`command_id` 缺失;设置锁房时 `password` 为空或超过 64 字符。 | 客户端修正参数,不自动重试。 |
|
||||||
|
| `401` | `UNAUTHORIZED` | token 缺失、无效、过期或会话失效。 | 重新登录。 |
|
||||||
|
| `403` | `PROFILE_REQUIRED` | 用户资料未完成。 | 跳转资料补全流程。 |
|
||||||
|
| `403` | `PERMISSION_DENIED` | 非房主设置密码;进锁房密码错误;用户被 ban。 | 设置密码时隐藏入口或提示无权限;进房时提示密码错误。 |
|
||||||
|
| `404` | `NOT_FOUND` | 房间不存在。 | 刷新列表或退出房间页。 |
|
||||||
|
| `409` | `ROOM_CLOSED` | 房间已关闭。 | 刷新列表或退出房间页。 |
|
||||||
|
| `409` | `CONFLICT` | 同一个 `command_id` 被不同请求体复用。 | 为新的用户动作生成新的 `command_id`。 |
|
||||||
|
| `502` | `UPSTREAM_ERROR` | 内部服务暂不可用。 | 可提示稍后重试。 |
|
||||||
|
|
||||||
|
幂等规则:
|
||||||
|
|
||||||
|
| 场景 | 返回 |
|
||||||
|
| --- | --- |
|
||||||
|
| 同一个 `command_id` 重试同一次设置密码 | 返回同一命令结果,不重复变更房间。 |
|
||||||
|
| 同一个 `command_id` 换另一个密码 | `409 CONFLICT`。 |
|
||||||
|
| 重复清空已解锁房间 | `200 OK`,`data.room.locked=false`。 |
|
||||||
|
| 同一个用户重复 JoinRoom | 返回当前房间首屏数据,不重复创建 presence。 |
|
||||||
|
|
||||||
|
## Flutter 解析示例
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class RoomCommandResult {
|
||||||
|
RoomCommandResult({
|
||||||
|
required this.applied,
|
||||||
|
required this.roomVersion,
|
||||||
|
required this.serverTimeMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool applied;
|
||||||
|
final int roomVersion;
|
||||||
|
final int serverTimeMs;
|
||||||
|
|
||||||
|
factory RoomCommandResult.fromJson(Map<String, dynamic> json) {
|
||||||
|
return RoomCommandResult(
|
||||||
|
applied: json['applied'] as bool? ?? false,
|
||||||
|
roomVersion: (json['room_version'] as num?)?.toInt() ?? 0,
|
||||||
|
serverTimeMs: (json['server_time_ms'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoomCard {
|
||||||
|
RoomCard({
|
||||||
|
required this.roomId,
|
||||||
|
required this.imGroupId,
|
||||||
|
required this.title,
|
||||||
|
required this.locked,
|
||||||
|
required this.onlineCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String roomId;
|
||||||
|
final String imGroupId;
|
||||||
|
final String title;
|
||||||
|
final bool locked;
|
||||||
|
final int onlineCount;
|
||||||
|
|
||||||
|
factory RoomCard.fromJson(Map<String, dynamic> json) {
|
||||||
|
return RoomCard(
|
||||||
|
roomId: json['room_id'] as String? ?? '',
|
||||||
|
imGroupId: json['im_group_id'] as String? ?? '',
|
||||||
|
title: json['title'] as String? ?? '',
|
||||||
|
locked: json['locked'] as bool? ?? false,
|
||||||
|
onlineCount: (json['online_count'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoomLockResult {
|
||||||
|
RoomLockResult({
|
||||||
|
required this.result,
|
||||||
|
required this.room,
|
||||||
|
required this.serverTimeMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
final RoomCommandResult result;
|
||||||
|
final RoomCard room;
|
||||||
|
final int serverTimeMs;
|
||||||
|
|
||||||
|
factory RoomLockResult.fromJson(Map<String, dynamic> json) {
|
||||||
|
return RoomLockResult(
|
||||||
|
result: RoomCommandResult.fromJson(
|
||||||
|
json['result'] as Map<String, dynamic>? ?? const {},
|
||||||
|
),
|
||||||
|
room: RoomCard.fromJson(
|
||||||
|
json['room'] as Map<String, dynamic>? ?? const {},
|
||||||
|
),
|
||||||
|
serverTimeMs: (json['server_time_ms'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
调用时先解析外层 envelope:只有 `code == "OK"` 时读取 `data`;其他 `code` 按登录态、参数错误、权限错误或服务错误处理,并把 `request_id` 写入客户端日志。
|
||||||
|
|
||||||
|
客户端生成 `command_id` 时按一次用户动作生成一次,例如:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
String newRoomCommandId(String action, String roomId, String userId) {
|
||||||
|
return 'cmd_${action}_${roomId}_${userId}_${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
同一次 HTTP 重试复用原 `command_id`;用户重新输入密码或再次点击设置按钮时生成新的 `command_id`。
|
||||||
@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
`notice-service` 是外部通知投递服务,不是新的业务状态 owner。它从各 owner service 的 outbox 或只读事件源读取已经提交的事实,再投递到用户私有实时通道、后续 push 或站内 inbox。
|
`notice-service` 是外部通知投递服务,不是新的业务状态 owner。它从各 owner service 的 outbox 或只读事件源读取已经提交的事实,再投递到用户私有实时通道、后续 push 或站内 inbox。
|
||||||
|
|
||||||
当前已实现的第一条链路是:
|
当前已实现的链路是:
|
||||||
|
|
||||||
- `wallet-service` 在同一账务事务里写 `wallet_outbox.WalletBalanceChanged`。
|
- `wallet-service` 在同一账务事务里写 `wallet_outbox.WalletBalanceChanged`。
|
||||||
- `notice-service` 抢占该事件,写自己的 `notice_delivery_events` 投递位点。
|
- `notice-service` 抢占该事件,写自己的 `notice_delivery_events` 投递位点。
|
||||||
- `notice-service` 通过腾讯云 IM C2C `TIMCustomElem` 给用户发送 `wallet_notice`。
|
- `notice-service` 通过腾讯云 IM C2C `TIMCustomElem` 给用户发送 `wallet_notice`。
|
||||||
- 客户端收到通知后按 `asset_type + balance_version` 更新本地余额;乱序或重复事件直接丢弃。
|
- 客户端收到通知后按 `asset_type + balance_version` 更新本地余额;乱序或重复事件直接丢弃。
|
||||||
|
- `room-service` 写 `room_outbox.RoomUserKicked` 后,`notice-service` 只给被踢用户发送 `room_notice` C2C 私有通知;房间群系统消息和 IM 群成员移除仍由 room-service 的房间 IM bridge 负责。
|
||||||
|
|
||||||
## 服务边界
|
## 服务边界
|
||||||
|
|
||||||
@ -17,12 +18,17 @@
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `wallet-service` | 余额、流水、交易幂等、`wallet_outbox` 事实源;不调用 IM、不关心客户端在线状态 |
|
| `wallet-service` | 余额、流水、交易幂等、`wallet_outbox` 事实源;不调用 IM、不关心客户端在线状态 |
|
||||||
| `notice-service` | 通知投递位点、抢占锁、退避重试、死信、腾讯云 IM 单聊投递 |
|
| `notice-service` | 通知投递位点、抢占锁、退避重试、死信、腾讯云 IM 单聊投递 |
|
||||||
| `room-service` | 房间状态和房间群系统消息 outbox;不发送钱包私有余额 |
|
| `room-service` | 房间状态、房间群系统消息 outbox、腾讯 IM 群组/成员桥接;不直接发送用户私有通知 |
|
||||||
| `activity-service` | 活动、任务、系统消息业务事实;后续可新增 activity notice 模块 |
|
| `activity-service` | 活动、任务、系统消息业务事实;后续可新增 activity notice 模块 |
|
||||||
| 腾讯云 IM | 客户端长连接、单聊/群消息、离线/漫游投递 |
|
| 腾讯云 IM | 客户端长连接、单聊/群消息、离线/漫游投递 |
|
||||||
|
|
||||||
`notice-service` 不直接修改钱包、房间、用户或活动业务表。它只读取 owner outbox,并写自己的投递状态表。
|
`notice-service` 不直接修改钱包、房间、用户或活动业务表。它只读取 owner outbox,并写自己的投递状态表。
|
||||||
|
|
||||||
|
房间 IM 被拆成两种通道:
|
||||||
|
|
||||||
|
- 房间群通道:`RoomCreated` 建群、`RoomUserKicked` 移除群成员、`room_user_kicked` 群系统消息由 room-service outbox worker 投递。它依赖房间群生命周期和群成员控制,不进入 notice-service。
|
||||||
|
- 用户私有通道:被踢本人可能已经先被移出 IM 群,不能保证收到群消息;因此 notice-service 监听同一条 `RoomUserKicked` 事实,发送 C2C `room_notice` 给 `target_user_id`。
|
||||||
|
|
||||||
## 当前目录结构
|
## 当前目录结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@ -32,6 +38,7 @@ services/notice-service/
|
|||||||
deploy/mysql/initdb/ notice-service 自己的库表
|
deploy/mysql/initdb/ notice-service 自己的库表
|
||||||
internal/app/ 进程装配、health、worker 生命周期
|
internal/app/ 进程装配、health、worker 生命周期
|
||||||
internal/config/ 配置解析和默认值
|
internal/config/ 配置解析和默认值
|
||||||
|
internal/modules/roomnotice/ 房间私有通知模块
|
||||||
internal/modules/walletnotice/ 钱包余额通知模块
|
internal/modules/walletnotice/ 钱包余额通知模块
|
||||||
internal/platform/mysql/ notice-service MySQL 连接池
|
internal/platform/mysql/ notice-service MySQL 连接池
|
||||||
```
|
```
|
||||||
@ -76,6 +83,34 @@ sequenceDiagram
|
|||||||
|
|
||||||
主请求链路只等待钱包扣费和房间状态提交,不等待腾讯云 IM 私有通知。因此送礼请求不会因为 IM 抖动变慢。IM 投递失败时,用户仍可以通过钱包接口拉取最终余额;notice worker 会按退避策略补偿。
|
主请求链路只等待钱包扣费和房间状态提交,不等待腾讯云 IM 私有通知。因此送礼请求不会因为 IM 抖动变慢。IM 投递失败时,用户仍可以通过钱包接口拉取最终余额;notice worker 会按退避策略补偿。
|
||||||
|
|
||||||
|
## 房间踢人私有通知链路
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Owner as Room Owner App
|
||||||
|
participant Gateway as gateway-service
|
||||||
|
participant Room as room-service
|
||||||
|
participant RoomDB as hyapp_room
|
||||||
|
participant Notice as notice-service
|
||||||
|
participant NoticeDB as hyapp_notice
|
||||||
|
participant TencentIM as Tencent IM
|
||||||
|
participant Target as Kicked User App
|
||||||
|
|
||||||
|
Owner->>Gateway: POST /api/v1/rooms/user/kick
|
||||||
|
Gateway->>Room: KickUser(command_id, target_user_id)
|
||||||
|
Room->>Room: remove presence, release mic, write ban
|
||||||
|
Room->>RoomDB: command log + snapshot + room_outbox.RoomUserKicked
|
||||||
|
Room-->>Gateway: success
|
||||||
|
Room-->>TencentIM: room outbox worker deletes group member and sends group event
|
||||||
|
Notice->>RoomDB: claim RoomUserKicked
|
||||||
|
Notice->>NoticeDB: notice_delivery_events=delivering
|
||||||
|
Notice->>TencentIM: C2C TIMCustomElem(room_notice)
|
||||||
|
TencentIM-->>Target: private kicked notice
|
||||||
|
Notice->>NoticeDB: delivered / retryable / failed
|
||||||
|
```
|
||||||
|
|
||||||
|
房间群消息和 C2C 私有通知使用同一个 `event_id`,客户端按 `event_id` 去重即可。notice-service 不修改 `room_outbox.status`,它只用 `notice_delivery_events` 记录自己的 C2C 投递状态,避免和 room-service 群消息 worker 互相覆盖。
|
||||||
|
|
||||||
## Payload 约定
|
## Payload 约定
|
||||||
|
|
||||||
`WalletBalanceChanged` 发送给客户端的核心字段:
|
`WalletBalanceChanged` 发送给客户端的核心字段:
|
||||||
@ -126,16 +161,25 @@ sequenceDiagram
|
|||||||
本地默认关闭 worker,避免误发真实腾讯云 IM:
|
本地默认关闭 worker,避免误发真实腾讯云 IM:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
wallet_database: "hyapp_wallet"
|
||||||
|
room_database: "hyapp_room"
|
||||||
|
|
||||||
tencent_im:
|
tencent_im:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
wallet_notice_worker:
|
wallet_notice_worker:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
room_notice_worker:
|
||||||
|
enabled: false
|
||||||
```
|
```
|
||||||
|
|
||||||
线上启用钱包余额通知必须同时开启:
|
线上启用私有通知必须同时开启腾讯云 IM,并配置对应 owner outbox 所在库名:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
wallet_database: "hyapp_wallet"
|
||||||
|
room_database: "hyapp_room"
|
||||||
|
|
||||||
tencent_im:
|
tencent_im:
|
||||||
enabled: true
|
enabled: true
|
||||||
sdk_app_id: 1400000000
|
sdk_app_id: 1400000000
|
||||||
@ -148,6 +192,13 @@ wallet_notice_worker:
|
|||||||
batch_size: 100
|
batch_size: 100
|
||||||
lock_ttl: 30s
|
lock_ttl: 30s
|
||||||
max_retry_count: 10
|
max_retry_count: 10
|
||||||
|
|
||||||
|
room_notice_worker:
|
||||||
|
enabled: true
|
||||||
|
poll_interval: 500ms
|
||||||
|
batch_size: 100
|
||||||
|
lock_ttl: 30s
|
||||||
|
max_retry_count: 10
|
||||||
```
|
```
|
||||||
|
|
||||||
`notice-service` 使用 `13009` 作为 gRPC health 端口,`13109` 作为 HTTP health 端口。
|
`notice-service` 使用 `13009` 作为 gRPC health 端口,`13109` 作为 HTTP health 端口。
|
||||||
@ -155,10 +206,11 @@ wallet_notice_worker:
|
|||||||
## 后续扩展顺序
|
## 后续扩展顺序
|
||||||
|
|
||||||
1. 钱包余额通知:已按 `walletnotice` 模块落地。
|
1. 钱包余额通知:已按 `walletnotice` 模块落地。
|
||||||
2. 幸运礼物中奖:仍由 `wallet-service` 写余额 outbox;notice 不需要新增特殊账务逻辑,只要客户端按 `balance_version` 更新。
|
2. 房间踢人私有通知:已按 `roomnotice` 模块落地,只消费 `RoomUserKicked` 的 C2C 通知。
|
||||||
3. 活动/系统私有通知:新增 `activitynotice` 模块,读取 activity outbox,复用 `notice_delivery_events` 或新增同语义位点表。
|
3. 幸运礼物中奖:仍由 `wallet-service` 写余额 outbox;notice 不需要新增特殊账务逻辑,只要客户端按 `balance_version` 更新。
|
||||||
4. 站内 inbox:新增 inbox 模块,写 notice-service 自己的 inbox 表,再按客户端分页接口读取。
|
4. 活动/系统私有通知:新增 `activitynotice` 模块,读取 activity outbox,复用 `notice_delivery_events` 或新增同语义位点表。
|
||||||
5. Push:新增 push 模块,和 IM 投递使用不同 `channel`,独立重试和死信。
|
5. 站内 inbox:新增 inbox 模块,写 notice-service 自己的 inbox 表,再按客户端分页接口读取。
|
||||||
|
6. Push:新增 push 模块,和 IM 投递使用不同 `channel`,独立重试和死信。
|
||||||
|
|
||||||
## 真实库验证
|
## 真实库验证
|
||||||
|
|
||||||
|
|||||||
@ -959,7 +959,7 @@ paths:
|
|||||||
- rooms
|
- rooms
|
||||||
summary: Mine 房间关系流
|
summary: Mine 房间关系流
|
||||||
operationId: listRoomFeeds
|
operationId: listRoomFeeds
|
||||||
description: 查询 Mine 页下方 Visited/Friend/Following 房间流;`visited` 读取 JoinRoom 写入的房间访问索引,`friend/following` 由 gateway 先读取 user-service 关系事实,再让 room-service 返回这些关系用户的 active 房间卡片和 `im_group_id`,不作为公共房间发现列表。
|
description: 查询 Mine 页下方 Visited/Friend/Following/Followed 房间流;`visited` 读取 JoinRoom 写入的房间访问索引,`friend/following` 由 gateway 先读取 user-service 关系事实,`followed` 读取 room-service 房间关注关系,再让 room-service 返回 active 房间卡片和 `im_group_id`,不作为公共房间发现列表。
|
||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -971,6 +971,7 @@ paths:
|
|||||||
- visited
|
- visited
|
||||||
- friend
|
- friend
|
||||||
- following
|
- following
|
||||||
|
- followed
|
||||||
- name: cursor
|
- name: cursor
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
@ -982,7 +983,7 @@ paths:
|
|||||||
format: int32
|
format: int32
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: 查询成功;`visited` 由 JoinRoom 成功写入,`friend/following` 同步使用 user-service 好友/关注关系,不依赖 Redis 缓存命中。
|
description: 查询成功;`visited` 由 JoinRoom 成功写入,`friend/following` 同步使用 user-service 好友/关注关系,`followed` 同步使用 room-service 房间关注关系,不依赖 Redis 缓存命中。
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/RoomListEnvelope"
|
$ref: "#/definitions/RoomListEnvelope"
|
||||||
"400":
|
"400":
|
||||||
@ -1068,6 +1069,63 @@ paths:
|
|||||||
$ref: "#/responses/Conflict"
|
$ref: "#/responses/Conflict"
|
||||||
"502":
|
"502":
|
||||||
$ref: "#/responses/UpstreamError"
|
$ref: "#/responses/UpstreamError"
|
||||||
|
/api/v1/rooms/{room_id}/follow:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- rooms
|
||||||
|
summary: 关注房间
|
||||||
|
operationId: followRoom
|
||||||
|
description: 使用 access token user_id 建立当前用户对房间的关注关系;重复关注幂等,`followed_at_ms` 不因重复点击刷新。
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: room_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
pattern: "^[A-Za-z0-9_-]{1,48}$"
|
||||||
|
maxLength: 48
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 关注成功,`data.is_followed=true`。
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/RoomFollowEnvelope"
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "#/responses/Unauthorized"
|
||||||
|
"404":
|
||||||
|
$ref: "#/responses/NotFound"
|
||||||
|
"409":
|
||||||
|
$ref: "#/responses/Conflict"
|
||||||
|
"502":
|
||||||
|
$ref: "#/responses/UpstreamError"
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- rooms
|
||||||
|
summary: 取消关注房间
|
||||||
|
operationId: unfollowRoom
|
||||||
|
description: 使用 access token user_id 取消当前用户对房间的关注关系;未关注时按幂等成功返回 `is_followed=false`。
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: room_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
pattern: "^[A-Za-z0-9_-]{1,48}$"
|
||||||
|
maxLength: 48
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 取消成功,`data.is_followed=false`。
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/RoomFollowEnvelope"
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "#/responses/Unauthorized"
|
||||||
|
"502":
|
||||||
|
$ref: "#/responses/UpstreamError"
|
||||||
/api/v1/rooms/create:
|
/api/v1/rooms/create:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@ -1435,6 +1493,40 @@ paths:
|
|||||||
$ref: "#/responses/Internal"
|
$ref: "#/responses/Internal"
|
||||||
"502":
|
"502":
|
||||||
$ref: "#/responses/UpstreamError"
|
$ref: "#/responses/UpstreamError"
|
||||||
|
/api/v1/rooms/password:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- rooms
|
||||||
|
summary: 设置或清空房间密码
|
||||||
|
operationId: setRoomPassword
|
||||||
|
description: 仅房主可调用。`locked=true` 时 `password` 必须非空;`locked=false` 清空密码并解除锁房。
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: body
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/SetRoomPasswordRequest"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 密码状态修改成功,`data.room.locked` 返回最新锁房标识。
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/SetRoomPasswordEnvelope"
|
||||||
|
"400":
|
||||||
|
$ref: "#/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "#/responses/Unauthorized"
|
||||||
|
"403":
|
||||||
|
$ref: "#/responses/Forbidden"
|
||||||
|
"404":
|
||||||
|
$ref: "#/responses/NotFound"
|
||||||
|
"409":
|
||||||
|
$ref: "#/responses/Conflict"
|
||||||
|
"500":
|
||||||
|
$ref: "#/responses/Internal"
|
||||||
|
"502":
|
||||||
|
$ref: "#/responses/UpstreamError"
|
||||||
/api/v1/rooms/admin/set:
|
/api/v1/rooms/admin/set:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@ -3771,6 +3863,13 @@ definitions:
|
|||||||
description: 房间命令幂等键;客户端按用户动作生成,同一动作的 HTTP 重试必须复用。
|
description: 房间命令幂等键;客户端按用户动作生成,同一动作的 HTTP 重试必须复用。
|
||||||
role:
|
role:
|
||||||
type: string
|
type: string
|
||||||
|
enum:
|
||||||
|
- audience
|
||||||
|
description: 首版固定 audience;上麦成功后客户端再切换发布角色。
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
maxLength: 64
|
||||||
|
description: 锁房时必填;只用于本次 JoinRoom 校验,服务端不保存明文。
|
||||||
RoomHeartbeatRequest:
|
RoomHeartbeatRequest:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@ -3879,6 +3978,19 @@ definitions:
|
|||||||
description: 房间命令幂等键;客户端按用户动作生成,同一动作的 HTTP 重试必须复用。
|
description: 房间命令幂等键;客户端按用户动作生成,同一动作的 HTTP 重试必须复用。
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
SetRoomPasswordRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
room_id:
|
||||||
|
type: string
|
||||||
|
command_id:
|
||||||
|
type: string
|
||||||
|
description: 房间命令幂等键;客户端按用户动作生成,同一动作的 HTTP 重试必须复用。
|
||||||
|
locked:
|
||||||
|
type: boolean
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
description: "`locked=true` 时必填;`locked=false` 时忽略。"
|
||||||
SetRoomAdminRequest:
|
SetRoomAdminRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -4030,6 +4142,8 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
chat_enabled:
|
chat_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
locked:
|
||||||
|
type: boolean
|
||||||
mic_seats:
|
mic_seats:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -4089,6 +4203,9 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
locked:
|
||||||
|
type: boolean
|
||||||
|
description: true 表示该房间需要入房密码;列表和搜索只返回标识,不返回密码。
|
||||||
heat:
|
heat:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
@ -4160,6 +4277,9 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
chat_enabled:
|
chat_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
locked:
|
||||||
|
type: boolean
|
||||||
|
description: true 表示该房间需要入房密码。
|
||||||
heat:
|
heat:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
@ -4303,6 +4423,27 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
room:
|
room:
|
||||||
$ref: "#/definitions/RoomSnapshot"
|
$ref: "#/definitions/RoomSnapshot"
|
||||||
|
is_followed:
|
||||||
|
type: boolean
|
||||||
|
description: 当前 token 用户是否已关注该房间;来自 room-service 房间关注关系,不属于 Room Cell 核心状态。
|
||||||
|
server_time_ms:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
RoomFollowData:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- room_id
|
||||||
|
- is_followed
|
||||||
|
- server_time_ms
|
||||||
|
properties:
|
||||||
|
room_id:
|
||||||
|
type: string
|
||||||
|
is_followed:
|
||||||
|
type: boolean
|
||||||
|
followed_at_ms:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: POST 关注成功时返回;DELETE 取消关注时省略。
|
||||||
server_time_ms:
|
server_time_ms:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
@ -4465,6 +4606,12 @@ definitions:
|
|||||||
$ref: "#/definitions/CommandResult"
|
$ref: "#/definitions/CommandResult"
|
||||||
room:
|
room:
|
||||||
$ref: "#/definitions/RoomSnapshot"
|
$ref: "#/definitions/RoomSnapshot"
|
||||||
|
rtc_kicked:
|
||||||
|
type: boolean
|
||||||
|
description: room-service 是否已通过 TRTC 服务端接口移除目标用户;失败不回滚踢人事实。
|
||||||
|
rtc_kick_error:
|
||||||
|
type: string
|
||||||
|
description: TRTC 服务端踢人失败原因,仅用于排障和告警。
|
||||||
UnbanUserResponse:
|
UnbanUserResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -4747,6 +4894,13 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
$ref: "#/definitions/RoomSnapshotData"
|
$ref: "#/definitions/RoomSnapshotData"
|
||||||
|
RoomFollowEnvelope:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: "#/definitions/RoomFollowData"
|
||||||
JoinRoomEnvelope:
|
JoinRoomEnvelope:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||||
@ -4810,6 +4964,13 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
$ref: "#/definitions/SetChatEnabledResponse"
|
$ref: "#/definitions/SetChatEnabledResponse"
|
||||||
|
SetRoomPasswordEnvelope:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: "#/definitions/UpdateRoomProfileResponse"
|
||||||
SetRoomAdminEnvelope:
|
SetRoomAdminEnvelope:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||||
|
|||||||
46
docs/房间区域置顶.md
Normal file
46
docs/房间区域置顶.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# 房间区域置顶
|
||||||
|
|
||||||
|
房间置顶是公共房间发现列表的运营排序能力。房间列表按 `visible_region_id` 隔离,所以置顶也按区域生效:后台选择一个房间后,后端使用该房间当前 `visible_region_id` 建立 `room_region_pins` 记录。
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
- `room-service` 仍然是房间状态 owner;置顶不进入 Room Cell、snapshot 或 command log。
|
||||||
|
- 置顶只影响 `GET /api/v1/rooms?tab=hot/new` 的排序,不影响进房权限。
|
||||||
|
- 有效置顶条件为 `status=active` 且 `expires_at_ms > now_ms`。
|
||||||
|
- 房间改区域后,旧区域置顶不会自动迁移到新区域;后台需要取消旧置顶并在新区域重新置顶。
|
||||||
|
|
||||||
|
## 排序
|
||||||
|
|
||||||
|
同一区域公共列表排序:
|
||||||
|
|
||||||
|
1. 有效置顶房间排在普通房间之前。
|
||||||
|
2. 置顶房间按 `weight DESC, expires_at_ms DESC, room_id ASC`。
|
||||||
|
3. 非置顶房间保留原排序:`hot` 按 `sort_score DESC`,`new` 按 `created_at_ms DESC`。
|
||||||
|
|
||||||
|
列表 cursor 包含置顶分区、权重、过期时间和原列表排序键,跨页时不会重复返回置顶房间。
|
||||||
|
|
||||||
|
## 后台接口
|
||||||
|
|
||||||
|
后台接口使用后台登录 token 和 `/api/v1` 前缀。
|
||||||
|
|
||||||
|
| 方法 | 路径 | 权限 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `GET` | `/api/v1/admin/rooms/pins` | `room-pin:view` | 查询房间置顶列表,默认只返回有效置顶 |
|
||||||
|
| `POST` | `/api/v1/admin/rooms/pins` | `room-pin:create` | 新增或恢复房间置顶 |
|
||||||
|
| `DELETE` | `/api/v1/admin/rooms/pins/{pin_id}` | `room-pin:cancel` | 取消房间置顶 |
|
||||||
|
|
||||||
|
创建请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"roomId": "room_123",
|
||||||
|
"weight": 100,
|
||||||
|
"durationDays": 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `roomId` 可以是长房间 ID 或短房间 ID,最终落库使用长房间 ID。
|
||||||
|
- `durationDays` 不传时默认 30 天。
|
||||||
|
- 只能置顶当前 `active` 房间。
|
||||||
|
|
||||||
|
列表返回的 `room` 包含房间长短 ID、昵称、头像和区域;`user` 包含房主头像、长短 ID 和昵称;`remainingMs` 是当前 UTC 业务时间下的剩余毫秒数。
|
||||||
10
docs/接口清单.md
10
docs/接口清单.md
@ -54,12 +54,14 @@
|
|||||||
| DELETE | `/api/v1/users/{user_id}/friend` | users | `deleteFriend` | 双向删除好友关系 |
|
| DELETE | `/api/v1/users/{user_id}/friend` | users | `deleteFriend` | 双向删除好友关系 |
|
||||||
| GET | `/api/v1/rooms` | rooms | `listRooms` | 房间发现列表,支持 `tab=hot/new`,卡片返回 `im_group_id` |
|
| GET | `/api/v1/rooms` | rooms | `listRooms` | 房间发现列表,支持 `tab=hot/new`,卡片返回 `im_group_id` |
|
||||||
| GET | `/api/v1/rooms/me` | rooms | `getMyRoom` | 查询 Mine 顶部我的房间卡片 |
|
| GET | `/api/v1/rooms/me` | rooms | `getMyRoom` | 查询 Mine 顶部我的房间卡片 |
|
||||||
| GET | `/api/v1/rooms/feeds` | rooms | `listRoomFeeds` | Mine 页 Visited/Friend/Following 房间流 |
|
| GET | `/api/v1/rooms/feeds` | rooms | `listRoomFeeds` | Mine 页 Visited/Friend/Following/Followed 房间流 |
|
||||||
| GET | `/api/v1/rooms/current` | rooms | `getCurrentRoom` | 查询当前可恢复房间 |
|
| GET | `/api/v1/rooms/current` | rooms | `getCurrentRoom` | 查询当前可恢复房间 |
|
||||||
| GET | `/api/v1/rooms/snapshot` | rooms | `getRoomSnapshot` | 查询房间完整快照 |
|
| GET | `/api/v1/rooms/snapshot` | rooms | `getRoomSnapshot` | 查询房间完整快照 |
|
||||||
| GET | `/api/v1/rooms/{room_id}/detail` | rooms | `getRoomDetail` | 查询房间详情页首屏聚合包 |
|
| GET | `/api/v1/rooms/{room_id}/detail` | rooms | `getRoomDetail` | 查询房间详情页首屏聚合包 |
|
||||||
| GET | `/api/v1/rooms/{room_id}/online-users` | rooms | `listRoomOnlineUsers` | 分页查询房间在线用户 |
|
| GET | `/api/v1/rooms/{room_id}/online-users` | rooms | `listRoomOnlineUsers` | 分页查询房间在线用户 |
|
||||||
| GET | `/api/v1/rooms/{room_id}/gift-panel` | rooms | `getRoomGiftPanel` | 查询房间礼物面板初始化数据 |
|
| GET | `/api/v1/rooms/{room_id}/gift-panel` | rooms | `getRoomGiftPanel` | 查询房间礼物面板初始化数据 |
|
||||||
|
| POST | `/api/v1/rooms/{room_id}/follow` | rooms | `followRoom` | 关注房间 |
|
||||||
|
| DELETE | `/api/v1/rooms/{room_id}/follow` | rooms | `unfollowRoom` | 取消关注房间 |
|
||||||
| POST | `/api/v1/rooms/create` | rooms | `createRoom` | 创建房间 |
|
| POST | `/api/v1/rooms/create` | rooms | `createRoom` | 创建房间 |
|
||||||
| POST | `/api/v1/rooms/profile/update` | rooms | `updateRoomProfile` | 修改房间资料和座位数 |
|
| POST | `/api/v1/rooms/profile/update` | rooms | `updateRoomProfile` | 修改房间资料和座位数 |
|
||||||
| POST | `/api/v1/rooms/join` | rooms | `joinRoom` | 加入房间并返回首屏数据和 RTC token |
|
| POST | `/api/v1/rooms/join` | rooms | `joinRoom` | 加入房间并返回首屏数据和 RTC token |
|
||||||
@ -73,6 +75,7 @@
|
|||||||
| POST | `/api/v1/rooms/mic/mute` | rooms | `setMicMute` | 设置服务端可见麦克风静音状态 |
|
| POST | `/api/v1/rooms/mic/mute` | rooms | `setMicMute` | 设置服务端可见麦克风静音状态 |
|
||||||
| POST | `/api/v1/rooms/mic/lock` | rooms | `setMicSeatLock` | 锁定或解锁麦位 |
|
| POST | `/api/v1/rooms/mic/lock` | rooms | `setMicSeatLock` | 锁定或解锁麦位 |
|
||||||
| POST | `/api/v1/rooms/chat/enabled` | rooms | `setChatEnabled` | 开启或关闭房间公屏 |
|
| POST | `/api/v1/rooms/chat/enabled` | rooms | `setChatEnabled` | 开启或关闭房间公屏 |
|
||||||
|
| POST | `/api/v1/rooms/password` | rooms | `setRoomPassword` | 房主设置或清空房间入房密码 |
|
||||||
| POST | `/api/v1/rooms/admin/set` | rooms | `setRoomAdmin` | 添加或移除房间管理员 |
|
| POST | `/api/v1/rooms/admin/set` | rooms | `setRoomAdmin` | 添加或移除房间管理员 |
|
||||||
| POST | `/api/v1/rooms/user/mute` | rooms | `muteUser` | 设置房间内禁言 |
|
| POST | `/api/v1/rooms/user/mute` | rooms | `muteUser` | 设置房间内禁言 |
|
||||||
| POST | `/api/v1/rooms/user/kick` | rooms | `kickUser` | 踢出房间用户 |
|
| POST | `/api/v1/rooms/user/kick` | rooms | `kickUser` | 踢出房间用户 |
|
||||||
@ -177,6 +180,9 @@
|
|||||||
| POST | `/api/v1/admin/resources/{resource_id}/disable` | admin | `disableResource` | disableResource |
|
| POST | `/api/v1/admin/resources/{resource_id}/disable` | admin | `disableResource` | disableResource |
|
||||||
| POST | `/api/v1/admin/resources/{resource_id}/enable` | admin | `enableResource` | enableResource |
|
| POST | `/api/v1/admin/resources/{resource_id}/enable` | admin | `enableResource` | enableResource |
|
||||||
| GET | `/api/v1/admin/rooms` | admin | `listRooms` | listRooms |
|
| GET | `/api/v1/admin/rooms` | admin | `listRooms` | listRooms |
|
||||||
|
| GET | `/api/v1/admin/rooms/pins` | admin | `listRoomPins` | 查询房间区域置顶列表 |
|
||||||
|
| POST | `/api/v1/admin/rooms/pins` | admin | `createRoomPin` | 新增或恢复房间区域置顶 |
|
||||||
|
| DELETE | `/api/v1/admin/rooms/pins/{pin_id}` | admin | `cancelRoomPin` | 取消房间区域置顶 |
|
||||||
| DELETE | `/api/v1/admin/rooms/{room_id}` | admin | `deleteRoom` | deleteRoom |
|
| DELETE | `/api/v1/admin/rooms/{room_id}` | admin | `deleteRoom` | deleteRoom |
|
||||||
| PATCH | `/api/v1/admin/rooms/{room_id}` | admin | `updateRoom` | updateRoom |
|
| PATCH | `/api/v1/admin/rooms/{room_id}` | admin | `updateRoom` | updateRoom |
|
||||||
| GET | `/api/v1/app/users` | admin | `appListUsers` | appListUsers |
|
| GET | `/api/v1/app/users` | admin | `appListUsers` | appListUsers |
|
||||||
@ -292,6 +298,8 @@
|
|||||||
| POST | `/hyapp.room.v1.RoomCommandService/KickUser` | room-command | `roomKickUser` | 踢出房间用户 |
|
| POST | `/hyapp.room.v1.RoomCommandService/KickUser` | room-command | `roomKickUser` | 踢出房间用户 |
|
||||||
| POST | `/hyapp.room.v1.RoomCommandService/UnbanUser` | room-command | `roomUnbanUser` | 解除房间 ban |
|
| POST | `/hyapp.room.v1.RoomCommandService/UnbanUser` | room-command | `roomUnbanUser` | 解除房间 ban |
|
||||||
| POST | `/hyapp.room.v1.RoomCommandService/SendGift` | room-command | `roomSendGift` | 发送礼物 |
|
| POST | `/hyapp.room.v1.RoomCommandService/SendGift` | room-command | `roomSendGift` | 发送礼物 |
|
||||||
|
| POST | `/hyapp.room.v1.RoomCommandService/FollowRoom` | room-command | `roomFollowRoom` | 建立用户对房间的关注关系 |
|
||||||
|
| POST | `/hyapp.room.v1.RoomCommandService/UnfollowRoom` | room-command | `roomUnfollowRoom` | 取消用户对房间的关注关系 |
|
||||||
| POST | `/hyapp.room.v1.RoomGuardService/CheckSpeakPermission` | room-guard | `roomCheckSpeakPermission` | 发言前守卫 |
|
| POST | `/hyapp.room.v1.RoomGuardService/CheckSpeakPermission` | room-guard | `roomCheckSpeakPermission` | 发言前守卫 |
|
||||||
| POST | `/hyapp.room.v1.RoomGuardService/VerifyRoomPresence` | room-guard | `roomVerifyRoomPresence` | 入群前 presence 守卫 |
|
| POST | `/hyapp.room.v1.RoomGuardService/VerifyRoomPresence` | room-guard | `roomVerifyRoomPresence` | 入群前 presence 守卫 |
|
||||||
| POST | `/hyapp.room.v1.RoomQueryService/GetCurrentRoom` | room-query | `roomGetCurrentRoom` | 查询用户当前可恢复房间 |
|
| POST | `/hyapp.room.v1.RoomQueryService/GetCurrentRoom` | room-query | `roomGetCurrentRoom` | 查询用户当前可恢复房间 |
|
||||||
|
|||||||
@ -110,7 +110,8 @@ sequenceDiagram
|
|||||||
- `GET /api/v1/rooms/feeds?tab=visited` 是房间访问历史,只由 JoinRoom 成功后写入 room-service 的房间 feed 索引;不要使用 `/users/{user_id}/visit` 用户主页访问记录填充。
|
- `GET /api/v1/rooms/feeds?tab=visited` 是房间访问历史,只由 JoinRoom 成功后写入 room-service 的房间 feed 索引;不要使用 `/users/{user_id}/visit` 用户主页访问记录填充。
|
||||||
- `GET /api/v1/rooms/feeds?tab=following` 由 gateway 调用 `ListFollowing` 读取关注用户,再把关系用户和 `followed_at_ms` 传给 room-service 查询 active 房间卡片。
|
- `GET /api/v1/rooms/feeds?tab=following` 由 gateway 调用 `ListFollowing` 读取关注用户,再把关系用户和 `followed_at_ms` 传给 room-service 查询 active 房间卡片。
|
||||||
- `GET /api/v1/rooms/feeds?tab=friend` 由 gateway 调用 `ListFriends` 读取好友用户,再把关系用户和 `friended_at_ms` 传给 room-service 查询 active 房间卡片。
|
- `GET /api/v1/rooms/feeds?tab=friend` 由 gateway 调用 `ListFriends` 读取好友用户,再把关系用户和 `friended_at_ms` 传给 room-service 查询 active 房间卡片。
|
||||||
- 好友/关注关系事实仍归 user-service;room-service 只拥有房间状态和房间列表卡片,不保存好友/关注关系作为事实。
|
- `GET /api/v1/rooms/feeds?tab=followed` 是房间关注列表,只读取 room-service 的 `room_follows` 用户-房间关系,再 join active 房间卡片。
|
||||||
|
- 好友/用户关注关系事实仍归 user-service;房间关注关系事实归 room-service,不能用 user-service 的用户关注表替代。
|
||||||
|
|
||||||
## App Notes
|
## App Notes
|
||||||
|
|
||||||
|
|||||||
@ -93,7 +93,7 @@ sequenceDiagram
|
|||||||
G->>U: GetUser(user_id)
|
G->>U: GetUser(user_id)
|
||||||
U-->>G: region_id
|
U-->>G: region_id
|
||||||
G->>R: ListRooms(region_id, tab, cursor, limit)
|
G->>R: ListRooms(region_id, tab, cursor, limit)
|
||||||
R->>S: Read room_list_entries or Redis ZSET
|
R->>S: Read room_list_entries + active room_region_pins
|
||||||
S-->>R: RoomListItem[]
|
S-->>R: RoomListItem[]
|
||||||
R-->>G: ListRoomsResponse
|
R-->>G: ListRoomsResponse
|
||||||
G-->>C: {code,message,request_id,data}
|
G-->>C: {code,message,request_id,data}
|
||||||
@ -107,6 +107,14 @@ gateway -> room-service.JoinRoom -> Tencent IM join group callback guard
|
|||||||
|
|
||||||
因此列表允许秒级最终一致;真正能否进入房间由 `JoinRoom` 和 IM 守卫决定。
|
因此列表允许秒级最终一致;真正能否进入房间由 `JoinRoom` 和 IM 守卫决定。
|
||||||
|
|
||||||
|
区域置顶只影响当前 `visible_region_id` 的公共发现列表排序。`room_region_pins` 保存 `app_code + visible_region_id + room_id` 的运营置顶关系;查询时只 join `status=active` 且 `expires_at_ms > now_ms` 的记录,排序优先级为:
|
||||||
|
|
||||||
|
1. 有效区域置顶在普通房间之前。
|
||||||
|
2. 置顶房间按 `weight DESC, expires_at_ms DESC, room_id ASC`。
|
||||||
|
3. 普通房间继续按 `hot` 的 `sort_score DESC` 或 `new` 的 `created_at_ms DESC`。
|
||||||
|
|
||||||
|
置顶不进入 Room Cell、command log 或 snapshot;它是房间列表读模型的运营控制面。房间改区域后,旧区域置顶不会自动跨区域生效,后台需要在新区域重新建立置顶。
|
||||||
|
|
||||||
## Room Region Ownership
|
## Room Region Ownership
|
||||||
|
|
||||||
### CreateRoom
|
### CreateRoom
|
||||||
@ -194,6 +202,7 @@ CREATE TABLE IF NOT EXISTS room_list_entries (
|
|||||||
cover_url VARCHAR(512) NOT NULL DEFAULT '',
|
cover_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
mode VARCHAR(64) NOT NULL,
|
mode VARCHAR(64) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
|
locked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
heat BIGINT NOT NULL DEFAULT 0,
|
heat BIGINT NOT NULL DEFAULT 0,
|
||||||
online_count INT NOT NULL DEFAULT 0,
|
online_count INT NOT NULL DEFAULT 0,
|
||||||
seat_count INT NOT NULL DEFAULT 0,
|
seat_count INT NOT NULL DEFAULT 0,
|
||||||
@ -222,6 +231,7 @@ CREATE TABLE IF NOT EXISTS room_list_entries (
|
|||||||
| `cover_url` | `RoomState.RoomExt["cover_url"]` | 创建房间 |
|
| `cover_url` | `RoomState.RoomExt["cover_url"]` | 创建房间 |
|
||||||
| `mode` | RoomMeta/RoomState | 创建房间或模式变更 |
|
| `mode` | RoomMeta/RoomState | 创建房间或模式变更 |
|
||||||
| `status` | RoomState | 创建、关闭、封禁、恢复 |
|
| `status` | RoomState | 创建、关闭、封禁、恢复 |
|
||||||
|
| `locked` | `RoomState.RoomPasswordHash != ""` | SetRoomPassword |
|
||||||
| `heat` | RoomState | SendGift、热度刷新 |
|
| `heat` | RoomState | SendGift、热度刷新 |
|
||||||
| `online_count` | RoomState presence | JoinRoom、LeaveRoom、stale cleanup |
|
| `online_count` | RoomState presence | JoinRoom、LeaveRoom、stale cleanup |
|
||||||
| `occupied_seat_count` | RoomState mic seats | MicUp、MicDown、ChangeMicSeat |
|
| `occupied_seat_count` | RoomState mic seats | MicUp、MicDown、ChangeMicSeat |
|
||||||
@ -348,6 +358,7 @@ message RoomListItem {
|
|||||||
int64 visible_region_id = 12;
|
int64 visible_region_id = 12;
|
||||||
string app_code = 13;
|
string app_code = 13;
|
||||||
string room_short_id = 14;
|
string room_short_id = 14;
|
||||||
|
bool locked = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListRoomsResponse {
|
message ListRoomsResponse {
|
||||||
@ -384,6 +395,7 @@ message ListRoomsResponse {
|
|||||||
- 命令成功提交后调用 `RoomListProjector` 更新 MySQL 投影。
|
- 命令成功提交后调用 `RoomListProjector` 更新 MySQL 投影。
|
||||||
- 投影更新失败时写 `room_outbox` 或独立 `room_projection_tasks` 等待补偿。
|
- 投影更新失败时写 `room_outbox` 或独立 `room_projection_tasks` 等待补偿。
|
||||||
- 列表查询只读 `room_list_entries`,不直接读取 Room Cell 内存。
|
- 列表查询只读 `room_list_entries`,不直接读取 Room Cell 内存。
|
||||||
|
- 公共列表和带 `query` 的搜索列表都从同一投影读取 `locked`;它只表达是否需要入房密码,不携带密码明文或哈希。
|
||||||
|
|
||||||
## Consistency Model
|
## Consistency Model
|
||||||
|
|
||||||
@ -453,7 +465,7 @@ JoinRoom(room_id, user_id)
|
|||||||
|
|
||||||
1. 新增管理端房间区域迁移命令。
|
1. 新增管理端房间区域迁移命令。
|
||||||
2. 新增区域迁移审计表。
|
2. 新增区域迁移审计表。
|
||||||
3. 支持按区域隐藏、封禁或置顶房间。
|
3. 支持按区域隐藏、封禁或置顶房间;置顶由 `room_region_pins` 按区域独立维护。
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
@ -468,6 +480,7 @@ JoinRoom(room_id, user_id)
|
|||||||
| 创建房间 | 写 `rooms.visible_region_id` 和 `room_list_entries` |
|
| 创建房间 | 写 `rooms.visible_region_id` 和 `room_list_entries` |
|
||||||
| 房主改国家 | 已创建房间列表区域不变 |
|
| 房主改国家 | 已创建房间列表区域不变 |
|
||||||
| Join/Leave | 列表在线人数最终更新 |
|
| Join/Leave | 列表在线人数最终更新 |
|
||||||
|
| 区域置顶 | 置顶房间排在同区域普通房间前,翻页不重复 |
|
||||||
| SendGift | 热度和排序分更新 |
|
| SendGift | 热度和排序分更新 |
|
||||||
| Redis miss | MySQL 回源成功 |
|
| Redis miss | MySQL 回源成功 |
|
||||||
| 列表房间已关闭 | `JoinRoom` 拒绝,客户端刷新 |
|
| 列表房间已关闭 | `JoinRoom` 拒绝,客户端刷新 |
|
||||||
|
|||||||
@ -251,7 +251,7 @@ return snapshot
|
|||||||
|
|
||||||
## Tencent IM Message Contract
|
## Tencent IM Message Contract
|
||||||
|
|
||||||
room-service 发给腾讯云 IM 的房间系统消息使用 `TIMCustomElem`。
|
room-service 发给腾讯云 IM 群的房间系统消息使用 `TIMCustomElem`。给单个用户的私有通知不从这个 bridge 发,统一由 notice-service 读取 owner outbox 后走腾讯云 IM C2C。
|
||||||
|
|
||||||
客户端只依赖以下稳定字段:
|
客户端只依赖以下稳定字段:
|
||||||
|
|
||||||
@ -290,6 +290,8 @@ room-service 发给腾讯云 IM 的房间系统消息使用 `TIMCustomElem`。
|
|||||||
| `room_user_kicked` | `RoomUserKicked` |
|
| `room_user_kicked` | `RoomUserKicked` |
|
||||||
| `room_gift_sent` | `RoomGiftSent` |
|
| `room_gift_sent` | `RoomGiftSent` |
|
||||||
|
|
||||||
|
`room_user_kicked` 还会由 notice-service 读取同一条 `RoomUserKicked` 事实,给被踢用户发送 C2C `room_notice`。room-service 仍负责把目标用户移出腾讯 IM 群;notice-service 不处理建群、退群或群成员权限。
|
||||||
|
|
||||||
## Recovery Rules
|
## Recovery Rules
|
||||||
|
|
||||||
节点重启或接管时:
|
节点重启或接管时:
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
| Area | Owner | Rule |
|
| Area | Owner | Rule |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| HTTP 入口 | `gateway-service` | 鉴权、profile gate、协议转换、腾讯 IM/RTC token 签发 |
|
| HTTP 入口 | `gateway-service` | 鉴权、profile gate、协议转换、腾讯 IM/RTC token 签发 |
|
||||||
| 房间业务状态 | `room-service` | Room Cell、presence、麦位、房管、snapshot、outbox |
|
| 房间业务状态 | `room-service` | Room Cell、presence、麦位、房管、snapshot、outbox、房间关注关系 |
|
||||||
| 房间长连接和公屏 | 腾讯云 IM SDK | 客户端登录、房间群、公屏、系统消息接收 |
|
| 房间长连接和公屏 | 腾讯云 IM SDK | 客户端登录、房间群、公屏、系统消息接收 |
|
||||||
| 真实音频频道 | 腾讯云 RTC SDK | 进 RTC 房、切换 audience/anchor、发音频 |
|
| 真实音频频道 | 腾讯云 RTC SDK | 进 RTC 房、切换 audience/anchor、发音频 |
|
||||||
| 用户主数据 | `user-service` | 登录、资料完成状态、用户区域、push token |
|
| 用户主数据 | `user-service` | 登录、资料完成状态、用户区域、push token |
|
||||||
@ -40,7 +40,7 @@ sequenceDiagram
|
|||||||
R-->>G: owner room card from rooms + snapshot
|
R-->>G: owner room card from rooms + snapshot
|
||||||
G-->>C: has_room + room card + im_group_id
|
G-->>C: has_room + room card + im_group_id
|
||||||
|
|
||||||
C->>G: GET /api/v1/rooms/feeds?tab=visited|friend|following
|
C->>G: GET /api/v1/rooms/feeds?tab=visited|friend|following|followed
|
||||||
opt tab=friend|following
|
opt tab=friend|following
|
||||||
G->>U: ListFriends/ListFollowing(cursor)
|
G->>U: ListFriends/ListFollowing(cursor)
|
||||||
U-->>G: related user ids + relation times
|
U-->>G: related user ids + relation times
|
||||||
@ -64,7 +64,14 @@ sequenceDiagram
|
|||||||
G-->>C: optional active-room resync
|
G-->>C: optional active-room resync
|
||||||
|
|
||||||
C->>G: GET /api/v1/rooms/{room_id}/detail
|
C->>G: GET /api/v1/rooms/{room_id}/detail
|
||||||
G-->>C: render-ready room detail package
|
G-->>C: render-ready room detail package + is_followed
|
||||||
|
|
||||||
|
opt user toggles room follow
|
||||||
|
C->>G: POST|DELETE /api/v1/rooms/{room_id}/follow
|
||||||
|
G->>R: FollowRoom|UnfollowRoom(user_id from token)
|
||||||
|
R-->>G: current follow state
|
||||||
|
G-->>C: is_followed
|
||||||
|
end
|
||||||
|
|
||||||
loop while room page is active
|
loop while room page is active
|
||||||
C->>G: POST /api/v1/rooms/heartbeat
|
C->>G: POST /api/v1/rooms/heartbeat
|
||||||
@ -78,32 +85,48 @@ sequenceDiagram
|
|||||||
| 1 | `GET /api/v1/im/usersig` | yes | 用户级 IM 登录票据,可在 App 启动后预取并按 TTL 复用 |
|
| 1 | `GET /api/v1/im/usersig` | yes | 用户级 IM 登录票据,可在 App 启动后预取并按 TTL 复用 |
|
||||||
| 2 | Tencent IM SDK `login` | yes | 客户端先建立 IM 身份,房间群仍等 JoinRoom 成功后再进 |
|
| 2 | Tencent IM SDK `login` | yes | 客户端先建立 IM 身份,房间群仍等 JoinRoom 成功后再进 |
|
||||||
| 3 | `GET /api/v1/rooms/me` | yes for Mine top card | 直接读取 owner 房间权威元数据,不依赖发现页投影 |
|
| 3 | `GET /api/v1/rooms/me` | yes for Mine top card | 直接读取 owner 房间权威元数据,不依赖发现页投影 |
|
||||||
| 4 | `GET /api/v1/rooms/feeds?tab=visited\|friend\|following` | yes for Mine lists | Mine 列表 tab;`visited` 由 JoinRoom 成功写入,`friend/following` 由 gateway 先查 user-service 关系,再让 room-service 返回这些关系用户的 active 房间卡片 |
|
| 4 | `GET /api/v1/rooms/feeds?tab=visited\|friend\|following\|followed` | yes for Mine lists | Mine 列表 tab;`visited` 由 JoinRoom 成功写入,`friend/following` 由 gateway 先查 user-service 关系,`followed` 直接读取 room-service 的房间关注关系 |
|
||||||
| 5 | `POST /api/v1/rooms/join` | yes | 建立业务 presence,并返回首屏房间数据和内嵌 RTC token |
|
| 5 | `POST /api/v1/rooms/join` | yes | 建立业务 presence,并返回首屏房间数据和内嵌 RTC token |
|
||||||
| 6 | Tencent IM SDK `joinGroup(im_group_id)` | yes | 必须在 JoinRoom 成功后执行,IM 回调守卫会校验 room-service presence |
|
| 6 | Tencent IM SDK `joinGroup(im_group_id)` | yes | 必须在 JoinRoom 成功后执行,IM 回调守卫会校验 room-service presence |
|
||||||
| 7 | Tencent RTC SDK `enterRoom` | yes for voice room audio | 使用 JoinRoom 响应里的 `rtc.token` 进入真实音频频道旁听 |
|
| 7 | Tencent RTC SDK `enterRoom` | yes for voice room audio | 使用 JoinRoom 响应里的 `rtc.token` 进入真实音频频道旁听 |
|
||||||
| 8 | `POST /api/v1/rooms/heartbeat` | yes | 刷新业务 presence,避免 stale timeout |
|
| 8 | `POST /api/v1/rooms/heartbeat` | yes | 刷新业务 presence,避免 stale timeout |
|
||||||
| 9 | `GET /api/v1/rooms/{room_id}/detail` | optional after join/restore | 详情页聚合包;用于 JoinRoom 后补偿刷新、回前台、设计稿首屏重绘 |
|
| 9 | `GET /api/v1/rooms/{room_id}/detail` | optional after join/restore | 详情页聚合包;返回 `is_followed`,用于关注按钮初始状态 |
|
||||||
| 10 | `GET /api/v1/rooms/{room_id}/gift-panel` | lazy on gift button | 打开礼物面板时再拉,不放进房间首屏 |
|
| 10 | `POST /api/v1/rooms/{room_id}/follow` / `DELETE /api/v1/rooms/{room_id}/follow` | user action | 建立或取消当前用户对房间的关注关系;gateway 只使用 token user_id |
|
||||||
| 11 | `GET /api/v1/rooms/{room_id}/online-users` | lazy on online count | 点击右上角在线人数时分页拉取 |
|
| 11 | `GET /api/v1/rooms/{room_id}/gift-panel` | lazy on gift button | 打开礼物面板时再拉,不放进房间首屏 |
|
||||||
|
| 12 | `GET /api/v1/rooms/{room_id}/online-users` | lazy on online count | 点击右上角在线人数时分页拉取 |
|
||||||
|
|
||||||
如果 JoinRoom 响应里的 `rtc.available=false`,客户端可以先展示房间页并重试 `POST /api/v1/rtc/token`;RTC 失败不能回滚已经成功的业务 presence。
|
如果 JoinRoom 响应里的 `rtc.available=false`,客户端可以先展示房间页并重试 `POST /api/v1/rtc/token`;RTC 失败不能回滚已经成功的业务 presence。
|
||||||
|
|
||||||
### Mine Feed Cache Boundary
|
### Mine Feed Cache Boundary
|
||||||
|
|
||||||
当前 `friend/following` 不强制加 Redis。原因是关注、取关、同意好友和删除好友都要求列表立即反映关系事实;首版直接读 user-service MySQL 关系表,再查 room-service active 房间卡片,正确性更直接。
|
当前 `friend/following/followed` 不强制加 Redis。原因是关注、取关、同意好友和删除好友都要求列表立即反映关系事实;`friend/following` 直接读 user-service MySQL 关系表,`followed` 直接读 room-service 的 `room_follows`,再查 active 房间卡片,正确性更直接。
|
||||||
|
|
||||||
后续可以加 Redis 的位置:
|
后续可以加 Redis 的位置:
|
||||||
|
|
||||||
- `hot/new` 公共列表:room-service 用 Redis ZSET 缓存排序 room_id,miss 后回源 `room_list_entries`。
|
- `hot/new` 公共列表:room-service 用 Redis ZSET 缓存排序 room_id,miss 后回源 `room_list_entries`。
|
||||||
- 房间卡片短 TTL 缓存:key 可按 `app_code + region_id + room_id`,但 MySQL `room_list_entries` 仍是事实来源。
|
- 房间卡片短 TTL 缓存:key 可按 `app_code + region_id + room_id`,但 MySQL `room_list_entries` 仍是事实来源。
|
||||||
- `friend/following` 关系 ID 短 TTL 缓存:只能放在 gateway 或 user-service 读优化层,`follow/unfollow/accept/delete` 成功后必须主动失效。
|
- `friend/following` 关系 ID 短 TTL 缓存:只能放在 gateway 或 user-service 读优化层,`follow/unfollow/accept/delete` 成功后必须主动失效。
|
||||||
|
- `followed` 房间 ID 短 TTL 缓存:只能放在 room-service 读优化层,`FollowRoom/UnfollowRoom` 成功后必须主动失效。
|
||||||
- `visited` room_id 列表缓存:可以由 JoinRoom 成功后刷新,Redis 写失败不能影响 JoinRoom。
|
- `visited` room_id 列表缓存:可以由 JoinRoom 成功后刷新,Redis 写失败不能影响 JoinRoom。
|
||||||
|
|
||||||
不建议首版缓存 `friend/following` 完整房间卡片列表。这个列表同时依赖 user-service 关系事实、room-service 房间 active 状态、区域过滤和搜索条件,失效面较大;先保持同步查询,等 QPS 或延迟数据证明需要缓存再加。
|
不建议首版缓存 `friend/following/followed` 完整房间卡片列表。这个列表同时依赖关系事实、room-service 房间 active 状态、区域过滤和搜索条件,失效面较大;先保持同步查询,等 QPS 或延迟数据证明需要缓存再加。
|
||||||
|
|
||||||
## Core Interfaces
|
## Core Interfaces
|
||||||
|
|
||||||
|
### Room List Cards
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/rooms?tab=hot&query=100001
|
||||||
|
GET /api/v1/rooms/feeds?tab=followed
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- 公共发现列表、房间搜索和 Mine 页房间流都返回同一套房间卡片字段。
|
||||||
|
- `locked=true` 表示 JoinRoom 需要 `password`;列表和搜索只返回锁房标识,不返回明文或哈希。
|
||||||
|
- `im_group_id` 只代表腾讯 IM 目标群,不能绕过 JoinRoom 的锁房、ban 和 presence 校验。
|
||||||
|
|
||||||
### Join Room
|
### Join Room
|
||||||
|
|
||||||
```http
|
```http
|
||||||
@ -118,7 +141,8 @@ Request:
|
|||||||
{
|
{
|
||||||
"room_id": "lalu_xxx",
|
"room_id": "lalu_xxx",
|
||||||
"command_id": "cmd_join_<room_id>_<user_id>_<nonce>",
|
"command_id": "cmd_join_<room_id>_<user_id>_<nonce>",
|
||||||
"role": "audience"
|
"role": "audience",
|
||||||
|
"password": "1234"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -143,6 +167,7 @@ Response `data`:
|
|||||||
"mode": "voice",
|
"mode": "voice",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"chat_enabled": true,
|
"chat_enabled": true,
|
||||||
|
"locked": true,
|
||||||
"heat": 1000,
|
"heat": 1000,
|
||||||
"online_count": 26,
|
"online_count": 26,
|
||||||
"seat_count": 15,
|
"seat_count": 15,
|
||||||
@ -209,6 +234,8 @@ Rules:
|
|||||||
- `room_id` 必须来自房间列表、分享或恢复接口,不能由客户端拼业务含义。
|
- `room_id` 必须来自房间列表、分享或恢复接口,不能由客户端拼业务含义。
|
||||||
- `command_id` 由客户端按用户动作生成,用于房间命令幂等;重试同一次进房必须复用同一个值。
|
- `command_id` 由客户端按用户动作生成,用于房间命令幂等;重试同一次进房必须复用同一个值。
|
||||||
- `role` 只作为展示默认值,不能通过自报 `admin/host/owner` 获得权限。
|
- `role` 只作为展示默认值,不能通过自报 `admin/host/owner` 获得权限。
|
||||||
|
- 带锁房间的新进入用户必须传正确 `password`;密码错误返回 403,不创建 presence,不签发 RTC token。
|
||||||
|
- 房主不需要密码进入自己的房间;已在房内的用户不会因为房主重新设置密码被挤出。
|
||||||
- `im.group_id` 当前等于 `room_id`,但客户端必须使用服务端返回值,不能硬编码映射规则。
|
- `im.group_id` 当前等于 `room_id`,但客户端必须使用服务端返回值,不能硬编码映射规则。
|
||||||
- JoinRoom 成功后才能调用腾讯 IM SDK `joinGroup`;列表里的 `im_group_id` 只用于准备,不代表入群授权。
|
- JoinRoom 成功后才能调用腾讯 IM SDK `joinGroup`;列表里的 `im_group_id` 只用于准备,不代表入群授权。
|
||||||
- JoinRoom 会尝试内嵌签发 RTC token;签发失败时返回 `rtc.available=false`,客户端可以继续展示房间并单独重试 `/api/v1/rtc/token`。
|
- JoinRoom 会尝试内嵌签发 RTC token;签发失败时返回 `rtc.available=false`,客户端可以继续展示房间并单独重试 `/api/v1/rtc/token`。
|
||||||
@ -229,6 +256,7 @@ Response `data`:
|
|||||||
"seats": [],
|
"seats": [],
|
||||||
"rank_top": [],
|
"rank_top": [],
|
||||||
"online_count": 30,
|
"online_count": 30,
|
||||||
|
"is_followed": true,
|
||||||
"permissions": {},
|
"permissions": {},
|
||||||
"profiles": [],
|
"profiles": [],
|
||||||
"im": {},
|
"im": {},
|
||||||
@ -240,9 +268,35 @@ Rules:
|
|||||||
|
|
||||||
- `detail` 只读 Room Cell/snapshot,不刷新 heartbeat,不隐式进房。
|
- `detail` 只读 Room Cell/snapshot,不刷新 heartbeat,不隐式进房。
|
||||||
- viewer 必须仍在 room-service presence 内;未进房、被踢或被 ban 返回 `PERMISSION_DENIED`。
|
- viewer 必须仍在 room-service presence 内;未进房、被踢或被 ban 返回 `PERMISSION_DENIED`。
|
||||||
|
- `is_followed` 来自 room-service 的 `room_follows` 关系表,只表达当前 token 用户是否关注该房间,不写入 Room Cell 快照。
|
||||||
- `permissions.can_close_room` 表示设计稿右上角电源按钮可用,但当前语义是“退出房间”,不是关闭房间生命周期。
|
- `permissions.can_close_room` 表示设计稿右上角电源按钮可用,但当前语义是“退出房间”,不是关闭房间生命周期。
|
||||||
- `profiles` 只返回首屏需要的 owner、host、viewer、麦位用户和榜单 top 用户。完整在线用户资料走在线用户分页或 `users/room-display-profiles:batch`。
|
- `profiles` 只返回首屏需要的 owner、host、viewer、麦位用户和榜单 top 用户。完整在线用户资料走在线用户分页或 `users/room-display-profiles:batch`。
|
||||||
|
|
||||||
|
### Follow Room
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/rooms/{room_id}/follow
|
||||||
|
DELETE /api/v1/rooms/{room_id}/follow
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
Response `data`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"room_id": "lalu_xxx",
|
||||||
|
"is_followed": true,
|
||||||
|
"followed_at_ms": 1700000000123,
|
||||||
|
"server_time_ms": 1700000000123
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- POST 重复关注保持幂等;已 active 的关注不会刷新 `followed_at_ms`,避免列表排序被重复点击打乱。
|
||||||
|
- DELETE 未关注或历史关系已取消时按幂等成功返回 `is_followed=false`。
|
||||||
|
- 关注列表走 `GET /api/v1/rooms/feeds?tab=followed`,只返回当前仍 active 且命中 viewer 区域桶的房间卡片。
|
||||||
|
|
||||||
### Online Users
|
### Online Users
|
||||||
|
|
||||||
```http
|
```http
|
||||||
@ -638,6 +692,7 @@ Rules:
|
|||||||
| `POST /api/v1/rooms/mic/lock` | `room_id, command_id, seat_no, locked` | 锁/解锁麦位 |
|
| `POST /api/v1/rooms/mic/lock` | `room_id, command_id, seat_no, locked` | 锁/解锁麦位 |
|
||||||
| `POST /api/v1/rooms/mic/mute` | `room_id, command_id, target_user_id, muted` | 修改麦克风静音状态;该状态会同步给房间其他用户 |
|
| `POST /api/v1/rooms/mic/mute` | `room_id, command_id, target_user_id, muted` | 修改麦克风静音状态;该状态会同步给房间其他用户 |
|
||||||
| `POST /api/v1/rooms/chat/enabled` | `room_id, command_id, enabled` | 开/关公屏,影响发言守卫 |
|
| `POST /api/v1/rooms/chat/enabled` | `room_id, command_id, enabled` | 开/关公屏,影响发言守卫 |
|
||||||
|
| `POST /api/v1/rooms/password` | `room_id, command_id, locked, password` | 房主设置或清空入房密码;成功后房间快照、列表和搜索的 `locked` 同步刷新 |
|
||||||
| `POST /api/v1/rooms/admin/set` | `room_id, command_id, target_user_id, is_admin` | 设置或取消房管 |
|
| `POST /api/v1/rooms/admin/set` | `room_id, command_id, target_user_id, is_admin` | 设置或取消房管 |
|
||||||
| `POST /api/v1/rooms/host/transfer` | `room_id, command_id, target_user_id` | 转移主持人 |
|
| `POST /api/v1/rooms/host/transfer` | `room_id, command_id, target_user_id` | 转移主持人 |
|
||||||
| `POST /api/v1/rooms/user/mute` | `room_id, command_id, target_user_id, muted, reason` | 禁言或解禁 |
|
| `POST /api/v1/rooms/user/mute` | `room_id, command_id, target_user_id, muted, reason` | 禁言或解禁 |
|
||||||
@ -648,7 +703,10 @@ Rules:
|
|||||||
Management event handling:
|
Management event handling:
|
||||||
|
|
||||||
- 管理动作成功后,room-service 写 command log、snapshot 和 outbox。
|
- 管理动作成功后,room-service 写 command log、snapshot 和 outbox。
|
||||||
- 客户端应通过腾讯云 IM 房间系统消息更新 UI。
|
- 踢人成功后 room-service 会移除业务 presence、释放麦位、写入房间 ban,并通过 room-service 房间 IM bridge 补偿移出腾讯 IM 房间群。
|
||||||
|
- 被踢本人私有通知由 notice-service 监听 `room_outbox.RoomUserKicked` 后发送 C2C `room_notice`;notice-service 不负责建群、踢群成员或房间群系统消息。
|
||||||
|
- 踢人成功后 room-service 会尽力调用 TRTC `RemoveUserByStrRoomId` 移出 RTC 房间;失败不回滚踢人事实,客户端仍必须按 IM/RTC 回调和后续 403 兜底清理。
|
||||||
|
- 客户端应通过腾讯云 IM 房间系统消息、notice-service C2C 私有通知、IM 群成员移除回调和 RTC 退房回调更新 UI。
|
||||||
- 如果本地 UI 状态和系统消息冲突,以最新 room snapshot 和 `room_version` 为准。
|
- 如果本地 UI 状态和系统消息冲突,以最新 room snapshot 和 `room_version` 为准。
|
||||||
|
|
||||||
## Gift And Interaction
|
## Gift And Interaction
|
||||||
|
|||||||
@ -244,6 +244,9 @@
|
|||||||
| `room:view` | `menu` | 房间列表、详情、房间状态 |
|
| `room:view` | `menu` | 房间列表、详情、房间状态 |
|
||||||
| `room:update` | `button` | 后台修改房间展示资料、模式、状态、区域 |
|
| `room:update` | `button` | 后台修改房间展示资料、模式、状态、区域 |
|
||||||
| `room:delete` | `button` | 后台删除房间 |
|
| `room:delete` | `button` | 后台删除房间 |
|
||||||
|
| `room-pin:view` | `menu` | 房间置顶菜单、置顶列表 |
|
||||||
|
| `room-pin:create` | `button` | 新增或恢复区域房间置顶 |
|
||||||
|
| `room-pin:cancel` | `button` | 取消区域房间置顶 |
|
||||||
| `room-config:view` | `menu` | 房间配置页面、读取座位数配置 |
|
| `room-config:view` | `menu` | 房间配置页面、读取座位数配置 |
|
||||||
| `room-config:update` | `button` | 保存启用座位数和默认座位数 |
|
| `room-config:update` | `button` | 保存启用座位数和默认座位数 |
|
||||||
| `room:create` | `button` | 后台创建房间 |
|
| `room:create` | `button` | 后台创建房间 |
|
||||||
|
|||||||
@ -34,6 +34,48 @@ func (h *Handler) ListRooms(c *gin.Context) {
|
|||||||
response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
|
response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListRoomPins(c *gin.Context) {
|
||||||
|
query, ok := parseRoomPinListQuery(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items, total, err := h.service.ListRoomPins(c.Request.Context(), query)
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取房间置顶列表失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CreateRoomPin(c *gin.Context) {
|
||||||
|
var req createRoomPinRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "房间置顶参数不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pin, err := h.service.CreateRoomPin(c.Request.Context(), req, shared.ActorFromContext(c).UserID)
|
||||||
|
if err != nil {
|
||||||
|
writeMutationError(c, err, "新增房间置顶失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeRoomAuditLog(c, h.audit, "create-room-pin", "room_region_pins", pin.Room.RoomID, "success", "create regional room pin")
|
||||||
|
response.Created(c, pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CancelRoomPin(c *gin.Context) {
|
||||||
|
pinID, ok := parseRoomPinID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pin, err := h.service.CancelRoomPin(c.Request.Context(), pinID, shared.ActorFromContext(c).UserID)
|
||||||
|
if err != nil {
|
||||||
|
writeMutationError(c, err, "取消房间置顶失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeRoomAuditLog(c, h.audit, "cancel-room-pin", "room_region_pins", strconv.FormatInt(pinID, 10), "success", "cancel regional room pin")
|
||||||
|
response.OK(c, pin)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) GetRoomConfig(c *gin.Context) {
|
func (h *Handler) GetRoomConfig(c *gin.Context) {
|
||||||
config, err := h.service.GetRoomConfig(c.Request.Context())
|
config, err := h.service.GetRoomConfig(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -108,6 +150,21 @@ func parseListQuery(c *gin.Context) (listQuery, bool) {
|
|||||||
return normalizeListQuery(query), true
|
return normalizeListQuery(query), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseRoomPinListQuery(c *gin.Context) (roomPinListQuery, bool) {
|
||||||
|
options := shared.ListOptions(c)
|
||||||
|
regionID, ok := parseOptionalInt64Query(c, "regionId", "region_id")
|
||||||
|
if !ok {
|
||||||
|
return roomPinListQuery{}, false
|
||||||
|
}
|
||||||
|
return normalizeRoomPinListQuery(roomPinListQuery{
|
||||||
|
Keyword: options.Keyword,
|
||||||
|
Page: options.Page,
|
||||||
|
PageSize: options.PageSize,
|
||||||
|
RegionID: regionID,
|
||||||
|
Status: options.Status,
|
||||||
|
}), true
|
||||||
|
}
|
||||||
|
|
||||||
func firstQueryValue(c *gin.Context, names ...string) string {
|
func firstQueryValue(c *gin.Context, names ...string) string {
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
if value := strings.TrimSpace(c.Query(name)); value != "" {
|
if value := strings.TrimSpace(c.Query(name)); value != "" {
|
||||||
@ -142,6 +199,16 @@ func parseRoomID(c *gin.Context) (string, bool) {
|
|||||||
return roomID, true
|
return roomID, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseRoomPinID(c *gin.Context) (int64, bool) {
|
||||||
|
raw := strings.TrimSpace(c.Param("pin_id"))
|
||||||
|
id, err := strconv.ParseInt(raw, 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
response.BadRequest(c, "置顶 ID 参数不正确")
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return id, true
|
||||||
|
}
|
||||||
|
|
||||||
func writeMutationError(c *gin.Context, err error, fallback string) {
|
func writeMutationError(c *gin.Context, err error, fallback string) {
|
||||||
if errors.Is(err, ErrNotFound) {
|
if errors.Is(err, ErrNotFound) {
|
||||||
response.NotFound(c, "房间不存在")
|
response.NotFound(c, "房间不存在")
|
||||||
|
|||||||
409
server/admin/internal/modules/roomadmin/pin.go
Normal file
409
server/admin/internal/modules/roomadmin/pin.go
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
package roomadmin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp-admin-server/internal/appctx"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
roomPinStatusActive = "active"
|
||||||
|
roomPinStatusCancelled = "cancelled"
|
||||||
|
roomPinStatusExpired = "expired"
|
||||||
|
roomPinStatusAll = "all"
|
||||||
|
defaultRoomPinDays = int64(30)
|
||||||
|
maxRoomPinDays = int64(3650)
|
||||||
|
roomPinDayMillis = int64(24 * 60 * 60 * 1000)
|
||||||
|
)
|
||||||
|
|
||||||
|
type RoomPinRoom struct {
|
||||||
|
CoverURL string `json:"coverUrl"`
|
||||||
|
RegionName string `json:"regionName"`
|
||||||
|
RoomID string `json:"roomId"`
|
||||||
|
RoomShortID string `json:"roomShortId"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
VisibleRegionID int64 `json:"visibleRegionId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomPin struct {
|
||||||
|
CancelledAtMS int64 `json:"cancelledAtMs"`
|
||||||
|
CancelledByAdminID uint `json:"cancelledByAdminId"`
|
||||||
|
CreatedAtMS int64 `json:"createdAtMs"`
|
||||||
|
CreatedByAdminID uint `json:"createdByAdminId"`
|
||||||
|
ExpiresAtMS int64 `json:"expiresAtMs"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
PinnedAtMS int64 `json:"pinnedAtMs"`
|
||||||
|
RemainingMS int64 `json:"remainingMs"`
|
||||||
|
Room RoomPinRoom `json:"room"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||||||
|
User RoomOwner `json:"user"`
|
||||||
|
Weight int64 `json:"weight"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListRoomPins(ctx context.Context, query roomPinListQuery) ([]RoomPin, int64, error) {
|
||||||
|
if s.db == nil {
|
||||||
|
return nil, 0, fmt.Errorf("room mysql is not configured")
|
||||||
|
}
|
||||||
|
query = normalizeRoomPinListQuery(query)
|
||||||
|
whereSQL, args := roomPinListWhere(ctx, query, time.Now().UTC().UnixMilli())
|
||||||
|
total, err := countRows(ctx, s.db, whereSQL, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := s.db.QueryContext(ctx, `
|
||||||
|
SELECT p.id,
|
||||||
|
p.visible_region_id,
|
||||||
|
p.room_id,
|
||||||
|
p.weight,
|
||||||
|
p.status,
|
||||||
|
p.pinned_at_ms,
|
||||||
|
p.expires_at_ms,
|
||||||
|
p.cancelled_at_ms,
|
||||||
|
p.created_by_admin_id,
|
||||||
|
p.cancelled_by_admin_id,
|
||||||
|
p.created_at_ms,
|
||||||
|
p.updated_at_ms,
|
||||||
|
r.room_short_id,
|
||||||
|
r.title,
|
||||||
|
r.cover_url,
|
||||||
|
r.owner_user_id,
|
||||||
|
r.status
|
||||||
|
`+whereSQL+`
|
||||||
|
ORDER BY p.weight DESC, p.expires_at_ms DESC, p.id DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
`, append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
items := make([]RoomPin, 0, query.PageSize)
|
||||||
|
for rows.Next() {
|
||||||
|
item, err := scanRoomPin(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if err := s.fillRoomPinDetails(ctx, items); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return items, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateRoomPin(ctx context.Context, req createRoomPinRequest, actorID uint) (RoomPin, error) {
|
||||||
|
if s.db == nil {
|
||||||
|
return RoomPin{}, fmt.Errorf("room mysql is not configured")
|
||||||
|
}
|
||||||
|
input, err := normalizeCreateRoomPinRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return RoomPin{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
room, _, err := s.getRoomForPin(ctx, input.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
return RoomPin{}, err
|
||||||
|
}
|
||||||
|
if room.Status != "active" {
|
||||||
|
return RoomPin{}, fmt.Errorf("%w: 只能置顶正常状态房间", ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
nowMS := time.Now().UTC().UnixMilli()
|
||||||
|
expiresAtMS := nowMS + input.DurationDays*roomPinDayMillis
|
||||||
|
appCode := appctx.FromContext(ctx)
|
||||||
|
if _, err := s.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO room_region_pins (
|
||||||
|
app_code, visible_region_id, room_id, weight, status, pinned_at_ms, expires_at_ms,
|
||||||
|
cancelled_at_ms, created_by_admin_id, cancelled_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
weight = VALUES(weight),
|
||||||
|
status = VALUES(status),
|
||||||
|
pinned_at_ms = VALUES(pinned_at_ms),
|
||||||
|
expires_at_ms = VALUES(expires_at_ms),
|
||||||
|
cancelled_at_ms = 0,
|
||||||
|
created_by_admin_id = VALUES(created_by_admin_id),
|
||||||
|
cancelled_by_admin_id = 0,
|
||||||
|
created_at_ms = VALUES(created_at_ms),
|
||||||
|
updated_at_ms = VALUES(updated_at_ms)
|
||||||
|
`, appCode, room.VisibleRegionID, room.RoomID, input.Weight, roomPinStatusActive, nowMS, expiresAtMS, actorID, nowMS, nowMS); err != nil {
|
||||||
|
return RoomPin{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pin, err := s.getRoomPinByRoom(ctx, room.VisibleRegionID, room.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
return RoomPin{}, err
|
||||||
|
}
|
||||||
|
return pin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CancelRoomPin(ctx context.Context, pinID int64, actorID uint) (RoomPin, error) {
|
||||||
|
if s.db == nil {
|
||||||
|
return RoomPin{}, fmt.Errorf("room mysql is not configured")
|
||||||
|
}
|
||||||
|
if pinID <= 0 {
|
||||||
|
return RoomPin{}, fmt.Errorf("%w: 置顶 ID 不正确", ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
|
||||||
|
nowMS := time.Now().UTC().UnixMilli()
|
||||||
|
appCode := appctx.FromContext(ctx)
|
||||||
|
result, err := s.db.ExecContext(ctx, `
|
||||||
|
UPDATE room_region_pins
|
||||||
|
SET status = ?, cancelled_at_ms = ?, cancelled_by_admin_id = ?, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND id = ? AND status <> ?
|
||||||
|
`, roomPinStatusCancelled, nowMS, actorID, nowMS, appCode, pinID, roomPinStatusCancelled)
|
||||||
|
if err != nil {
|
||||||
|
return RoomPin{}, err
|
||||||
|
}
|
||||||
|
if affected, err := result.RowsAffected(); err == nil && affected == 0 {
|
||||||
|
if _, err := s.getRoomPinByID(ctx, pinID); errors.Is(err, ErrNotFound) {
|
||||||
|
return RoomPin{}, ErrNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.getRoomPinByID(ctx, pinID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getRoomForPin(ctx context.Context, roomID string) (RoomPinRoom, int64, error) {
|
||||||
|
appCode := appctx.FromContext(ctx)
|
||||||
|
row := s.db.QueryRowContext(ctx, `
|
||||||
|
SELECT room_id,
|
||||||
|
room_short_id,
|
||||||
|
visible_region_id,
|
||||||
|
owner_user_id,
|
||||||
|
title,
|
||||||
|
cover_url,
|
||||||
|
status
|
||||||
|
FROM room_list_entries
|
||||||
|
WHERE app_code = ? AND (room_id = ? OR room_short_id = ?)
|
||||||
|
LIMIT 1
|
||||||
|
`, appCode, roomID, roomID)
|
||||||
|
|
||||||
|
var room RoomPinRoom
|
||||||
|
var ownerUserID int64
|
||||||
|
if err := row.Scan(&room.RoomID, &room.RoomShortID, &room.VisibleRegionID, &ownerUserID, &room.Title, &room.CoverURL, &room.Status); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return RoomPinRoom{}, 0, ErrNotFound
|
||||||
|
}
|
||||||
|
return RoomPinRoom{}, 0, err
|
||||||
|
}
|
||||||
|
if room.Status != "active" {
|
||||||
|
room.Status = "closed"
|
||||||
|
}
|
||||||
|
return room, ownerUserID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getRoomPinByRoom(ctx context.Context, regionID int64, roomID string) (RoomPin, error) {
|
||||||
|
return s.getRoomPin(ctx, `
|
||||||
|
WHERE p.app_code = ? AND p.visible_region_id = ? AND p.room_id = ?
|
||||||
|
`, appctx.FromContext(ctx), regionID, roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getRoomPinByID(ctx context.Context, pinID int64) (RoomPin, error) {
|
||||||
|
return s.getRoomPin(ctx, `
|
||||||
|
WHERE p.app_code = ? AND p.id = ?
|
||||||
|
`, appctx.FromContext(ctx), pinID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getRoomPin(ctx context.Context, whereSQL string, args ...any) (RoomPin, error) {
|
||||||
|
row := s.db.QueryRowContext(ctx, `
|
||||||
|
SELECT p.id,
|
||||||
|
p.visible_region_id,
|
||||||
|
p.room_id,
|
||||||
|
p.weight,
|
||||||
|
p.status,
|
||||||
|
p.pinned_at_ms,
|
||||||
|
p.expires_at_ms,
|
||||||
|
p.cancelled_at_ms,
|
||||||
|
p.created_by_admin_id,
|
||||||
|
p.cancelled_by_admin_id,
|
||||||
|
p.created_at_ms,
|
||||||
|
p.updated_at_ms,
|
||||||
|
r.room_short_id,
|
||||||
|
r.title,
|
||||||
|
r.cover_url,
|
||||||
|
r.owner_user_id,
|
||||||
|
r.status
|
||||||
|
FROM room_region_pins p
|
||||||
|
JOIN room_list_entries r ON r.app_code = p.app_code AND r.room_id = p.room_id
|
||||||
|
`+whereSQL+`
|
||||||
|
LIMIT 1
|
||||||
|
`, args...)
|
||||||
|
item, err := scanRoomPin(row)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return RoomPin{}, ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return RoomPin{}, err
|
||||||
|
}
|
||||||
|
items := []RoomPin{item}
|
||||||
|
if err := s.fillRoomPinDetails(ctx, items); err != nil {
|
||||||
|
return RoomPin{}, err
|
||||||
|
}
|
||||||
|
return items[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomPinListWhere(ctx context.Context, query roomPinListQuery, nowMS int64) (string, []any) {
|
||||||
|
where := []string{"p.app_code = ?"}
|
||||||
|
args := []any{appctx.FromContext(ctx)}
|
||||||
|
switch query.Status {
|
||||||
|
case roomPinStatusActive:
|
||||||
|
where = append(where, "p.status = ?", "p.expires_at_ms > ?")
|
||||||
|
args = append(args, roomPinStatusActive, nowMS)
|
||||||
|
case roomPinStatusExpired:
|
||||||
|
where = append(where, "p.status = ?", "p.expires_at_ms <= ?")
|
||||||
|
args = append(args, roomPinStatusActive, nowMS)
|
||||||
|
case roomPinStatusCancelled:
|
||||||
|
where = append(where, "p.status = ?")
|
||||||
|
args = append(args, roomPinStatusCancelled)
|
||||||
|
}
|
||||||
|
if query.RegionID > 0 {
|
||||||
|
where = append(where, "p.visible_region_id = ?")
|
||||||
|
args = append(args, query.RegionID)
|
||||||
|
}
|
||||||
|
if query.Keyword != "" {
|
||||||
|
like := "%" + query.Keyword + "%"
|
||||||
|
where = append(where, "(p.room_id LIKE ? OR r.room_short_id LIKE ? OR r.title LIKE ? OR CAST(r.owner_user_id AS CHAR) LIKE ?)")
|
||||||
|
args = append(args, like, like, like, like)
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
FROM room_region_pins p
|
||||||
|
JOIN room_list_entries r ON r.app_code = p.app_code AND r.room_id = p.room_id
|
||||||
|
WHERE ` + strings.Join(where, " AND "), args
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRoomPinListQuery(query roomPinListQuery) roomPinListQuery {
|
||||||
|
if query.Page < 1 {
|
||||||
|
query.Page = 1
|
||||||
|
}
|
||||||
|
if query.PageSize < 1 {
|
||||||
|
query.PageSize = 20
|
||||||
|
}
|
||||||
|
if query.PageSize > 100 {
|
||||||
|
query.PageSize = 100
|
||||||
|
}
|
||||||
|
query.Keyword = strings.TrimSpace(query.Keyword)
|
||||||
|
query.Status = normalizeRoomPinStatus(query.Status)
|
||||||
|
if query.RegionID < 0 {
|
||||||
|
query.RegionID = 0
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRoomPinStatus(status string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||||
|
case "", roomPinStatusActive:
|
||||||
|
return roomPinStatusActive
|
||||||
|
case roomPinStatusExpired:
|
||||||
|
return roomPinStatusExpired
|
||||||
|
case roomPinStatusCancelled:
|
||||||
|
return roomPinStatusCancelled
|
||||||
|
case roomPinStatusAll:
|
||||||
|
return roomPinStatusAll
|
||||||
|
default:
|
||||||
|
return roomPinStatusActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCreateRoomPinRequest(req createRoomPinRequest) (createRoomPinRequest, error) {
|
||||||
|
req.RoomID = strings.TrimSpace(req.RoomID)
|
||||||
|
if req.RoomID == "" || len(req.RoomID) > 64 {
|
||||||
|
return req, fmt.Errorf("%w: 请选择房间", ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
if req.Weight < 0 {
|
||||||
|
return req, fmt.Errorf("%w: 权重不能小于 0", ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
if req.DurationDays == 0 {
|
||||||
|
req.DurationDays = defaultRoomPinDays
|
||||||
|
}
|
||||||
|
if req.DurationDays < 0 || req.DurationDays > maxRoomPinDays {
|
||||||
|
return req, fmt.Errorf("%w: 置顶时间必须在 1 到 3650 天之间", ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type roomPinScanner interface {
|
||||||
|
Scan(dest ...any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanRoomPin(row roomPinScanner) (RoomPin, error) {
|
||||||
|
var item RoomPin
|
||||||
|
var ownerUserID int64
|
||||||
|
err := row.Scan(
|
||||||
|
&item.ID,
|
||||||
|
&item.Room.VisibleRegionID,
|
||||||
|
&item.Room.RoomID,
|
||||||
|
&item.Weight,
|
||||||
|
&item.Status,
|
||||||
|
&item.PinnedAtMS,
|
||||||
|
&item.ExpiresAtMS,
|
||||||
|
&item.CancelledAtMS,
|
||||||
|
&item.CreatedByAdminID,
|
||||||
|
&item.CancelledByAdminID,
|
||||||
|
&item.CreatedAtMS,
|
||||||
|
&item.UpdatedAtMS,
|
||||||
|
&item.Room.RoomShortID,
|
||||||
|
&item.Room.Title,
|
||||||
|
&item.Room.CoverURL,
|
||||||
|
&ownerUserID,
|
||||||
|
&item.Room.Status,
|
||||||
|
)
|
||||||
|
item.User = RoomOwner{UserID: strconv.FormatInt(ownerUserID, 10)}
|
||||||
|
item.RemainingMS = item.ExpiresAtMS - time.Now().UTC().UnixMilli()
|
||||||
|
if item.RemainingMS < 0 {
|
||||||
|
item.RemainingMS = 0
|
||||||
|
}
|
||||||
|
if item.Room.Status != "active" {
|
||||||
|
item.Room.Status = "closed"
|
||||||
|
}
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) fillRoomPinDetails(ctx context.Context, items []RoomPin) error {
|
||||||
|
if s.userDB == nil || len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerIDs := make([]int64, 0, len(items))
|
||||||
|
regionIDs := make([]int64, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
if ownerID, err := strconv.ParseInt(item.User.UserID, 10, 64); err == nil && ownerID > 0 {
|
||||||
|
ownerIDs = append(ownerIDs, ownerID)
|
||||||
|
}
|
||||||
|
if item.Room.VisibleRegionID > 0 {
|
||||||
|
regionIDs = append(regionIDs, item.Room.VisibleRegionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
owners, err := s.queryRoomOwners(ctx, uniqueInt64s(ownerIDs))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
regions, err := s.queryRegionNames(ctx, uniqueInt64s(regionIDs))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := range items {
|
||||||
|
if ownerID, err := strconv.ParseInt(items[index].User.UserID, 10, 64); err == nil {
|
||||||
|
if owner, ok := owners[ownerID]; ok {
|
||||||
|
items[index].User = owner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name, ok := regions[items[index].Room.VisibleRegionID]; ok {
|
||||||
|
items[index].Room.RegionName = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
46
server/admin/internal/modules/roomadmin/pin_test.go
Normal file
46
server/admin/internal/modules/roomadmin/pin_test.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package roomadmin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeRoomPinListQueryDefaultsToActive(t *testing.T) {
|
||||||
|
query := normalizeRoomPinListQuery(roomPinListQuery{Page: -1, PageSize: 200, Status: "unknown", RegionID: -10, Keyword: " 123 "})
|
||||||
|
if query.Page != 1 || query.PageSize != 100 {
|
||||||
|
t.Fatalf("pagination normalization mismatch: %+v", query)
|
||||||
|
}
|
||||||
|
if query.Status != roomPinStatusActive || query.RegionID != 0 || query.Keyword != "123" {
|
||||||
|
t.Fatalf("filter normalization mismatch: %+v", query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeCreateRoomPinRequestDefaultsDuration(t *testing.T) {
|
||||||
|
req, err := normalizeCreateRoomPinRequest(createRoomPinRequest{RoomID: " room-1 ", Weight: 10})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("normalize create pin failed: %v", err)
|
||||||
|
}
|
||||||
|
if req.RoomID != "room-1" || req.DurationDays != defaultRoomPinDays || req.Weight != 10 {
|
||||||
|
t.Fatalf("create pin normalization mismatch: %+v", req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeCreateRoomPinRequestRejectsInvalidInput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
req createRoomPinRequest
|
||||||
|
}{
|
||||||
|
{name: "empty_room", req: createRoomPinRequest{Weight: 1, DurationDays: 30}},
|
||||||
|
{name: "negative_weight", req: createRoomPinRequest{RoomID: "room-1", Weight: -1, DurationDays: 30}},
|
||||||
|
{name: "duration_too_large", req: createRoomPinRequest{RoomID: "room-1", Weight: 1, DurationDays: maxRoomPinDays + 1}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
_, err := normalizeCreateRoomPinRequest(test.req)
|
||||||
|
if !errors.Is(err, ErrInvalidArgument) {
|
||||||
|
t.Fatalf("expected ErrInvalidArgument, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,20 @@ type listQuery struct {
|
|||||||
SortDirection string
|
SortDirection string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type roomPinListQuery struct {
|
||||||
|
Keyword string
|
||||||
|
Page int
|
||||||
|
PageSize int
|
||||||
|
RegionID int64
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
type createRoomPinRequest struct {
|
||||||
|
DurationDays int64 `json:"durationDays"`
|
||||||
|
RoomID string `json:"roomId"`
|
||||||
|
Weight int64 `json:"weight"`
|
||||||
|
}
|
||||||
|
|
||||||
type updateRoomRequest struct {
|
type updateRoomRequest struct {
|
||||||
CoverURL *string `json:"coverUrl"`
|
CoverURL *string `json:"coverUrl"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
|
|||||||
@ -12,6 +12,9 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected.GET("/admin/rooms", middleware.RequirePermission("room:view"), h.ListRooms)
|
protected.GET("/admin/rooms", middleware.RequirePermission("room:view"), h.ListRooms)
|
||||||
|
protected.GET("/admin/rooms/pins", middleware.RequirePermission("room-pin:view"), h.ListRoomPins)
|
||||||
|
protected.POST("/admin/rooms/pins", middleware.RequirePermission("room-pin:create"), h.CreateRoomPin)
|
||||||
|
protected.DELETE("/admin/rooms/pins/:pin_id", middleware.RequirePermission("room-pin:cancel"), h.CancelRoomPin)
|
||||||
protected.GET("/admin/rooms/config", middleware.RequirePermission("room-config:view"), h.GetRoomConfig)
|
protected.GET("/admin/rooms/config", middleware.RequirePermission("room-config:view"), h.GetRoomConfig)
|
||||||
protected.PUT("/admin/rooms/config", middleware.RequirePermission("room-config:update"), h.UpdateRoomConfig)
|
protected.PUT("/admin/rooms/config", middleware.RequirePermission("room-config:update"), h.UpdateRoomConfig)
|
||||||
protected.PATCH("/admin/rooms/:room_id", middleware.RequirePermission("room:update"), h.UpdateRoom)
|
protected.PATCH("/admin/rooms/:room_id", middleware.RequirePermission("room:update"), h.UpdateRoom)
|
||||||
|
|||||||
@ -42,6 +42,7 @@ type Room struct {
|
|||||||
OwnerUserID string `json:"ownerUserId"`
|
OwnerUserID string `json:"ownerUserId"`
|
||||||
RegionName string `json:"regionName"`
|
RegionName string `json:"regionName"`
|
||||||
RoomID string `json:"roomId"`
|
RoomID string `json:"roomId"`
|
||||||
|
RoomShortID string `json:"roomShortId"`
|
||||||
RoomContribution int64 `json:"roomContribution"`
|
RoomContribution int64 `json:"roomContribution"`
|
||||||
SeatCount int32 `json:"seatCount"`
|
SeatCount int32 `json:"seatCount"`
|
||||||
SortScore int64 `json:"sortScore"`
|
SortScore int64 `json:"sortScore"`
|
||||||
@ -76,8 +77,8 @@ func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64
|
|||||||
}
|
}
|
||||||
if query.Keyword != "" {
|
if query.Keyword != "" {
|
||||||
like := "%" + query.Keyword + "%"
|
like := "%" + query.Keyword + "%"
|
||||||
whereSQL += " AND (rle.room_id LIKE ? OR rle.title LIKE ? OR CAST(rle.owner_user_id AS CHAR) LIKE ?)"
|
whereSQL += " AND (rle.room_id LIKE ? OR rle.room_short_id LIKE ? OR rle.title LIKE ? OR CAST(rle.owner_user_id AS CHAR) LIKE ?)"
|
||||||
args = append(args, like, like, like)
|
args = append(args, like, like, like, like)
|
||||||
}
|
}
|
||||||
total, err := countRows(ctx, s.db, whereSQL, args...)
|
total, err := countRows(ctx, s.db, whereSQL, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -85,6 +86,7 @@ func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64
|
|||||||
}
|
}
|
||||||
rows, err := s.db.QueryContext(ctx, fmt.Sprintf(`
|
rows, err := s.db.QueryContext(ctx, fmt.Sprintf(`
|
||||||
SELECT rle.room_id,
|
SELECT rle.room_id,
|
||||||
|
rle.room_short_id,
|
||||||
rle.visible_region_id,
|
rle.visible_region_id,
|
||||||
rle.owner_user_id,
|
rle.owner_user_id,
|
||||||
rle.title,
|
rle.title,
|
||||||
@ -131,6 +133,7 @@ func (s *Service) GetRoom(ctx context.Context, roomID string) (Room, error) {
|
|||||||
appCode := appctx.FromContext(ctx)
|
appCode := appctx.FromContext(ctx)
|
||||||
row := s.db.QueryRowContext(ctx, `
|
row := s.db.QueryRowContext(ctx, `
|
||||||
SELECT room_id,
|
SELECT room_id,
|
||||||
|
room_short_id,
|
||||||
visible_region_id,
|
visible_region_id,
|
||||||
owner_user_id,
|
owner_user_id,
|
||||||
title,
|
title,
|
||||||
@ -389,6 +392,7 @@ func scanRoom(row roomScanner) (Room, error) {
|
|||||||
var ownerUserID int64
|
var ownerUserID int64
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&item.RoomID,
|
&item.RoomID,
|
||||||
|
&item.RoomShortID,
|
||||||
&item.VisibleRegionID,
|
&item.VisibleRegionID,
|
||||||
&ownerUserID,
|
&ownerUserID,
|
||||||
&item.Title,
|
&item.Title,
|
||||||
|
|||||||
@ -23,6 +23,9 @@ var defaultPermissions = []model.Permission{
|
|||||||
{Name: "房间查看", Code: "room:view", Kind: "menu"},
|
{Name: "房间查看", Code: "room:view", Kind: "menu"},
|
||||||
{Name: "房间更新", Code: "room:update", Kind: "button"},
|
{Name: "房间更新", Code: "room:update", Kind: "button"},
|
||||||
{Name: "房间删除", Code: "room:delete", Kind: "button"},
|
{Name: "房间删除", Code: "room:delete", Kind: "button"},
|
||||||
|
{Name: "房间置顶查看", Code: "room-pin:view", Kind: "menu"},
|
||||||
|
{Name: "房间置顶创建", Code: "room-pin:create", Kind: "button"},
|
||||||
|
{Name: "房间置顶取消", Code: "room-pin:cancel", Kind: "button"},
|
||||||
{Name: "房间配置查看", Code: "room-config:view", Kind: "menu"},
|
{Name: "房间配置查看", Code: "room-config:view", Kind: "menu"},
|
||||||
{Name: "房间配置更新", Code: "room-config:update", Kind: "button"},
|
{Name: "房间配置更新", Code: "room-config:update", Kind: "button"},
|
||||||
{Name: "APP 配置查看", Code: "app-config:view", Kind: "menu"},
|
{Name: "APP 配置查看", Code: "app-config:view", Kind: "menu"},
|
||||||
@ -192,7 +195,8 @@ func (s *Store) seedMenus() error {
|
|||||||
{ParentID: &appUsersID, Title: "封禁列表", Code: "app-user-bans", Path: "/app/users/bans", Icon: "block", PermissionCode: "app-user:view", Sort: 61, Visible: true},
|
{ParentID: &appUsersID, Title: "封禁列表", Code: "app-user-bans", Path: "/app/users/bans", Icon: "block", PermissionCode: "app-user:view", Sort: 61, Visible: true},
|
||||||
{ParentID: &appUsersID, Title: "登录日志", Code: "app-user-login-logs", Path: "/app/users/login-logs", Icon: "login", PermissionCode: "app-user:view", Sort: 62, Visible: true},
|
{ParentID: &appUsersID, Title: "登录日志", Code: "app-user-login-logs", Path: "/app/users/login-logs", Icon: "login", PermissionCode: "app-user:view", Sort: 62, Visible: true},
|
||||||
{ParentID: &roomsID, Title: "房间列表", Code: "room-list", Path: "/rooms", Icon: "room", PermissionCode: "room:view", Sort: 65, Visible: true},
|
{ParentID: &roomsID, Title: "房间列表", Code: "room-list", Path: "/rooms", Icon: "room", PermissionCode: "room:view", Sort: 65, Visible: true},
|
||||||
{ParentID: &roomsID, Title: "房间配置", Code: "room-config", Path: "/rooms/config", Icon: "settings", PermissionCode: "room-config:view", Sort: 66, Visible: true},
|
{ParentID: &roomsID, Title: "房间置顶", Code: "room-pins", Path: "/rooms/pins", Icon: "push_pin", PermissionCode: "room-pin:view", Sort: 66, Visible: true},
|
||||||
|
{ParentID: &roomsID, Title: "房间配置", Code: "room-config", Path: "/rooms/config", Icon: "settings", PermissionCode: "room-config:view", Sort: 67, Visible: true},
|
||||||
{ParentID: &appConfigID, Title: "H5配置", Code: "app-config-h5", Path: "/app-config/h5", Icon: "settings", PermissionCode: "app-config:view", Sort: 66, Visible: true},
|
{ParentID: &appConfigID, Title: "H5配置", Code: "app-config-h5", Path: "/app-config/h5", Icon: "settings", PermissionCode: "app-config:view", Sort: 66, Visible: true},
|
||||||
{ParentID: &appConfigID, Title: "BANNER配置", Code: "app-config-banners", Path: "/app-config/banners", Icon: "image", PermissionCode: "app-config:view", Sort: 67, Visible: true},
|
{ParentID: &appConfigID, Title: "BANNER配置", Code: "app-config-banners", Path: "/app-config/banners", Icon: "image", PermissionCode: "app-config:view", Sort: 67, Visible: true},
|
||||||
{ParentID: &appConfigID, Title: "内购配置", Code: "payment-recharge-products", Path: "/app-config/recharge-products", Icon: "wallet", PermissionCode: "payment-product:view", Sort: 68, Visible: true},
|
{ParentID: &appConfigID, Title: "内购配置", Code: "payment-recharge-products", Path: "/app-config/recharge-products", Icon: "wallet", PermissionCode: "payment-product:view", Sort: 68, Visible: true},
|
||||||
@ -423,7 +427,7 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"overview:view",
|
"overview:view",
|
||||||
"user:view", "user:status",
|
"user:view", "user:status",
|
||||||
"app-user:view", "app-user:update", "app-user:status", "app-user:password",
|
"app-user:view", "app-user:update", "app-user:status", "app-user:password",
|
||||||
"room:view", "room:update", "room:delete", "room-config:view", "room-config:update",
|
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update",
|
||||||
"app-config:view", "app-config:update",
|
"app-config:view", "app-config:update",
|
||||||
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
||||||
"resource:view", "resource:create", "resource:update",
|
"resource:view", "resource:create", "resource:update",
|
||||||
@ -444,13 +448,14 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"upload:create",
|
"upload:create",
|
||||||
}
|
}
|
||||||
case "auditor":
|
case "auditor":
|
||||||
return []string{"overview:view", "log:view", "user:view", "app-user:view", "room:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "coin-ledger:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "role:view", "permission:view", "job:view"}
|
return []string{"overview:view", "log:view", "user:view", "app-user:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "coin-ledger:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "role:view", "permission:view", "job:view"}
|
||||||
case "readonly":
|
case "readonly":
|
||||||
return []string{
|
return []string{
|
||||||
"overview:view",
|
"overview:view",
|
||||||
"user:view",
|
"user:view",
|
||||||
"app-user:view",
|
"app-user:view",
|
||||||
"room:view",
|
"room:view",
|
||||||
|
"room-pin:view",
|
||||||
"room-config:view",
|
"room-config:view",
|
||||||
"app-config:view",
|
"app-config:view",
|
||||||
"app-version:view",
|
"app-version:view",
|
||||||
@ -485,7 +490,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
|||||||
case "ops-admin":
|
case "ops-admin":
|
||||||
return []string{
|
return []string{
|
||||||
"app-user:update", "app-user:status", "app-user:password",
|
"app-user:update", "app-user:status", "app-user:password",
|
||||||
"room:view", "room:update", "room:delete", "room-config:view", "room-config:update",
|
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update",
|
||||||
"app-config:view", "app-config:update",
|
"app-config:view", "app-config:update",
|
||||||
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
||||||
"resource:view", "resource:create", "resource:update",
|
"resource:view", "resource:create", "resource:update",
|
||||||
@ -501,7 +506,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
|||||||
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
||||||
}
|
}
|
||||||
case "auditor", "readonly":
|
case "auditor", "readonly":
|
||||||
return []string{"room:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "coin-seller:view", "coin-ledger:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view"}
|
return []string{"room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "coin-seller:view", "coin-ledger:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view"}
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,11 +22,14 @@ type RoomClient interface {
|
|||||||
ApplyRTCEvent(ctx context.Context, req *roomv1.ApplyRTCEventRequest) (*roomv1.ApplyRTCEventResponse, error)
|
ApplyRTCEvent(ctx context.Context, req *roomv1.ApplyRTCEventRequest) (*roomv1.ApplyRTCEventResponse, error)
|
||||||
SetMicSeatLock(ctx context.Context, req *roomv1.SetMicSeatLockRequest) (*roomv1.SetMicSeatLockResponse, error)
|
SetMicSeatLock(ctx context.Context, req *roomv1.SetMicSeatLockRequest) (*roomv1.SetMicSeatLockResponse, error)
|
||||||
SetChatEnabled(ctx context.Context, req *roomv1.SetChatEnabledRequest) (*roomv1.SetChatEnabledResponse, error)
|
SetChatEnabled(ctx context.Context, req *roomv1.SetChatEnabledRequest) (*roomv1.SetChatEnabledResponse, error)
|
||||||
|
SetRoomPassword(ctx context.Context, req *roomv1.SetRoomPasswordRequest) (*roomv1.SetRoomPasswordResponse, error)
|
||||||
SetRoomAdmin(ctx context.Context, req *roomv1.SetRoomAdminRequest) (*roomv1.SetRoomAdminResponse, error)
|
SetRoomAdmin(ctx context.Context, req *roomv1.SetRoomAdminRequest) (*roomv1.SetRoomAdminResponse, error)
|
||||||
MuteUser(ctx context.Context, req *roomv1.MuteUserRequest) (*roomv1.MuteUserResponse, error)
|
MuteUser(ctx context.Context, req *roomv1.MuteUserRequest) (*roomv1.MuteUserResponse, error)
|
||||||
KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*roomv1.KickUserResponse, error)
|
KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*roomv1.KickUserResponse, error)
|
||||||
UnbanUser(ctx context.Context, req *roomv1.UnbanUserRequest) (*roomv1.UnbanUserResponse, error)
|
UnbanUser(ctx context.Context, req *roomv1.UnbanUserRequest) (*roomv1.UnbanUserResponse, error)
|
||||||
SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*roomv1.SendGiftResponse, error)
|
SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*roomv1.SendGiftResponse, error)
|
||||||
|
FollowRoom(ctx context.Context, req *roomv1.FollowRoomRequest) (*roomv1.FollowRoomResponse, error)
|
||||||
|
UnfollowRoom(ctx context.Context, req *roomv1.UnfollowRoomRequest) (*roomv1.UnfollowRoomResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomGuardClient 抽象 gateway 对 room-service 实时守卫 RPC 的依赖。
|
// RoomGuardClient 抽象 gateway 对 room-service 实时守卫 RPC 的依赖。
|
||||||
@ -131,6 +134,10 @@ func (c *grpcRoomClient) SetChatEnabled(ctx context.Context, req *roomv1.SetChat
|
|||||||
return c.client.SetChatEnabled(ctx, req)
|
return c.client.SetChatEnabled(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *grpcRoomClient) SetRoomPassword(ctx context.Context, req *roomv1.SetRoomPasswordRequest) (*roomv1.SetRoomPasswordResponse, error) {
|
||||||
|
return c.client.SetRoomPassword(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *grpcRoomClient) SetRoomAdmin(ctx context.Context, req *roomv1.SetRoomAdminRequest) (*roomv1.SetRoomAdminResponse, error) {
|
func (c *grpcRoomClient) SetRoomAdmin(ctx context.Context, req *roomv1.SetRoomAdminRequest) (*roomv1.SetRoomAdminResponse, error) {
|
||||||
return c.client.SetRoomAdmin(ctx, req)
|
return c.client.SetRoomAdmin(ctx, req)
|
||||||
}
|
}
|
||||||
@ -151,6 +158,14 @@ func (c *grpcRoomClient) SendGift(ctx context.Context, req *roomv1.SendGiftReque
|
|||||||
return c.client.SendGift(ctx, req)
|
return c.client.SendGift(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *grpcRoomClient) FollowRoom(ctx context.Context, req *roomv1.FollowRoomRequest) (*roomv1.FollowRoomResponse, error) {
|
||||||
|
return c.client.FollowRoom(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcRoomClient) UnfollowRoom(ctx context.Context, req *roomv1.UnfollowRoomRequest) (*roomv1.UnfollowRoomResponse, error) {
|
||||||
|
return c.client.UnfollowRoom(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *grpcRoomGuardClient) CheckSpeakPermission(ctx context.Context, req *roomv1.CheckSpeakPermissionRequest) (*roomv1.CheckSpeakPermissionResponse, error) {
|
func (c *grpcRoomGuardClient) CheckSpeakPermission(ctx context.Context, req *roomv1.CheckSpeakPermissionRequest) (*roomv1.CheckSpeakPermissionResponse, error) {
|
||||||
return c.client.CheckSpeakPermission(ctx, req)
|
return c.client.CheckSpeakPermission(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
package activityapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/client"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler owns activity-facing HTTP endpoints.
|
||||||
|
// It keeps task and registration reward reads/writes behind activity-service, with gateway only adding auth context.
|
||||||
|
type Handler struct {
|
||||||
|
taskClient client.TaskClient
|
||||||
|
registrationReward client.RegistrationRewardClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
TaskClient client.TaskClient
|
||||||
|
RegistrationReward client.RegistrationRewardClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
taskClient: config.TaskClient,
|
||||||
|
registrationReward: config.RegistrationReward,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) TaskHandlers() httproutes.TaskHandlers {
|
||||||
|
return httproutes.TaskHandlers{
|
||||||
|
ListTaskTabs: h.listTaskTabs,
|
||||||
|
ClaimTaskReward: h.claimTaskReward,
|
||||||
|
GetRegistrationRewardEligibility: h.getRegistrationRewardEligibility,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package activityapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -29,7 +29,7 @@ func (h *Handler) getRegistrationRewardEligibility(writer http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.registrationReward.GetRegistrationRewardEligibility(request.Context(), &activityv1.GetRegistrationRewardEligibilityRequest{
|
resp, err := h.registrationReward.GetRegistrationRewardEligibility(request.Context(), &activityv1.GetRegistrationRewardEligibilityRequest{
|
||||||
Meta: activityMeta(request),
|
Meta: httpkit.ActivityMeta(request),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package activityapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
@ -94,7 +94,7 @@ func (h *Handler) listTaskTabs(writer http.ResponseWriter, request *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.taskClient.ListUserTasks(request.Context(), &activityv1.ListUserTasksRequest{
|
resp, err := h.taskClient.ListUserTasks(request.Context(), &activityv1.ListUserTasksRequest{
|
||||||
Meta: activityMeta(request),
|
Meta: httpkit.ActivityMeta(request),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -128,19 +128,20 @@ func (h *Handler) claimTaskReward(writer http.ResponseWriter, request *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body taskClaimRequestBody
|
var body taskClaimRequestBody
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
taskID := body.normalizedTaskID()
|
taskID := body.normalizedTaskID()
|
||||||
taskType := body.normalizedTaskType()
|
taskType := body.normalizedTaskType()
|
||||||
taskDay := body.normalizedTaskDay()
|
taskDay := body.normalizedTaskDay()
|
||||||
commandID := normalizeCommandID(body.normalizedCommandID())
|
// command_id 是客户端动作的幂等锚点,gateway 只裁剪空白,不替客户端补值。
|
||||||
|
commandID := strings.TrimSpace(body.normalizedCommandID())
|
||||||
if commandID == "" || taskID == "" || taskType == "" || taskDay == "" {
|
if commandID == "" || taskID == "" || taskType == "" || taskDay == "" {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.taskClient.ClaimTaskReward(request.Context(), &activityv1.ClaimTaskRewardRequest{
|
resp, err := h.taskClient.ClaimTaskReward(request.Context(), &activityv1.ClaimTaskRewardRequest{
|
||||||
Meta: activityMeta(request),
|
Meta: httpkit.ActivityMeta(request),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
TaskId: taskID,
|
TaskId: taskID,
|
||||||
TaskType: taskType,
|
TaskType: taskType,
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package http
|
package appapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -12,13 +11,6 @@ import (
|
|||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// appConfigReader 是 gateway 对后台 App 配置的只读依赖。
|
|
||||||
type appConfigReader interface {
|
|
||||||
ListH5Links(ctx context.Context) ([]appconfig.H5Link, error)
|
|
||||||
ListBanners(ctx context.Context, query appconfig.BannerQuery) ([]appconfig.Banner, error)
|
|
||||||
LatestVersion(ctx context.Context, query appconfig.VersionQuery) (appconfig.Version, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type appBootstrapBottomTab struct {
|
type appBootstrapBottomTab struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@ -111,9 +103,9 @@ func (h *Handler) listAppBanners(writer http.ResponseWriter, request *http.Reque
|
|||||||
|
|
||||||
items, err := h.appConfigReader.ListBanners(request.Context(), appconfig.BannerQuery{
|
items, err := h.appConfigReader.ListBanners(request.Context(), appconfig.BannerQuery{
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
Platform: firstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"),
|
Platform: httpkit.FirstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"),
|
||||||
RegionID: optionalInt64Query(request, "region_id"),
|
RegionID: optionalInt64Query(request, "region_id"),
|
||||||
Country: firstQueryOrHeader(request, "country", "X-Country-Code", "X-App-Country"),
|
Country: httpkit.FirstQueryOrHeader(request, "country", "X-Country-Code", "X-App-Country"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
@ -129,7 +121,7 @@ func (h *Handler) getAppVersion(writer http.ResponseWriter, request *http.Reques
|
|||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
platform := normalizeAppPlatform(firstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"))
|
platform := normalizeAppPlatform(httpkit.FirstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"))
|
||||||
if platform != "android" && platform != "ios" {
|
if platform != "android" && platform != "ios" {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "platform is invalid")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "platform is invalid")
|
||||||
return
|
return
|
||||||
@ -160,15 +152,8 @@ func (h *Handler) getAppVersion(writer http.ResponseWriter, request *http.Reques
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstQueryOrHeader(request *http.Request, queryKey string, headerNames ...string) string {
|
|
||||||
if value := strings.TrimSpace(request.URL.Query().Get(queryKey)); value != "" {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return firstHeader(request, headerNames...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func optionalInt64QueryOrHeader(request *http.Request, queryKey string, headerNames ...string) int64 {
|
func optionalInt64QueryOrHeader(request *http.Request, queryKey string, headerNames ...string) int64 {
|
||||||
value := firstQueryOrHeader(request, queryKey, headerNames...)
|
value := httpkit.FirstQueryOrHeader(request, queryKey, headerNames...)
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package appapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
@ -37,14 +37,14 @@ func (h *Handler) bindPushToken(writer http.ResponseWriter, request *http.Reques
|
|||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
}
|
}
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
body.DeviceID = pushDeviceID(request, body.DeviceID)
|
body.DeviceID = pushDeviceID(request, body.DeviceID)
|
||||||
body.Platform = pushPlatform(request, body.Platform)
|
body.Platform = pushPlatform(request, body.Platform)
|
||||||
|
|
||||||
resp, err := h.userDeviceClient.BindPushToken(request.Context(), &userv1.BindPushTokenRequest{
|
resp, err := h.userDeviceClient.BindPushToken(request.Context(), &userv1.BindPushTokenRequest{
|
||||||
Meta: authRequestMeta(request, body.DeviceID),
|
Meta: httpkit.UserMeta(request, body.DeviceID),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
DeviceId: body.DeviceID,
|
DeviceId: body.DeviceID,
|
||||||
PushToken: strings.TrimSpace(body.PushToken),
|
PushToken: strings.TrimSpace(body.PushToken),
|
||||||
@ -71,13 +71,13 @@ func (h *Handler) deletePushToken(writer http.ResponseWriter, request *http.Requ
|
|||||||
DeviceID string `json:"device_id"`
|
DeviceID string `json:"device_id"`
|
||||||
PushToken string `json:"push_token"`
|
PushToken string `json:"push_token"`
|
||||||
}
|
}
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
body.DeviceID = pushDeviceID(request, body.DeviceID)
|
body.DeviceID = pushDeviceID(request, body.DeviceID)
|
||||||
|
|
||||||
resp, err := h.userDeviceClient.DeletePushToken(request.Context(), &userv1.DeletePushTokenRequest{
|
resp, err := h.userDeviceClient.DeletePushToken(request.Context(), &userv1.DeletePushTokenRequest{
|
||||||
Meta: authRequestMeta(request, body.DeviceID),
|
Meta: httpkit.UserMeta(request, body.DeviceID),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
DeviceId: body.DeviceID,
|
DeviceId: body.DeviceID,
|
||||||
PushToken: strings.TrimSpace(body.PushToken),
|
PushToken: strings.TrimSpace(body.PushToken),
|
||||||
@ -120,12 +120,12 @@ func pushDeviceID(request *http.Request, bodyValue string) string {
|
|||||||
if value := strings.TrimSpace(bodyValue); value != "" {
|
if value := strings.TrimSpace(bodyValue); value != "" {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
return firstHeader(request, "X-Device-ID", "X-HY-Device-ID")
|
return httpkit.FirstHeader(request, "X-Device-ID", "X-HY-Device-ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
func pushPlatform(request *http.Request, bodyValue string) string {
|
func pushPlatform(request *http.Request, bodyValue string) string {
|
||||||
if value := strings.TrimSpace(bodyValue); value != "" {
|
if value := strings.TrimSpace(bodyValue); value != "" {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
return firstHeader(request, "X-App-Platform", "X-Platform")
|
return httpkit.FirstHeader(request, "X-App-Platform", "X-Platform")
|
||||||
}
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package appapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"hyapp/services/gateway-service/internal/appconfig"
|
||||||
|
"hyapp/services/gateway-service/internal/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigReader 是 gateway 对后台 App 配置的只读依赖。
|
||||||
|
type ConfigReader interface {
|
||||||
|
ListH5Links(ctx context.Context) ([]appconfig.H5Link, error)
|
||||||
|
ListBanners(ctx context.Context, query appconfig.BannerQuery) ([]appconfig.Banner, error)
|
||||||
|
LatestVersion(ctx context.Context, query appconfig.VersionQuery) (appconfig.Version, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectUploader 是 gateway 上传接口依赖的对象存储最小能力。
|
||||||
|
type ObjectUploader interface {
|
||||||
|
PutObject(ctx context.Context, key string, reader io.Reader, sizeBytes int64, contentType string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler owns app platform endpoints such as bootstrap, config, upload and device token binding.
|
||||||
|
// It does not own user identity or resource semantics; those remain in their owner services/packages.
|
||||||
|
type Handler struct {
|
||||||
|
appConfigReader ConfigReader
|
||||||
|
userDeviceClient client.UserDeviceClient
|
||||||
|
objectUploader ObjectUploader
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
AppConfigReader ConfigReader
|
||||||
|
UserDeviceClient client.UserDeviceClient
|
||||||
|
ObjectUploader ObjectUploader
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
appConfigReader: config.AppConfigReader,
|
||||||
|
userDeviceClient: config.UserDeviceClient,
|
||||||
|
objectUploader: config.ObjectUploader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetAppBootstrap(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.getAppBootstrap(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListH5Links(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.listH5Links(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListAppBanners(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.listAppBanners(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetAppVersion(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.getAppVersion(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UploadFile(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.uploadFile(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UploadAvatar(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.uploadAvatar(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) HandlePushToken(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.handlePushToken(writer, request)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package appapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -106,7 +106,7 @@ func (h *Handler) uploadObject(writer http.ResponseWriter, request *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
objectKey := buildUploadObjectKey(policy, auth.UserIDFromContext(request.Context()), header.Filename, contentType, timeNow())
|
objectKey := buildUploadObjectKey(policy, auth.UserIDFromContext(request.Context()), header.Filename, contentType, time.Now().UTC())
|
||||||
objectURL, err := h.objectUploader.PutObject(request.Context(), objectKey, io.MultiReader(bytes.NewReader(head), file), header.Size, contentType)
|
objectURL, err := h.objectUploader.PutObject(request.Context(), objectKey, io.MultiReader(bytes.NewReader(head), file), header.Size, contentType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
@ -276,7 +276,7 @@ func authData(token *userv1.AuthToken, isNewUser *bool) authTokenData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return authTokenData{
|
return authTokenData{
|
||||||
UserID: userIDString(token.GetUserId()),
|
UserID: httpkit.UserIDString(token.GetUserId()),
|
||||||
AppCode: token.GetAppCode(),
|
AppCode: token.GetAppCode(),
|
||||||
DisplayUserID: token.GetDisplayUserId(),
|
DisplayUserID: token.GetDisplayUserId(),
|
||||||
DefaultDisplayUserID: token.GetDefaultDisplayUserId(),
|
DefaultDisplayUserID: token.GetDefaultDisplayUserId(),
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
package callbackapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/services/gateway-service/internal/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TencentIMConfig 是腾讯云 IM 服务端回调校验需要的最小配置。
|
||||||
|
type TencentIMConfig struct {
|
||||||
|
SDKAppID int64
|
||||||
|
AdminIdentifier string
|
||||||
|
CallbackAuthToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TencentRTCConfig 是腾讯云 RTC 服务端回调校验需要的最小配置。
|
||||||
|
type TencentRTCConfig struct {
|
||||||
|
SDKAppID int64
|
||||||
|
CallbackSignKey string
|
||||||
|
UserSigTTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler owns external Tencent callback endpoints.
|
||||||
|
// Tencent callback responses intentionally bypass gateway envelope because Tencent only accepts provider-native shapes.
|
||||||
|
type Handler struct {
|
||||||
|
roomClient client.RoomClient
|
||||||
|
roomGuardClient client.RoomGuardClient
|
||||||
|
userProfileClient client.UserProfileClient
|
||||||
|
tencentIM TencentIMConfig
|
||||||
|
tencentRTC TencentRTCConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
RoomClient client.RoomClient
|
||||||
|
RoomGuardClient client.RoomGuardClient
|
||||||
|
UserProfileClient client.UserProfileClient
|
||||||
|
TencentIM TencentIMConfig
|
||||||
|
TencentRTC TencentRTCConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
roomClient: config.RoomClient,
|
||||||
|
roomGuardClient: config.RoomGuardClient,
|
||||||
|
userProfileClient: config.UserProfileClient,
|
||||||
|
tencentIM: config.TencentIM,
|
||||||
|
tencentRTC: config.TencentRTC,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) HandleTencentIMCallback(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.handleTencentIMCallback(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) HandleTencentRTCCallback(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.handleTencentRTCCallback(writer, request)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package callbackapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package callbackapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package gameapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@ -21,7 +21,7 @@ func (h *Handler) listGames(writer http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
userID := auth.UserIDFromContext(request.Context())
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -35,9 +35,9 @@ func (h *Handler) listGames(writer http.ResponseWriter, request *http.Request) {
|
|||||||
RoomId: strings.TrimSpace(request.URL.Query().Get("room_id")),
|
RoomId: strings.TrimSpace(request.URL.Query().Get("room_id")),
|
||||||
RegionId: userResp.GetUser().GetRegionId(),
|
RegionId: userResp.GetUser().GetRegionId(),
|
||||||
Language: requestLanguage(request),
|
Language: requestLanguage(request),
|
||||||
ClientPlatform: firstHeader(request, "X-App-Platform", "X-Platform"),
|
ClientPlatform: httpkit.FirstHeader(request, "X-App-Platform", "X-Platform"),
|
||||||
})
|
})
|
||||||
write(writer, request, gameListDataFromProto(resp), err)
|
httpkit.Write(writer, request, gameListDataFromProto(resp), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) launchGame(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) launchGame(writer http.ResponseWriter, request *http.Request) {
|
||||||
@ -54,12 +54,12 @@ func (h *Handler) launchGame(writer http.ResponseWriter, request *http.Request)
|
|||||||
Scene string `json:"scene"`
|
Scene string `json:"scene"`
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
}
|
}
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userID := auth.UserIDFromContext(request.Context())
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -74,7 +74,7 @@ func (h *Handler) launchGame(writer http.ResponseWriter, request *http.Request)
|
|||||||
Scene: body.Scene,
|
Scene: body.Scene,
|
||||||
RoomId: body.RoomID,
|
RoomId: body.RoomID,
|
||||||
})
|
})
|
||||||
write(writer, request, gameLaunchDataFromProto(resp), err)
|
httpkit.Write(writer, request, gameLaunchDataFromProto(resp), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleGameCallback(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) handleGameCallback(writer http.ResponseWriter, request *http.Request) {
|
||||||
@ -122,7 +122,7 @@ func gameMeta(request *http.Request) *gamev1.RequestMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func requestLanguage(request *http.Request) string {
|
func requestLanguage(request *http.Request) string {
|
||||||
value := firstHeader(request, "X-App-Language", "X-Language")
|
value := httpkit.FirstHeader(request, "X-App-Language", "X-Language")
|
||||||
if value == "" {
|
if value == "" {
|
||||||
value = request.Header.Get("Accept-Language")
|
value = request.Header.Get("Accept-Language")
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package gameapi
|
||||||
|
|
||||||
import gamev1 "hyapp.local/api/proto/game/v1"
|
import gamev1 "hyapp.local/api/proto/game/v1"
|
||||||
|
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package gameapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"hyapp/services/gateway-service/internal/client"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler owns game-facing HTTP endpoints.
|
||||||
|
// gateway only translates app/user context into game-service RPCs; game session state stays in game-service.
|
||||||
|
type Handler struct {
|
||||||
|
gameClient client.GameClient
|
||||||
|
userProfileClient client.UserProfileClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
GameClient client.GameClient
|
||||||
|
UserProfileClient client.UserProfileClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
gameClient: config.GameClient,
|
||||||
|
userProfileClient: config.UserProfileClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Handlers() httproutes.GameHandlers {
|
||||||
|
return httproutes.GameHandlers{
|
||||||
|
ListGames: h.listGames,
|
||||||
|
LaunchGame: h.launchGame,
|
||||||
|
HandleCallback: h.handleGameCallback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListGames(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.listGames(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) LaunchGame(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.launchGame(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) HandleCallback(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.handleGameCallback(writer, request)
|
||||||
|
}
|
||||||
@ -9,13 +9,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/pkg/idgen"
|
|
||||||
"hyapp/pkg/imgroup"
|
"hyapp/pkg/imgroup"
|
||||||
"hyapp/pkg/tencentim"
|
"hyapp/pkg/tencentim"
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
"hyapp/services/gateway-service/internal/client"
|
"hyapp/services/gateway-service/internal/client"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/appapi"
|
||||||
|
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
|
||||||
userv1 "hyapp.local/api/proto/user/v1"
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,7 +38,7 @@ type Handler struct {
|
|||||||
registrationReward client.RegistrationRewardClient
|
registrationReward client.RegistrationRewardClient
|
||||||
broadcastClient client.BroadcastClient
|
broadcastClient client.BroadcastClient
|
||||||
gameClient client.GameClient
|
gameClient client.GameClient
|
||||||
appConfigReader appConfigReader
|
appConfigReader appapi.ConfigReader
|
||||||
tencentIM TencentIMConfig
|
tencentIM TencentIMConfig
|
||||||
tencentRTC TencentRTCConfig
|
tencentRTC TencentRTCConfig
|
||||||
objectUploader ObjectUploader
|
objectUploader ObjectUploader
|
||||||
@ -190,25 +189,10 @@ func (h *Handler) SetGameClient(gameClient client.GameClient) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetAppConfigReader 注入后台 App 配置读取依赖。
|
// SetAppConfigReader 注入后台 App 配置读取依赖。
|
||||||
func (h *Handler) SetAppConfigReader(reader appConfigReader) {
|
func (h *Handler) SetAppConfigReader(reader appapi.ConfigReader) {
|
||||||
h.appConfigReader = reader
|
h.appConfigReader = reader
|
||||||
}
|
}
|
||||||
|
|
||||||
// meta 把外部 HTTP 请求上下文转换成 room-service 命令元信息。
|
|
||||||
// request_id 只做链路追踪;command_id 才是 room-service command log 使用的幂等键。
|
|
||||||
func meta(request *http.Request, roomID string, commandID string) *roomv1.RequestMeta {
|
|
||||||
return &roomv1.RequestMeta{
|
|
||||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
|
||||||
CommandId: normalizeCommandID(commandID),
|
|
||||||
ActorUserId: auth.UserIDFromContext(request.Context()),
|
|
||||||
RoomId: roomID,
|
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
|
||||||
GatewayNodeId: "gateway-local",
|
|
||||||
SessionId: idgen.New("sess"),
|
|
||||||
SentAtMs: time.Now().UnixMilli(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeCommandID(commandID string) string {
|
func normalizeCommandID(commandID string) string {
|
||||||
// command_id 是客户端按用户动作生成的写命令幂等键;gateway 只裁剪空白,不自动补值。
|
// command_id 是客户端按用户动作生成的写命令幂等键;gateway 只裁剪空白,不自动补值。
|
||||||
return strings.TrimSpace(commandID)
|
return strings.TrimSpace(commandID)
|
||||||
|
|||||||
@ -4,14 +4,22 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/pkg/idgen"
|
"hyapp/pkg/idgen"
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
|
|
||||||
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type requestIDContextKey struct{}
|
type requestIDContextKey struct{}
|
||||||
@ -67,6 +75,17 @@ func DecodeJSON(reader io.Reader, out any) error {
|
|||||||
return json.NewDecoder(reader).Decode(out)
|
return json.NewDecoder(reader).Decode(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decode 统一处理 HTTP JSON 入参解析失败分支。
|
||||||
|
// gateway 在这里拒绝非法 JSON,不把脏请求继续透传到内部 gRPC。
|
||||||
|
func Decode(writer http.ResponseWriter, request *http.Request, out any) bool {
|
||||||
|
if err := DecodeJSON(request.Body, out); err != nil {
|
||||||
|
WriteError(writer, request, http.StatusBadRequest, CodeInvalidJSON, "invalid request body")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// WriteOK 写成功 envelope。
|
// WriteOK 写成功 envelope。
|
||||||
func WriteOK(writer http.ResponseWriter, request *http.Request, data any) {
|
func WriteOK(writer http.ResponseWriter, request *http.Request, data any) {
|
||||||
WriteEnvelope(writer, http.StatusOK, ResponseEnvelope{
|
WriteEnvelope(writer, http.StatusOK, ResponseEnvelope{
|
||||||
@ -102,6 +121,16 @@ func WriteRPCError(writer http.ResponseWriter, request *http.Request, err error)
|
|||||||
WriteError(writer, request, statusCode, code, message)
|
WriteError(writer, request, statusCode, code, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write 统一把内部 gRPC 调用结果转换为 HTTP JSON 响应。
|
||||||
|
func Write(writer http.ResponseWriter, request *http.Request, body any, err error) {
|
||||||
|
if err != nil {
|
||||||
|
WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteOK(writer, request, body)
|
||||||
|
}
|
||||||
|
|
||||||
func MapReasonToHTTP(reason xerr.Code) (int, string, string) {
|
func MapReasonToHTTP(reason xerr.Code) (int, string, string) {
|
||||||
spec := xerr.SpecOf(reason)
|
spec := xerr.SpecOf(reason)
|
||||||
return spec.HTTPStatus, spec.PublicCode, spec.PublicMessage
|
return spec.HTTPStatus, spec.PublicCode, spec.PublicMessage
|
||||||
@ -129,6 +158,41 @@ func PositiveInt64PathValue(request *http.Request, name string) (int64, bool) {
|
|||||||
return value, err == nil && value > 0
|
return value, err == nil && value > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UserIDString(userID int64) string {
|
||||||
|
if userID > 0 {
|
||||||
|
return strconv.FormatInt(userID, 10)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func PositiveInt32Query(request *http.Request, name string, fallback int32) (int32, bool) {
|
||||||
|
raw := strings.TrimSpace(request.URL.Query().Get(name))
|
||||||
|
if raw == "" {
|
||||||
|
return fallback, true
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseInt(raw, 10, 32)
|
||||||
|
if err != nil || value <= 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return int32(value), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func FirstQueryOrHeader(request *http.Request, queryKey string, headerNames ...string) string {
|
||||||
|
if value := strings.TrimSpace(request.URL.Query().Get(queryKey)); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return FirstHeader(request, headerNames...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FirstHeader(request *http.Request, headerNames ...string) string {
|
||||||
|
for _, name := range headerNames {
|
||||||
|
if value := strings.TrimSpace(request.Header.Get(name)); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// RequireMethod 在业务 handler 之前固定 HTTP method,避免 path-only mux 让错误 method 进入命令链路。
|
// RequireMethod 在业务 handler 之前固定 HTTP method,避免 path-only mux 让错误 method 进入命令链路。
|
||||||
func RequireMethod(method string, next http.HandlerFunc) http.HandlerFunc {
|
func RequireMethod(method string, next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
@ -153,3 +217,121 @@ func RequireCompletedProfile(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
next.ServeHTTP(writer, request)
|
next.ServeHTTP(writer, request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RoomMeta 把外部 HTTP 请求上下文转换成 room-service 命令元信息。
|
||||||
|
// request_id 只做链路追踪;command_id 必须来自客户端用户动作,gateway 只裁剪空白。
|
||||||
|
func RoomMeta(request *http.Request, roomID string, commandID string) *roomv1.RequestMeta {
|
||||||
|
return &roomv1.RequestMeta{
|
||||||
|
RequestId: RequestIDFromContext(request.Context()),
|
||||||
|
CommandId: strings.TrimSpace(commandID),
|
||||||
|
ActorUserId: auth.UserIDFromContext(request.Context()),
|
||||||
|
RoomId: roomID,
|
||||||
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
|
GatewayNodeId: "gateway-local",
|
||||||
|
SessionId: idgen.New("sess"),
|
||||||
|
SentAtMs: time.Now().UnixMilli(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserMeta 生成 user-service RPC 的统一追踪和审计元信息。
|
||||||
|
func UserMeta(request *http.Request, deviceID string) *userv1.RequestMeta {
|
||||||
|
return UserMetaWithLoginContext(request, deviceID, "", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserMetaWithLoginContext(request *http.Request, deviceID string, platform string, language string, timezone string) *userv1.RequestMeta {
|
||||||
|
return &userv1.RequestMeta{
|
||||||
|
RequestId: RequestIDFromContext(request.Context()),
|
||||||
|
Caller: "gateway-service",
|
||||||
|
GatewayNodeId: "gateway-local",
|
||||||
|
SentAtMs: time.Now().UnixMilli(),
|
||||||
|
DeviceId: deviceID,
|
||||||
|
ClientIp: ClientIP(request),
|
||||||
|
UserAgent: request.UserAgent(),
|
||||||
|
CountryByIp: CountryByIP(request),
|
||||||
|
SessionId: auth.SessionIDFromContext(request.Context()),
|
||||||
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
|
Platform: strings.ToLower(strings.TrimSpace(platform)),
|
||||||
|
Language: strings.TrimSpace(language),
|
||||||
|
Timezone: strings.TrimSpace(timezone),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivityMeta 生成 activity-service RPC 的统一追踪元信息。
|
||||||
|
// 业务 actor 仍由各请求显式传 user_id,meta 只承载链路和租户上下文。
|
||||||
|
func ActivityMeta(request *http.Request) *activityv1.RequestMeta {
|
||||||
|
return &activityv1.RequestMeta{
|
||||||
|
RequestId: RequestIDFromContext(request.Context()),
|
||||||
|
Caller: "gateway-service",
|
||||||
|
GatewayNodeId: "gateway-local",
|
||||||
|
SentAtMs: time.Now().UnixMilli(),
|
||||||
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientIP 提取 gateway 看到的客户端地址。
|
||||||
|
func ClientIP(request *http.Request) string {
|
||||||
|
for _, header := range []string{"X-Trusted-Client-IP", "X-Real-IP"} {
|
||||||
|
if ip := NormalizeIP(request.Header.Get(header)); ip != "" {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
forwarded := strings.TrimSpace(request.Header.Get("X-Forwarded-For"))
|
||||||
|
if forwarded != "" {
|
||||||
|
for part := range strings.SplitSeq(forwarded, ",") {
|
||||||
|
ip := NormalizeIP(part)
|
||||||
|
if ip == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if addr, err := netip.ParseAddr(ip); err == nil && isPublicClientAddr(addr) {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(request.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return NormalizeIP(request.RemoteAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeIP(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if host, _, err := net.SplitHostPort(value); err == nil {
|
||||||
|
value = host
|
||||||
|
}
|
||||||
|
if addr, err := netip.ParseAddr(value); err == nil {
|
||||||
|
return addr.String()
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPublicClientAddr(addr netip.Addr) bool {
|
||||||
|
return addr.IsValid() &&
|
||||||
|
addr.IsGlobalUnicast() &&
|
||||||
|
!addr.IsPrivate() &&
|
||||||
|
!addr.IsLoopback() &&
|
||||||
|
!addr.IsLinkLocalUnicast() &&
|
||||||
|
!addr.IsLinkLocalMulticast() &&
|
||||||
|
!addr.IsMulticast() &&
|
||||||
|
!addr.IsUnspecified()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CountryByIP(request *http.Request) string {
|
||||||
|
// country_by_ip 不接受 JSON body;只采信 gateway/边缘层写入的地域头。
|
||||||
|
for _, header := range []string{"CF-IPCountry", "CloudFront-Viewer-Country", "X-Vercel-IP-Country", "X-AppEngine-Country", "X-Country-Code"} {
|
||||||
|
country := strings.ToUpper(strings.TrimSpace(request.Header.Get(header)))
|
||||||
|
if country == "" || country == "XX" || country == "UNKNOWN" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(country) >= 2 && len(country) <= 3 {
|
||||||
|
return country
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
@ -91,6 +91,7 @@ type RoomHandlers struct {
|
|||||||
GetRoomDetail http.HandlerFunc
|
GetRoomDetail http.HandlerFunc
|
||||||
ListRoomOnlineUsers http.HandlerFunc
|
ListRoomOnlineUsers http.HandlerFunc
|
||||||
GetRoomGiftPanel http.HandlerFunc
|
GetRoomGiftPanel http.HandlerFunc
|
||||||
|
FollowRoom http.HandlerFunc
|
||||||
CreateRoom http.HandlerFunc
|
CreateRoom http.HandlerFunc
|
||||||
UpdateRoomProfile http.HandlerFunc
|
UpdateRoomProfile http.HandlerFunc
|
||||||
JoinRoom http.HandlerFunc
|
JoinRoom http.HandlerFunc
|
||||||
@ -104,6 +105,7 @@ type RoomHandlers struct {
|
|||||||
SetMicMute http.HandlerFunc
|
SetMicMute http.HandlerFunc
|
||||||
SetMicSeatLock http.HandlerFunc
|
SetMicSeatLock http.HandlerFunc
|
||||||
SetChatEnabled http.HandlerFunc
|
SetChatEnabled http.HandlerFunc
|
||||||
|
SetRoomPassword http.HandlerFunc
|
||||||
SetRoomAdmin http.HandlerFunc
|
SetRoomAdmin http.HandlerFunc
|
||||||
MuteUser http.HandlerFunc
|
MuteUser http.HandlerFunc
|
||||||
KickUser http.HandlerFunc
|
KickUser http.HandlerFunc
|
||||||
@ -263,6 +265,7 @@ func (r routes) registerRoomRoutes() {
|
|||||||
r.profile("/rooms/{room_id}/detail", http.MethodGet, h.GetRoomDetail)
|
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}/online-users", http.MethodGet, h.ListRoomOnlineUsers)
|
||||||
r.profile("/rooms/{room_id}/gift-panel", http.MethodGet, h.GetRoomGiftPanel)
|
r.profile("/rooms/{room_id}/gift-panel", http.MethodGet, h.GetRoomGiftPanel)
|
||||||
|
r.profile("/rooms/{room_id}/follow", "", h.FollowRoom)
|
||||||
r.profile("/rooms/create", http.MethodPost, h.CreateRoom)
|
r.profile("/rooms/create", http.MethodPost, h.CreateRoom)
|
||||||
r.profile("/rooms/profile/update", http.MethodPost, h.UpdateRoomProfile)
|
r.profile("/rooms/profile/update", http.MethodPost, h.UpdateRoomProfile)
|
||||||
r.profile("/rooms/join", http.MethodPost, h.JoinRoom)
|
r.profile("/rooms/join", http.MethodPost, h.JoinRoom)
|
||||||
@ -276,6 +279,7 @@ func (r routes) registerRoomRoutes() {
|
|||||||
r.profile("/rooms/mic/mute", http.MethodPost, h.SetMicMute)
|
r.profile("/rooms/mic/mute", http.MethodPost, h.SetMicMute)
|
||||||
r.profile("/rooms/mic/lock", http.MethodPost, h.SetMicSeatLock)
|
r.profile("/rooms/mic/lock", http.MethodPost, h.SetMicSeatLock)
|
||||||
r.profile("/rooms/chat/enabled", http.MethodPost, h.SetChatEnabled)
|
r.profile("/rooms/chat/enabled", http.MethodPost, h.SetChatEnabled)
|
||||||
|
r.profile("/rooms/password", http.MethodPost, h.SetRoomPassword)
|
||||||
r.profile("/rooms/admin/set", http.MethodPost, h.SetRoomAdmin)
|
r.profile("/rooms/admin/set", http.MethodPost, h.SetRoomAdmin)
|
||||||
r.profile("/rooms/user/mute", http.MethodPost, h.MuteUser)
|
r.profile("/rooms/user/mute", http.MethodPost, h.MuteUser)
|
||||||
r.profile("/rooms/user/kick", http.MethodPost, h.KickUser)
|
r.profile("/rooms/user/kick", http.MethodPost, h.KickUser)
|
||||||
|
|||||||
@ -50,7 +50,7 @@ func TestListManagerGrantResourcesChecksCapabilityAndFiltersWalletRequest(t *tes
|
|||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
if hostClient.lastCapability == nil || hostClient.lastCapability.GetActorUserId() != 42 || hostClient.lastCapability.GetCapability() != managerResourceGrantCapability {
|
if hostClient.lastCapability == nil || hostClient.lastCapability.GetActorUserId() != 42 || hostClient.lastCapability.GetCapability() != "manager_resource_grant" {
|
||||||
t.Fatalf("manager capability request mismatch: %+v", hostClient.lastCapability)
|
t.Fatalf("manager capability request mismatch: %+v", hostClient.lastCapability)
|
||||||
}
|
}
|
||||||
if walletClient.lastListResources == nil {
|
if walletClient.lastListResources == nil {
|
||||||
@ -65,10 +65,13 @@ func TestListManagerGrantResourcesChecksCapabilityAndFiltersWalletRequest(t *tes
|
|||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
RequestID string `json:"request_id"`
|
RequestID string `json:"request_id"`
|
||||||
Data struct {
|
Data struct {
|
||||||
Items []managerGrantResourceData `json:"items"`
|
Items []struct {
|
||||||
Total int64 `json:"total"`
|
ResourceID string `json:"resource_id"`
|
||||||
Page int32 `json:"page"`
|
Name string `json:"name"`
|
||||||
PageSize int32 `json:"page_size"`
|
} `json:"items"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Page int32 `json:"page"`
|
||||||
|
PageSize int32 `json:"page_size"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
||||||
@ -124,8 +127,11 @@ func TestBusinessUserLookupForwardsSceneAndActor(t *testing.T) {
|
|||||||
var envelope struct {
|
var envelope struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Data struct {
|
Data struct {
|
||||||
Items []businessUserLookupData `json:"items"`
|
Items []struct {
|
||||||
Total int `json:"total"`
|
UserID string `json:"user_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"items"`
|
||||||
|
Total int `json:"total"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
package managerapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/client"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler owns manager-center HTTP endpoints that are still part of the app gateway.
|
||||||
|
// Capability checks stay in user-service, and resource grants stay in wallet-service.
|
||||||
|
type Handler struct {
|
||||||
|
walletClient client.WalletClient
|
||||||
|
userHostClient client.UserHostClient
|
||||||
|
userProfileClient client.UserProfileClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
WalletClient client.WalletClient
|
||||||
|
UserHostClient client.UserHostClient
|
||||||
|
UserProfileClient client.UserProfileClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
walletClient: config.WalletClient,
|
||||||
|
userHostClient: config.UserHostClient,
|
||||||
|
userProfileClient: config.UserProfileClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ManagerHandlers() httproutes.ManagerHandlers {
|
||||||
|
return httproutes.ManagerHandlers{
|
||||||
|
ListManagerGrantResources: h.listManagerGrantResources,
|
||||||
|
GrantManagerResource: h.grantManagerResource,
|
||||||
|
LookupBusinessUsers: h.lookupBusinessUsers,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package managerapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -88,7 +88,7 @@ func (h *Handler) lookupBusinessUsers(writer http.ResponseWriter, request *http.
|
|||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
|
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||||
if !ok {
|
if !ok {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
@ -97,7 +97,7 @@ func (h *Handler) lookupBusinessUsers(writer http.ResponseWriter, request *http.
|
|||||||
pageSize = 20
|
pageSize = 20
|
||||||
}
|
}
|
||||||
resp, err := h.userProfileClient.BusinessUserLookup(request.Context(), &userv1.BusinessUserLookupRequest{
|
resp, err := h.userProfileClient.BusinessUserLookup(request.Context(), &userv1.BusinessUserLookupRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
ActorUserId: auth.UserIDFromContext(request.Context()),
|
ActorUserId: auth.UserIDFromContext(request.Context()),
|
||||||
Scene: strings.TrimSpace(request.URL.Query().Get("scene")),
|
Scene: strings.TrimSpace(request.URL.Query().Get("scene")),
|
||||||
Keyword: strings.TrimSpace(request.URL.Query().Get("keyword")),
|
Keyword: strings.TrimSpace(request.URL.Query().Get("keyword")),
|
||||||
@ -131,7 +131,7 @@ func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http
|
|||||||
DurationMS *int64 `json:"duration_ms"`
|
DurationMS *int64 `json:"duration_ms"`
|
||||||
DurationMSAlt *int64 `json:"durationMs"`
|
DurationMSAlt *int64 `json:"durationMs"`
|
||||||
}
|
}
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if body.Quantity != nil || body.DurationMS != nil || body.DurationMSAlt != nil {
|
if body.Quantity != nil || body.DurationMS != nil || body.DurationMSAlt != nil {
|
||||||
@ -152,7 +152,7 @@ func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: targetUserID,
|
UserId: targetUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -201,7 +201,7 @@ func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http
|
|||||||
|
|
||||||
func (h *Handler) requireManagerResourceGrantCapability(writer http.ResponseWriter, request *http.Request) bool {
|
func (h *Handler) requireManagerResourceGrantCapability(writer http.ResponseWriter, request *http.Request) bool {
|
||||||
resp, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
|
resp, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
ActorUserId: auth.UserIDFromContext(request.Context()),
|
ActorUserId: auth.UserIDFromContext(request.Context()),
|
||||||
Capability: managerResourceGrantCapability,
|
Capability: managerResourceGrantCapability,
|
||||||
})
|
})
|
||||||
@ -221,7 +221,7 @@ func managerGrantResourceFromProto(resource *walletv1.Resource) managerGrantReso
|
|||||||
return managerGrantResourceData{}
|
return managerGrantResourceData{}
|
||||||
}
|
}
|
||||||
return managerGrantResourceData{
|
return managerGrantResourceData{
|
||||||
ResourceID: userIDString(resource.GetResourceId()),
|
ResourceID: httpkit.UserIDString(resource.GetResourceId()),
|
||||||
ResourceType: resource.GetResourceType(),
|
ResourceType: resource.GetResourceType(),
|
||||||
Name: resource.GetName(),
|
Name: resource.GetName(),
|
||||||
AssetURL: resource.GetAssetUrl(),
|
AssetURL: resource.GetAssetUrl(),
|
||||||
@ -237,7 +237,7 @@ func businessUserLookupFromProto(user *userv1.BusinessUserLookupItem) businessUs
|
|||||||
return businessUserLookupData{}
|
return businessUserLookupData{}
|
||||||
}
|
}
|
||||||
return businessUserLookupData{
|
return businessUserLookupData{
|
||||||
UserID: userIDString(user.GetUserId()),
|
UserID: httpkit.UserIDString(user.GetUserId()),
|
||||||
DisplayUserID: user.GetDisplayUserId(),
|
DisplayUserID: user.GetDisplayUserId(),
|
||||||
Username: user.GetUsername(),
|
Username: user.GetUsername(),
|
||||||
Avatar: user.GetAvatar(),
|
Avatar: user.GetAvatar(),
|
||||||
@ -254,25 +254,36 @@ func managerResourceGrantFromProto(grant *walletv1.ResourceGrant) managerResourc
|
|||||||
}
|
}
|
||||||
resourceID := grant.GetGrantSubjectId()
|
resourceID := grant.GetGrantSubjectId()
|
||||||
if len(grant.GetItems()) > 0 && grant.GetItems()[0].GetResourceId() > 0 {
|
if len(grant.GetItems()) > 0 && grant.GetItems()[0].GetResourceId() > 0 {
|
||||||
resourceID = userIDString(grant.GetItems()[0].GetResourceId())
|
resourceID = httpkit.UserIDString(grant.GetItems()[0].GetResourceId())
|
||||||
}
|
}
|
||||||
return managerResourceGrantData{
|
return managerResourceGrantData{
|
||||||
GrantID: grant.GetGrantId(),
|
GrantID: grant.GetGrantId(),
|
||||||
CommandID: grant.GetCommandId(),
|
CommandID: grant.GetCommandId(),
|
||||||
TargetUserID: userIDString(grant.GetTargetUserId()),
|
TargetUserID: httpkit.UserIDString(grant.GetTargetUserId()),
|
||||||
ResourceID: resourceID,
|
ResourceID: resourceID,
|
||||||
Status: grant.GetStatus(),
|
Status: grant.GetStatus(),
|
||||||
GrantSource: grant.GetGrantSource(),
|
GrantSource: grant.GetGrantSource(),
|
||||||
OperatorUserID: userIDString(grant.GetOperatorUserId()),
|
OperatorUserID: httpkit.UserIDString(grant.GetOperatorUserId()),
|
||||||
CreatedAtMS: grant.GetCreatedAtMs(),
|
CreatedAtMS: grant.GetCreatedAtMs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func managerPage(request *http.Request) (int32, int32, bool) {
|
func managerPage(request *http.Request) (int32, int32, bool) {
|
||||||
page, pageSize, ok := resourcePage(request)
|
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
|
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||||
|
if !ok {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
if raw := strings.TrimSpace(request.URL.Query().Get("pageSize")); raw != "" {
|
||||||
|
parsed, err := strconv.Atoi(raw)
|
||||||
|
if err != nil || parsed <= 0 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
pageSize = int32(parsed)
|
||||||
|
}
|
||||||
if pageSize > 20 {
|
if pageSize > 20 {
|
||||||
pageSize = 20
|
pageSize = 20
|
||||||
}
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package messageapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"hyapp/services/gateway-service/internal/client"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler owns message inbox endpoints and safe profile batches used by Tencent IM conversation UI.
|
||||||
|
// Private chat delivery remains Tencent IM owned; these handlers only expose backend-owned system inbox facts.
|
||||||
|
type Handler struct {
|
||||||
|
messageClient client.MessageInboxClient
|
||||||
|
userProfileClient client.UserProfileClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
MessageClient client.MessageInboxClient
|
||||||
|
UserProfileClient client.UserProfileClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
messageClient: config.MessageClient,
|
||||||
|
userProfileClient: config.UserProfileClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) MessageHandlers() httproutes.MessageHandlers {
|
||||||
|
return httproutes.MessageHandlers{
|
||||||
|
ListMessageTabs: h.listMessageTabs,
|
||||||
|
MarkInboxSectionRead: h.markInboxSectionRead,
|
||||||
|
MarkInboxMessageReadPath: h.markInboxMessageReadByPath,
|
||||||
|
DeleteInboxMessagePath: h.deleteInboxMessageByPath,
|
||||||
|
ListInboxMessages: h.listInboxMessages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) BatchUserProfiles(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.batchUserProfiles(writer, request)
|
||||||
|
}
|
||||||
@ -1,16 +1,14 @@
|
|||||||
package http
|
package messageapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
userv1 "hyapp.local/api/proto/user/v1"
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
"hyapp/pkg/appcode"
|
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type messageTabSectionData struct {
|
type messageTabSectionData struct {
|
||||||
@ -51,7 +49,7 @@ func (h *Handler) listMessageTabs(writer http.ResponseWriter, request *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.messageClient.ListMessageTabs(request.Context(), &activityv1.ListMessageTabsRequest{
|
resp, err := h.messageClient.ListMessageTabs(request.Context(), &activityv1.ListMessageTabsRequest{
|
||||||
Meta: activityMeta(request),
|
Meta: httpkit.ActivityMeta(request),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -83,7 +81,7 @@ func (h *Handler) listInboxMessages(writer http.ResponseWriter, request *http.Re
|
|||||||
}
|
}
|
||||||
section := strings.TrimSpace(request.URL.Query().Get("section"))
|
section := strings.TrimSpace(request.URL.Query().Get("section"))
|
||||||
resp, err := h.messageClient.ListInboxMessages(request.Context(), &activityv1.ListInboxMessagesRequest{
|
resp, err := h.messageClient.ListInboxMessages(request.Context(), &activityv1.ListInboxMessagesRequest{
|
||||||
Meta: activityMeta(request),
|
Meta: httpkit.ActivityMeta(request),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
Section: section,
|
Section: section,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
@ -120,11 +118,11 @@ func (h *Handler) markInboxSectionRead(writer http.ResponseWriter, request *http
|
|||||||
var body struct {
|
var body struct {
|
||||||
Section string `json:"section"`
|
Section string `json:"section"`
|
||||||
}
|
}
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.messageClient.MarkInboxSectionRead(request.Context(), &activityv1.MarkInboxSectionReadRequest{
|
resp, err := h.messageClient.MarkInboxSectionRead(request.Context(), &activityv1.MarkInboxSectionReadRequest{
|
||||||
Meta: activityMeta(request),
|
Meta: httpkit.ActivityMeta(request),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
Section: body.Section,
|
Section: body.Section,
|
||||||
})
|
})
|
||||||
@ -173,7 +171,7 @@ func (h *Handler) markInboxMessageRead(writer http.ResponseWriter, request *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.messageClient.MarkInboxMessageRead(request.Context(), &activityv1.MarkInboxMessageReadRequest{
|
resp, err := h.messageClient.MarkInboxMessageRead(request.Context(), &activityv1.MarkInboxMessageReadRequest{
|
||||||
Meta: activityMeta(request),
|
Meta: httpkit.ActivityMeta(request),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
MessageId: messageID,
|
MessageId: messageID,
|
||||||
})
|
})
|
||||||
@ -186,7 +184,7 @@ func (h *Handler) markInboxMessageRead(writer http.ResponseWriter, request *http
|
|||||||
|
|
||||||
func (h *Handler) deleteInboxMessage(writer http.ResponseWriter, request *http.Request, messageID string) {
|
func (h *Handler) deleteInboxMessage(writer http.ResponseWriter, request *http.Request, messageID string) {
|
||||||
resp, err := h.messageClient.DeleteInboxMessage(request.Context(), &activityv1.DeleteInboxMessageRequest{
|
resp, err := h.messageClient.DeleteInboxMessage(request.Context(), &activityv1.DeleteInboxMessageRequest{
|
||||||
Meta: activityMeta(request),
|
Meta: httpkit.ActivityMeta(request),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
MessageId: messageID,
|
MessageId: messageID,
|
||||||
})
|
})
|
||||||
@ -209,7 +207,7 @@ func (h *Handler) batchUserProfiles(writer http.ResponseWriter, request *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserIds: userIDs,
|
UserIds: userIDs,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -234,16 +232,6 @@ func (h *Handler) batchUserProfiles(writer http.ResponseWriter, request *http.Re
|
|||||||
httpkit.WriteOK(writer, request, map[string]any{"profiles": items})
|
httpkit.WriteOK(writer, request, map[string]any{"profiles": items})
|
||||||
}
|
}
|
||||||
|
|
||||||
func activityMeta(request *http.Request) *activityv1.RequestMeta {
|
|
||||||
return &activityv1.RequestMeta{
|
|
||||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
|
||||||
Caller: "gateway-service",
|
|
||||||
GatewayNodeId: "gateway-local",
|
|
||||||
SentAtMs: time.Now().UnixMilli(),
|
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseMessagePageSize(raw string) (int32, bool) {
|
func parseMessagePageSize(raw string) (int32, bool) {
|
||||||
raw = strings.TrimSpace(raw)
|
raw = strings.TrimSpace(raw)
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package resourceapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"hyapp/services/gateway-service/internal/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler owns public resource catalog and current-user resource inventory endpoints.
|
||||||
|
// Wallet-service remains the owner for entitlement and grant semantics.
|
||||||
|
type Handler struct {
|
||||||
|
walletClient client.WalletClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
WalletClient client.WalletClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Handler {
|
||||||
|
return &Handler{walletClient: config.WalletClient}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListResources(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.listResources(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetResourceGroup(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.getResourceGroup(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListGifts(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.listGifts(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListMyResources(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.listMyResources(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) EquipMyResource(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.equipMyResource(writer, request)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package resourceapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -357,11 +357,11 @@ func userResourceFromProto(item *walletv1.UserResourceEntitlement) userResourceD
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resourcePage(request *http.Request) (int32, int32, bool) {
|
func resourcePage(request *http.Request) (int32, int32, bool) {
|
||||||
page, ok := parsePositiveInt32Query(request, "page", 1)
|
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
|
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
@ -375,18 +375,6 @@ func resourcePage(request *http.Request) (int32, int32, bool) {
|
|||||||
return page, pageSize, true
|
return page, pageSize, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePositiveInt32Query(request *http.Request, name string, fallback int32) (int32, bool) {
|
|
||||||
raw := strings.TrimSpace(request.URL.Query().Get(name))
|
|
||||||
if raw == "" {
|
|
||||||
return fallback, true
|
|
||||||
}
|
|
||||||
value, err := strconv.Atoi(raw)
|
|
||||||
if err != nil || value <= 0 {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
return int32(value), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func optionalNonNegativeInt64Query(request *http.Request, name string) (int64, bool, bool) {
|
func optionalNonNegativeInt64Query(request *http.Request, name string) (int64, bool, bool) {
|
||||||
raw := strings.TrimSpace(request.URL.Query().Get(name))
|
raw := strings.TrimSpace(request.URL.Query().Get(name))
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/roomapi"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -45,11 +46,14 @@ type fakeRoomClient struct {
|
|||||||
lastRTCEvent *roomv1.ApplyRTCEventRequest
|
lastRTCEvent *roomv1.ApplyRTCEventRequest
|
||||||
lastMicSeatLock *roomv1.SetMicSeatLockRequest
|
lastMicSeatLock *roomv1.SetMicSeatLockRequest
|
||||||
lastChatEnabled *roomv1.SetChatEnabledRequest
|
lastChatEnabled *roomv1.SetChatEnabledRequest
|
||||||
|
lastRoomPassword *roomv1.SetRoomPasswordRequest
|
||||||
lastSetAdmin *roomv1.SetRoomAdminRequest
|
lastSetAdmin *roomv1.SetRoomAdminRequest
|
||||||
lastMute *roomv1.MuteUserRequest
|
lastMute *roomv1.MuteUserRequest
|
||||||
lastKick *roomv1.KickUserRequest
|
lastKick *roomv1.KickUserRequest
|
||||||
lastUnban *roomv1.UnbanUserRequest
|
lastUnban *roomv1.UnbanUserRequest
|
||||||
lastGift *roomv1.SendGiftRequest
|
lastGift *roomv1.SendGiftRequest
|
||||||
|
lastFollowRoom *roomv1.FollowRoomRequest
|
||||||
|
lastUnfollowRoom *roomv1.UnfollowRoomRequest
|
||||||
createErr error
|
createErr error
|
||||||
updateProfileErr error
|
updateProfileErr error
|
||||||
joinErr error
|
joinErr error
|
||||||
@ -175,6 +179,24 @@ func (f *fakeRoomClient) SetChatEnabled(_ context.Context, req *roomv1.SetChatEn
|
|||||||
return &roomv1.SetChatEnabledResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 8}}, nil
|
return &roomv1.SetChatEnabledResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 8}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeRoomClient) SetRoomPassword(_ context.Context, req *roomv1.SetRoomPasswordRequest) (*roomv1.SetRoomPasswordResponse, error) {
|
||||||
|
f.lastRoomPassword = req
|
||||||
|
return &roomv1.SetRoomPasswordResponse{
|
||||||
|
Result: &roomv1.CommandResult{Applied: true, RoomVersion: 9, ServerTimeMs: 1_700_000_000_009},
|
||||||
|
Room: &roomv1.RoomSnapshot{
|
||||||
|
RoomId: req.GetMeta().GetRoomId(),
|
||||||
|
OwnerUserId: req.GetMeta().GetActorUserId(),
|
||||||
|
Mode: "voice",
|
||||||
|
Status: "active",
|
||||||
|
ChatEnabled: true,
|
||||||
|
Locked: req.GetLocked(),
|
||||||
|
MicSeats: []*roomv1.SeatState{{SeatNo: 1}, {SeatNo: 2}},
|
||||||
|
RoomExt: map[string]string{"title": "Room"},
|
||||||
|
Version: 9,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeRoomClient) SetRoomAdmin(_ context.Context, req *roomv1.SetRoomAdminRequest) (*roomv1.SetRoomAdminResponse, error) {
|
func (f *fakeRoomClient) SetRoomAdmin(_ context.Context, req *roomv1.SetRoomAdminRequest) (*roomv1.SetRoomAdminResponse, error) {
|
||||||
f.lastSetAdmin = req
|
f.lastSetAdmin = req
|
||||||
return &roomv1.SetRoomAdminResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 9}}, nil
|
return &roomv1.SetRoomAdminResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 9}}, nil
|
||||||
@ -200,6 +222,16 @@ func (f *fakeRoomClient) SendGift(_ context.Context, req *roomv1.SendGiftRequest
|
|||||||
return &roomv1.SendGiftResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 14}}, nil
|
return &roomv1.SendGiftResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 14}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeRoomClient) FollowRoom(_ context.Context, req *roomv1.FollowRoomRequest) (*roomv1.FollowRoomResponse, error) {
|
||||||
|
f.lastFollowRoom = req
|
||||||
|
return &roomv1.FollowRoomResponse{RoomId: req.GetRoomId(), Followed: true, FollowedAtMs: 1_700_000_000_123, ServerTimeMs: 1_700_000_000_123}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRoomClient) UnfollowRoom(_ context.Context, req *roomv1.UnfollowRoomRequest) (*roomv1.UnfollowRoomResponse, error) {
|
||||||
|
f.lastUnfollowRoom = req
|
||||||
|
return &roomv1.UnfollowRoomResponse{RoomId: req.GetRoomId(), Followed: false, ServerTimeMs: 1_700_000_000_124}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type fakeUserAuthClient struct {
|
type fakeUserAuthClient struct {
|
||||||
lastLoginThirdParty *userv1.LoginThirdPartyRequest
|
lastLoginThirdParty *userv1.LoginThirdPartyRequest
|
||||||
lastLoginPassword *userv1.LoginPasswordRequest
|
lastLoginPassword *userv1.LoginPasswordRequest
|
||||||
@ -1242,6 +1274,15 @@ func TestRoomCommandsPropagateClientCommandID(t *testing.T) {
|
|||||||
return client.lastChatEnabled.GetMeta()
|
return client.lastChatEnabled.GetMeta()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "room_password",
|
||||||
|
path: "/api/v1/rooms/password",
|
||||||
|
body: `{"room_id":"room-1","command_id":"cmd-room-password-1","locked":true,"password":"1234"}`,
|
||||||
|
commandID: "cmd-room-password-1",
|
||||||
|
extractMeta: func(client *fakeRoomClient) *roomv1.RequestMeta {
|
||||||
|
return client.lastRoomPassword.GetMeta()
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "set_admin",
|
name: "set_admin",
|
||||||
path: "/api/v1/rooms/admin/set",
|
path: "/api/v1/rooms/admin/set",
|
||||||
@ -1323,9 +1364,9 @@ func TestRoomCommandsPropagateClientCommandID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T) {
|
func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T) {
|
||||||
previousNow := timeNow
|
previousNow := roomapi.TimeNow
|
||||||
timeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
roomapi.TimeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
||||||
defer func() { timeNow = previousNow }()
|
defer func() { roomapi.TimeNow = previousNow }()
|
||||||
|
|
||||||
roomClient := &fakeRoomClient{joinResp: &roomv1.JoinRoomResponse{
|
roomClient := &fakeRoomClient{joinResp: &roomv1.JoinRoomResponse{
|
||||||
Result: &roomv1.CommandResult{Applied: true, RoomVersion: 7, ServerTimeMs: 1_700_000_000_123},
|
Result: &roomv1.CommandResult{Applied: true, RoomVersion: 7, ServerTimeMs: 1_700_000_000_123},
|
||||||
@ -1336,6 +1377,7 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
|||||||
Mode: "voice",
|
Mode: "voice",
|
||||||
Status: "active",
|
Status: "active",
|
||||||
ChatEnabled: true,
|
ChatEnabled: true,
|
||||||
|
Locked: true,
|
||||||
MicSeats: []*roomv1.SeatState{{SeatNo: 1, UserId: 102, PublishState: "publishing", MicSessionId: "mic-102"}, {SeatNo: 2, Locked: true}},
|
MicSeats: []*roomv1.SeatState{{SeatNo: 1, UserId: 102, PublishState: "publishing", MicSessionId: "mic-102"}, {SeatNo: 2, Locked: true}},
|
||||||
OnlineUsers: []*roomv1.RoomUser{{UserId: 42, Role: "audience"}, {UserId: 102, Role: "admin"}},
|
OnlineUsers: []*roomv1.RoomUser{{UserId: 42, Role: "audience"}, {UserId: 102, Role: "admin"}},
|
||||||
GiftRank: []*roomv1.RankItem{{UserId: 301, Score: 900, GiftValue: 900}},
|
GiftRank: []*roomv1.RankItem{{UserId: 301, Score: 900, GiftValue: 900}},
|
||||||
@ -1356,7 +1398,7 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
|||||||
AppScene: "voice_chat_room",
|
AppScene: "voice_chat_room",
|
||||||
})
|
})
|
||||||
router := handler.Routes(auth.NewVerifier("secret"))
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/join", bytes.NewReader([]byte(`{"room_id":"room_1001","command_id":"cmd-join","role":"audience"}`)))
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/join", bytes.NewReader([]byte(`{"room_id":"room_1001","command_id":"cmd-join","role":"audience","password":"1234"}`)))
|
||||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
request.Header.Set("X-Request-ID", "req-join-initial")
|
request.Header.Set("X-Request-ID", "req-join-initial")
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
@ -1372,9 +1414,12 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
|||||||
}
|
}
|
||||||
data := response.Data.(map[string]any)
|
data := response.Data.(map[string]any)
|
||||||
room := data["room"].(map[string]any)
|
room := data["room"].(map[string]any)
|
||||||
if room["room_id"] != "room_1001" || room["im_group_id"] != "room_1001" || room["title"] != "Live Room" || room["online_count"].(float64) != 2 {
|
if room["room_id"] != "room_1001" || room["im_group_id"] != "room_1001" || room["title"] != "Live Room" || room["online_count"].(float64) != 2 || room["locked"] != true {
|
||||||
t.Fatalf("unexpected initial room data: %+v", room)
|
t.Fatalf("unexpected initial room data: %+v", room)
|
||||||
}
|
}
|
||||||
|
if roomClient.lastJoin == nil || roomClient.lastJoin.GetPassword() != "1234" {
|
||||||
|
t.Fatalf("join must forward room password to room-service: %+v", roomClient.lastJoin)
|
||||||
|
}
|
||||||
seats := data["seats"].([]any)
|
seats := data["seats"].([]any)
|
||||||
if len(seats) != 2 || seats[0].(map[string]any)["user_id"] != "102" {
|
if len(seats) != 2 || seats[0].(map[string]any)["user_id"] != "102" {
|
||||||
t.Fatalf("join response must include first-screen seats: %+v", seats)
|
t.Fatalf("join response must include first-screen seats: %+v", seats)
|
||||||
@ -1401,7 +1446,7 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
|
|||||||
profileClient := &fakeUserProfileClient{regionID: 1001}
|
profileClient := &fakeUserProfileClient{regionID: 1001}
|
||||||
queryClient := &fakeRoomQueryClient{resp: &roomv1.ListRoomsResponse{
|
queryClient := &fakeRoomQueryClient{resp: &roomv1.ListRoomsResponse{
|
||||||
Rooms: []*roomv1.RoomListItem{
|
Rooms: []*roomv1.RoomListItem{
|
||||||
{RoomId: "room-1", VisibleRegionId: 1001, Heat: 90},
|
{RoomId: "room-1", VisibleRegionId: 1001, Heat: 90, Locked: true},
|
||||||
},
|
},
|
||||||
NextCursor: "cursor-2",
|
NextCursor: "cursor-2",
|
||||||
}}
|
}}
|
||||||
@ -1437,13 +1482,43 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
|
|||||||
data := response.Data.(map[string]any)
|
data := response.Data.(map[string]any)
|
||||||
rooms := data["rooms"].([]any)
|
rooms := data["rooms"].([]any)
|
||||||
first := rooms[0].(map[string]any)
|
first := rooms[0].(map[string]any)
|
||||||
if first["im_group_id"] != "room-1" {
|
if first["im_group_id"] != "room-1" || first["locked"] != true {
|
||||||
t.Fatalf("room list must expose room IM group id: %+v", first)
|
t.Fatalf("room list must expose room IM group id: %+v", first)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetRoomPasswordForwardsPasswordAndReturnsLockedState(t *testing.T) {
|
||||||
|
client := &fakeRoomClient{}
|
||||||
|
router := NewHandlerWithClients(client, nil, nil, &fakeUserProfileClient{}).Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/password", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-password","locked":true,"password":" 1234 "}`)))
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
request.Header.Set("X-Request-ID", "req-password")
|
||||||
|
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 client.lastRoomPassword == nil || !client.lastRoomPassword.GetLocked() || client.lastRoomPassword.GetPassword() != "1234" {
|
||||||
|
t.Fatalf("room password request was not normalized and forwarded: %+v", client.lastRoomPassword)
|
||||||
|
}
|
||||||
|
if strings.Contains(recorder.Body.String(), "password_hash") || strings.Contains(recorder.Body.String(), "room_password_hash") {
|
||||||
|
t.Fatalf("set password response must not expose password hash: %s", recorder.Body.String())
|
||||||
|
}
|
||||||
|
var response httpkit.ResponseEnvelope
|
||||||
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("decode room password response failed: %v", err)
|
||||||
|
}
|
||||||
|
data := response.Data.(map[string]any)
|
||||||
|
room := data["room"].(map[string]any)
|
||||||
|
if room["locked"] != true {
|
||||||
|
t.Fatalf("set password response must expose locked state: %+v", room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestListRoomsRejectsMineFeedTabs(t *testing.T) {
|
func TestListRoomsRejectsMineFeedTabs(t *testing.T) {
|
||||||
for _, tab := range []string{"visited", "friend", "following", "me"} {
|
for _, tab := range []string{"visited", "friend", "following", "followed", "me"} {
|
||||||
t.Run(tab, func(t *testing.T) {
|
t.Run(tab, func(t *testing.T) {
|
||||||
queryClient := &fakeRoomQueryClient{}
|
queryClient := &fakeRoomQueryClient{}
|
||||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})
|
||||||
@ -1466,7 +1541,7 @@ func TestListRoomsRejectsMineFeedTabs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestListRoomFeedsSupportsMineFeedTabs(t *testing.T) {
|
func TestListRoomFeedsSupportsMineFeedTabs(t *testing.T) {
|
||||||
for _, tab := range []string{"visited", "friend", "following"} {
|
for _, tab := range []string{"visited", "friend", "following", "followed"} {
|
||||||
t.Run(tab, func(t *testing.T) {
|
t.Run(tab, func(t *testing.T) {
|
||||||
profileClient := &fakeUserProfileClient{regionID: 1001}
|
profileClient := &fakeUserProfileClient{regionID: 1001}
|
||||||
socialClient := &fakeUserSocialClient{}
|
socialClient := &fakeUserSocialClient{}
|
||||||
@ -1475,7 +1550,7 @@ func TestListRoomFeedsSupportsMineFeedTabs(t *testing.T) {
|
|||||||
}}
|
}}
|
||||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||||
handler.SetRoomQueryClient(queryClient)
|
handler.SetRoomQueryClient(queryClient)
|
||||||
if tab != "visited" {
|
if tab == "friend" || tab == "following" {
|
||||||
handler.SetUserSocialClient(socialClient)
|
handler.SetUserSocialClient(socialClient)
|
||||||
}
|
}
|
||||||
router := handler.Routes(auth.NewVerifier("secret"))
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
@ -1493,9 +1568,9 @@ func TestListRoomFeedsSupportsMineFeedTabs(t *testing.T) {
|
|||||||
t.Fatalf("feed tab was not forwarded with authenticated user and region: %+v", queryClient.lastFeeds)
|
t.Fatalf("feed tab was not forwarded with authenticated user and region: %+v", queryClient.lastFeeds)
|
||||||
}
|
}
|
||||||
switch tab {
|
switch tab {
|
||||||
case "visited":
|
case "visited", "followed":
|
||||||
if len(queryClient.lastFeeds.GetRelatedUsers()) != 0 || socialClient.lastFollowing != nil || socialClient.lastFriends != nil {
|
if len(queryClient.lastFeeds.GetRelatedUsers()) != 0 || socialClient.lastFollowing != nil || socialClient.lastFriends != nil {
|
||||||
t.Fatalf("visited feed must not load social relations: feeds=%+v social=%+v/%+v", queryClient.lastFeeds, socialClient.lastFollowing, socialClient.lastFriends)
|
t.Fatalf("%s feed must not load social relations: feeds=%+v social=%+v/%+v", tab, queryClient.lastFeeds, socialClient.lastFollowing, socialClient.lastFriends)
|
||||||
}
|
}
|
||||||
case "following":
|
case "following":
|
||||||
if socialClient.lastFollowing == nil || socialClient.lastFollowing.GetUserId() != 42 {
|
if socialClient.lastFollowing == nil || socialClient.lastFollowing.GetUserId() != 42 {
|
||||||
@ -1524,12 +1599,12 @@ func TestListRoomFeedsRelationCursorContinuesUserSocialScan(t *testing.T) {
|
|||||||
handler.SetRoomQueryClient(queryClient)
|
handler.SetRoomQueryClient(queryClient)
|
||||||
handler.SetUserSocialClient(socialClient)
|
handler.SetUserSocialClient(socialClient)
|
||||||
router := handler.Routes(auth.NewVerifier("secret"))
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
cursorPayload, _ := json.Marshal(roomFeedRelationCursor{
|
cursorPayload, _ := json.Marshal(map[string]any{
|
||||||
Tab: "following",
|
"tab": "following",
|
||||||
Query: "music",
|
"query": "music",
|
||||||
UpdatedAtMS: 8000,
|
"updated_at_ms": int64(8000),
|
||||||
SubjectUserID: 10002,
|
"subject_user_id": int64(10002),
|
||||||
RoomID: "room-feed",
|
"room_id": "room-feed",
|
||||||
})
|
})
|
||||||
cursor := base64.RawURLEncoding.EncodeToString(cursorPayload)
|
cursor := base64.RawURLEncoding.EncodeToString(cursorPayload)
|
||||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms/feeds?tab=following&q=music&cursor="+cursor, nil)
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms/feeds?tab=following&q=music&cursor="+cursor, nil)
|
||||||
@ -1546,6 +1621,52 @@ func TestListRoomFeedsRelationCursorContinuesUserSocialScan(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRoomFollowActionsUseAuthenticatedUser(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
}{
|
||||||
|
{name: "follow", method: http.MethodPost},
|
||||||
|
{name: "unfollow", method: http.MethodDelete},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
roomClient := &fakeRoomClient{}
|
||||||
|
router := NewHandler(roomClient).Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(test.method, "/api/v1/rooms/room-follow/follow", nil)
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
request.Header.Set("X-Request-ID", "req-room-"+test.name)
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
var response httpkit.ResponseEnvelope
|
||||||
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("decode room follow response failed: %v", err)
|
||||||
|
}
|
||||||
|
data := response.Data.(map[string]any)
|
||||||
|
switch test.method {
|
||||||
|
case http.MethodPost:
|
||||||
|
if roomClient.lastFollowRoom == nil || roomClient.lastFollowRoom.GetUserId() != 42 || roomClient.lastFollowRoom.GetRoomId() != "room-follow" {
|
||||||
|
t.Fatalf("follow room request must use authenticated user and path room: %+v", roomClient.lastFollowRoom)
|
||||||
|
}
|
||||||
|
if data["is_followed"] != true || data["room_id"] != "room-follow" {
|
||||||
|
t.Fatalf("follow response mismatch: %+v", data)
|
||||||
|
}
|
||||||
|
case http.MethodDelete:
|
||||||
|
if roomClient.lastUnfollowRoom == nil || roomClient.lastUnfollowRoom.GetUserId() != 42 || roomClient.lastUnfollowRoom.GetRoomId() != "room-follow" {
|
||||||
|
t.Fatalf("unfollow room request must use authenticated user and path room: %+v", roomClient.lastUnfollowRoom)
|
||||||
|
}
|
||||||
|
if data["is_followed"] != false || data["room_id"] != "room-follow" {
|
||||||
|
t.Fatalf("unfollow response mismatch: %+v", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetMyRoomUsesAuthenticatedOwnerAndReturnsIMGroup(t *testing.T) {
|
func TestGetMyRoomUsesAuthenticatedOwnerAndReturnsIMGroup(t *testing.T) {
|
||||||
queryClient := &fakeRoomQueryClient{myRoomResp: &roomv1.GetMyRoomResponse{
|
queryClient := &fakeRoomQueryClient{myRoomResp: &roomv1.GetMyRoomResponse{
|
||||||
HasRoom: true,
|
HasRoom: true,
|
||||||
@ -1755,6 +1876,52 @@ func TestGetRoomSnapshotUsesAuthenticatedUser(t *testing.T) {
|
|||||||
if !ok || len(seats) != 2 {
|
if !ok || len(seats) != 2 {
|
||||||
t.Fatalf("snapshot DTO must include seat list: %+v", data["seats"])
|
t.Fatalf("snapshot DTO must include seat list: %+v", data["seats"])
|
||||||
}
|
}
|
||||||
|
if data["is_followed"] != false {
|
||||||
|
t.Fatalf("snapshot DTO must expose is_followed=false explicitly: %+v", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRoomDetailIncludesFollowState(t *testing.T) {
|
||||||
|
queryClient := &fakeRoomQueryClient{snapshotResp: &roomv1.GetRoomSnapshotResponse{
|
||||||
|
Room: &roomv1.RoomSnapshot{
|
||||||
|
RoomId: "room-detail",
|
||||||
|
OwnerUserId: 1001,
|
||||||
|
Status: "active",
|
||||||
|
ChatEnabled: true,
|
||||||
|
Version: 9,
|
||||||
|
MicSeats: []*roomv1.SeatState{{SeatNo: 1, UserId: 42}},
|
||||||
|
OnlineUsers: []*roomv1.RoomUser{
|
||||||
|
{UserId: 42, Role: "audience", JoinedAtMs: 1000, LastSeenAtMs: 2000},
|
||||||
|
},
|
||||||
|
RoomExt: map[string]string{"title": "Detail Room"},
|
||||||
|
},
|
||||||
|
ServerTimeMs: 123456,
|
||||||
|
IsFollowed: true,
|
||||||
|
}}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||||
|
handler.SetRoomQueryClient(queryClient)
|
||||||
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms/room-detail/detail", nil)
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
request.Header.Set("X-Request-ID", "req-room-detail")
|
||||||
|
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.lastSnapshot == nil || queryClient.lastSnapshot.GetViewerUserId() != 42 || queryClient.lastSnapshot.GetRoomId() != "room-detail" {
|
||||||
|
t.Fatalf("detail must read snapshot with authenticated viewer: %+v", queryClient.lastSnapshot)
|
||||||
|
}
|
||||||
|
var response httpkit.ResponseEnvelope
|
||||||
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("decode detail response failed: %v", err)
|
||||||
|
}
|
||||||
|
data := response.Data.(map[string]any)
|
||||||
|
if data["is_followed"] != true {
|
||||||
|
t.Fatalf("detail response must expose room follow state: %+v", data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetRoomSnapshotRejectsInvalidRoomIDBeforeGRPC(t *testing.T) {
|
func TestGetRoomSnapshotRejectsInvalidRoomIDBeforeGRPC(t *testing.T) {
|
||||||
@ -3481,9 +3648,9 @@ func TestTencentIMUserSigUsesAuthenticatedUserIDAndFailsClosed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTencentRTCTokenUsesPresenceGuardAndInternalUserID(t *testing.T) {
|
func TestTencentRTCTokenUsesPresenceGuardAndInternalUserID(t *testing.T) {
|
||||||
previousNow := timeNow
|
previousNow := roomapi.TimeNow
|
||||||
timeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
roomapi.TimeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
||||||
defer func() { timeNow = previousNow }()
|
defer func() { roomapi.TimeNow = previousNow }()
|
||||||
|
|
||||||
guard := &fakeRoomGuardClient{presenceResp: &roomv1.VerifyRoomPresenceResponse{Present: true, RoomVersion: 9}}
|
guard := &fakeRoomGuardClient{presenceResp: &roomv1.VerifyRoomPresenceResponse{Present: true, RoomVersion: 9}}
|
||||||
handler := NewHandlerWithConfig(&fakeRoomClient{}, guard, nil, nil, TencentIMConfig{})
|
handler := NewHandlerWithConfig(&fakeRoomClient{}, guard, nil, nil, TencentIMConfig{})
|
||||||
@ -3856,7 +4023,11 @@ func assertTencentCallback(t *testing.T, recorder *httptest.ResponseRecorder, st
|
|||||||
t.Fatalf("status mismatch: got %d want %d body=%s", recorder.Code, statusCode, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d want %d body=%s", recorder.Code, statusCode, recorder.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
var response tencentIMCallbackResponse
|
var response struct {
|
||||||
|
ActionStatus string `json:"ActionStatus"`
|
||||||
|
ErrorInfo string `json:"ErrorInfo"`
|
||||||
|
ErrorCode int `json:"ErrorCode"`
|
||||||
|
}
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode tencent callback response failed: %v", err)
|
t.Fatalf("decode tencent callback response failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -3872,7 +4043,10 @@ func assertTencentRTCCallback(t *testing.T, recorder *httptest.ResponseRecorder,
|
|||||||
t.Fatalf("status mismatch: got %d want %d body=%s", recorder.Code, statusCode, recorder.Body.String())
|
t.Fatalf("status mismatch: got %d want %d body=%s", recorder.Code, statusCode, recorder.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
var response tencentRTCCallbackResponse
|
var response struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode tencent rtc callback response failed: %v", err)
|
t.Fatalf("decode tencent rtc callback response failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
package roomapi
|
||||||
|
|
||||||
|
import walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
|
||||||
|
type resourceData struct {
|
||||||
|
ResourceID int64 `json:"resource_id"`
|
||||||
|
ResourceCode string `json:"resource_code"`
|
||||||
|
ResourceType string `json:"resource_type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Grantable bool `json:"grantable"`
|
||||||
|
GrantStrategy string `json:"grant_strategy"`
|
||||||
|
WalletAssetType string `json:"wallet_asset_type,omitempty"`
|
||||||
|
WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"`
|
||||||
|
UsageScopes []string `json:"usage_scopes"`
|
||||||
|
AssetURL string `json:"asset_url"`
|
||||||
|
PreviewURL string `json:"preview_url"`
|
||||||
|
AnimationURL string `json:"animation_url"`
|
||||||
|
MetadataJSON string `json:"metadata_json"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type giftConfigData struct {
|
||||||
|
GiftID string `json:"gift_id"`
|
||||||
|
ResourceID int64 `json:"resource_id"`
|
||||||
|
Resource resourceData `json:"resource"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
PresentationJSON string `json:"presentation_json"`
|
||||||
|
PriceVersion string `json:"price_version"`
|
||||||
|
GiftTypeCode string `json:"gift_type_code"`
|
||||||
|
ChargeAssetType string `json:"charge_asset_type"`
|
||||||
|
CoinPrice int64 `json:"coin_price"`
|
||||||
|
GiftPointAmount int64 `json:"gift_point_amount"`
|
||||||
|
HeatValue int64 `json:"heat_value"`
|
||||||
|
EffectiveFromMS int64 `json:"effective_from_ms"`
|
||||||
|
EffectiveToMS int64 `json:"effective_to_ms"`
|
||||||
|
EffectTypes []string `json:"effect_types"`
|
||||||
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
RegionIDs []int64 `json:"region_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func giftFromProto(gift *walletv1.GiftConfig) giftConfigData {
|
||||||
|
if gift == nil {
|
||||||
|
return giftConfigData{}
|
||||||
|
}
|
||||||
|
return giftConfigData{
|
||||||
|
GiftID: gift.GetGiftId(),
|
||||||
|
ResourceID: gift.GetResourceId(),
|
||||||
|
Resource: resourceFromProto(gift.GetResource()),
|
||||||
|
Status: gift.GetStatus(),
|
||||||
|
Name: gift.GetName(),
|
||||||
|
SortOrder: gift.GetSortOrder(),
|
||||||
|
PresentationJSON: gift.GetPresentationJson(),
|
||||||
|
PriceVersion: gift.GetPriceVersion(),
|
||||||
|
GiftTypeCode: gift.GetGiftTypeCode(),
|
||||||
|
ChargeAssetType: gift.GetChargeAssetType(),
|
||||||
|
CoinPrice: gift.GetCoinPrice(),
|
||||||
|
GiftPointAmount: gift.GetGiftPointAmount(),
|
||||||
|
HeatValue: gift.GetHeatValue(),
|
||||||
|
EffectiveFromMS: gift.GetEffectiveFromMs(),
|
||||||
|
EffectiveToMS: gift.GetEffectiveToMs(),
|
||||||
|
EffectTypes: gift.GetEffectTypes(),
|
||||||
|
CreatedAtMS: gift.GetCreatedAtMs(),
|
||||||
|
UpdatedAtMS: gift.GetUpdatedAtMs(),
|
||||||
|
RegionIDs: gift.GetRegionIds(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceFromProto(item *walletv1.Resource) resourceData {
|
||||||
|
if item == nil {
|
||||||
|
return resourceData{}
|
||||||
|
}
|
||||||
|
return resourceData{
|
||||||
|
ResourceID: item.GetResourceId(),
|
||||||
|
ResourceCode: item.GetResourceCode(),
|
||||||
|
ResourceType: item.GetResourceType(),
|
||||||
|
Name: item.GetName(),
|
||||||
|
Status: item.GetStatus(),
|
||||||
|
Grantable: item.GetGrantable(),
|
||||||
|
GrantStrategy: item.GetGrantStrategy(),
|
||||||
|
WalletAssetType: item.GetWalletAssetType(),
|
||||||
|
WalletAssetAmount: item.GetWalletAssetAmount(),
|
||||||
|
UsageScopes: item.GetUsageScopes(),
|
||||||
|
AssetURL: item.GetAssetUrl(),
|
||||||
|
PreviewURL: item.GetPreviewUrl(),
|
||||||
|
AnimationURL: item.GetAnimationUrl(),
|
||||||
|
MetadataJSON: item.GetMetadataJson(),
|
||||||
|
SortOrder: item.GetSortOrder(),
|
||||||
|
CreatedAtMS: item.GetCreatedAtMs(),
|
||||||
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
package roomapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"hyapp/pkg/tencentrtc"
|
||||||
|
"hyapp/services/gateway-service/internal/client"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler owns room-facing HTTP endpoints in gateway.
|
||||||
|
// It keeps gateway as protocol adapter only: room state still belongs to room-service.
|
||||||
|
type Handler struct {
|
||||||
|
roomClient client.RoomClient
|
||||||
|
roomGuardClient client.RoomGuardClient
|
||||||
|
roomQueryClient client.RoomQueryClient
|
||||||
|
userProfileClient client.UserProfileClient
|
||||||
|
userSocialClient client.UserSocialClient
|
||||||
|
walletClient client.WalletClient
|
||||||
|
rtcTokenConfig tencentrtc.TokenConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
RoomClient client.RoomClient
|
||||||
|
RoomGuardClient client.RoomGuardClient
|
||||||
|
RoomQueryClient client.RoomQueryClient
|
||||||
|
UserProfileClient client.UserProfileClient
|
||||||
|
UserSocialClient client.UserSocialClient
|
||||||
|
WalletClient client.WalletClient
|
||||||
|
RTCTokenConfig tencentrtc.TokenConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
roomClient: config.RoomClient,
|
||||||
|
roomGuardClient: config.RoomGuardClient,
|
||||||
|
roomQueryClient: config.RoomQueryClient,
|
||||||
|
userProfileClient: config.UserProfileClient,
|
||||||
|
userSocialClient: config.UserSocialClient,
|
||||||
|
walletClient: config.WalletClient,
|
||||||
|
rtcTokenConfig: config.RTCTokenConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) RoomHandlers() httproutes.RoomHandlers {
|
||||||
|
return httproutes.RoomHandlers{
|
||||||
|
GetMyRoom: h.getMyRoom,
|
||||||
|
ListRoomFeeds: h.listRoomFeeds,
|
||||||
|
ListRooms: h.listRooms,
|
||||||
|
GetCurrentRoom: h.getCurrentRoom,
|
||||||
|
GetRoomSnapshot: h.getRoomSnapshot,
|
||||||
|
GetRoomDetail: h.getRoomDetail,
|
||||||
|
ListRoomOnlineUsers: h.listRoomOnlineUsers,
|
||||||
|
GetRoomGiftPanel: h.getRoomGiftPanel,
|
||||||
|
FollowRoom: h.handleRoomFollow,
|
||||||
|
CreateRoom: h.createRoom,
|
||||||
|
UpdateRoomProfile: h.updateRoomProfile,
|
||||||
|
JoinRoom: h.joinRoom,
|
||||||
|
RoomHeartbeat: h.roomHeartbeat,
|
||||||
|
LeaveRoom: h.leaveRoom,
|
||||||
|
CloseRoom: h.closeRoom,
|
||||||
|
MicUp: h.micUp,
|
||||||
|
MicDown: h.micDown,
|
||||||
|
ChangeMicSeat: h.changeMicSeat,
|
||||||
|
ConfirmMicPublishing: h.confirmMicPublishing,
|
||||||
|
SetMicMute: h.setMicMute,
|
||||||
|
SetMicSeatLock: h.setMicSeatLock,
|
||||||
|
SetChatEnabled: h.setChatEnabled,
|
||||||
|
SetRoomPassword: h.setRoomPassword,
|
||||||
|
SetRoomAdmin: h.setRoomAdmin,
|
||||||
|
MuteUser: h.muteUser,
|
||||||
|
KickUser: h.kickUser,
|
||||||
|
UnbanUser: h.unbanUser,
|
||||||
|
SendGift: h.sendGift,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) IssueTencentRTCToken(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.issueTencentRTCToken(writer, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) BatchRoomDisplayProfiles(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.batchRoomDisplayProfiles(writer, request)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package roomapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@ -8,13 +8,14 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
|
||||||
userv1 "hyapp.local/api/proto/user/v1"
|
|
||||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
|
||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/pkg/roomid"
|
"hyapp/pkg/roomid"
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
|
|
||||||
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -51,7 +52,7 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
|
|||||||
|
|
||||||
viewerUserID := auth.UserIDFromContext(request.Context())
|
viewerUserID := auth.UserIDFromContext(request.Context())
|
||||||
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: viewerUserID,
|
UserId: viewerUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -61,7 +62,7 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomQueryClient.ListRooms(request.Context(), &roomv1.ListRoomsRequest{
|
resp, err := h.roomQueryClient.ListRooms(request.Context(), &roomv1.ListRoomsRequest{
|
||||||
Meta: meta(request, "", ""),
|
Meta: httpkit.RoomMeta(request, "", ""),
|
||||||
ViewerUserId: viewerUserID,
|
ViewerUserId: viewerUserID,
|
||||||
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
||||||
Tab: tab,
|
Tab: tab,
|
||||||
@ -76,7 +77,7 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
|
|||||||
httpkit.WriteOK(writer, request, roomListDataFromProto(resp))
|
httpkit.WriteOK(writer, request, roomListDataFromProto(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// listRoomFeeds 查询 Mine 页 visited/friend/following 用户房间流。
|
// listRoomFeeds 查询 Mine 页 visited/friend/following/followed 房间流。
|
||||||
func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userProfileClient == nil || h.roomQueryClient == nil {
|
if h.userProfileClient == nil || h.roomQueryClient == nil {
|
||||||
// feed 同样需要 user-service 区域归属,避免跨区域露出用户关系流房间。
|
// feed 同样需要 user-service 区域归属,避免跨区域露出用户关系流房间。
|
||||||
@ -97,7 +98,7 @@ func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Reques
|
|||||||
|
|
||||||
viewerUserID := auth.UserIDFromContext(request.Context())
|
viewerUserID := auth.UserIDFromContext(request.Context())
|
||||||
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: viewerUserID,
|
UserId: viewerUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -116,7 +117,7 @@ func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomQueryClient.ListRoomFeeds(request.Context(), &roomv1.ListRoomFeedsRequest{
|
resp, err := h.roomQueryClient.ListRoomFeeds(request.Context(), &roomv1.ListRoomFeedsRequest{
|
||||||
Meta: meta(request, "", ""),
|
Meta: httpkit.RoomMeta(request, "", ""),
|
||||||
ViewerUserId: viewerUserID,
|
ViewerUserId: viewerUserID,
|
||||||
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
||||||
Tab: tab,
|
Tab: tab,
|
||||||
@ -133,7 +134,7 @@ func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) roomFeedRelatedUsers(request *http.Request, tab string, query string, rawCursor string) ([]*roomv1.RoomFeedRelatedUser, bool, error) {
|
func (h *Handler) roomFeedRelatedUsers(request *http.Request, tab string, query string, rawCursor string) ([]*roomv1.RoomFeedRelatedUser, bool, error) {
|
||||||
if tab == "visited" {
|
if tab == "visited" || tab == "followed" {
|
||||||
return nil, true, nil
|
return nil, true, nil
|
||||||
}
|
}
|
||||||
if h.userSocialClient == nil {
|
if h.userSocialClient == nil {
|
||||||
@ -165,7 +166,7 @@ func (h *Handler) roomFeedRelatedUserBatch(request *http.Request, tab string, vi
|
|||||||
switch tab {
|
switch tab {
|
||||||
case "following":
|
case "following":
|
||||||
resp, err := h.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{
|
resp, err := h.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: viewerUserID,
|
UserId: viewerUserID,
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PageSize: defaultRoomFeedRelationPageSize,
|
PageSize: defaultRoomFeedRelationPageSize,
|
||||||
@ -187,7 +188,7 @@ func (h *Handler) roomFeedRelatedUserBatch(request *http.Request, tab string, vi
|
|||||||
return relatedUsers, cursor, nil
|
return relatedUsers, cursor, nil
|
||||||
case "friend":
|
case "friend":
|
||||||
resp, err := h.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{
|
resp, err := h.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: viewerUserID,
|
UserId: viewerUserID,
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PageSize: defaultRoomFeedRelationPageSize,
|
PageSize: defaultRoomFeedRelationPageSize,
|
||||||
@ -250,6 +251,8 @@ func parseRoomFeedTab(raw string) (string, bool) {
|
|||||||
return "friend", true
|
return "friend", true
|
||||||
case "following":
|
case "following":
|
||||||
return "following", true
|
return "following", true
|
||||||
|
case "followed":
|
||||||
|
return "followed", true
|
||||||
default:
|
default:
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@ -288,7 +291,7 @@ func (h *Handler) getMyRoom(writer http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomQueryClient.GetMyRoom(request.Context(), &roomv1.GetMyRoomRequest{
|
resp, err := h.roomQueryClient.GetMyRoom(request.Context(), &roomv1.GetMyRoomRequest{
|
||||||
Meta: meta(request, "", ""),
|
Meta: httpkit.RoomMeta(request, "", ""),
|
||||||
OwnerUserId: auth.UserIDFromContext(request.Context()),
|
OwnerUserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -307,7 +310,7 @@ func (h *Handler) getCurrentRoom(writer http.ResponseWriter, request *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomQueryClient.GetCurrentRoom(request.Context(), &roomv1.GetCurrentRoomRequest{
|
resp, err := h.roomQueryClient.GetCurrentRoom(request.Context(), &roomv1.GetCurrentRoomRequest{
|
||||||
Meta: meta(request, "", ""),
|
Meta: httpkit.RoomMeta(request, "", ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -363,11 +366,11 @@ func (h *Handler) getRoomSnapshot(writer http.ResponseWriter, request *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
|
resp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
|
||||||
Meta: meta(request, roomID, ""),
|
Meta: httpkit.RoomMeta(request, roomID, ""),
|
||||||
RoomId: roomID,
|
RoomId: roomID,
|
||||||
ViewerUserId: auth.UserIDFromContext(request.Context()),
|
ViewerUserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
write(writer, request, roomSnapshotDataFromProto(resp), err)
|
httpkit.Write(writer, request, roomSnapshotDataFromProto(resp), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRoomDetail 返回语音房详情页首屏可直接渲染的数据。
|
// getRoomDetail 返回语音房详情页首屏可直接渲染的数据。
|
||||||
@ -385,7 +388,7 @@ func (h *Handler) getRoomDetail(writer http.ResponseWriter, request *http.Reques
|
|||||||
|
|
||||||
viewerUserID := auth.UserIDFromContext(request.Context())
|
viewerUserID := auth.UserIDFromContext(request.Context())
|
||||||
resp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
|
resp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
|
||||||
Meta: meta(request, roomID, ""),
|
Meta: httpkit.RoomMeta(request, roomID, ""),
|
||||||
RoomId: roomID,
|
RoomId: roomID,
|
||||||
ViewerUserId: viewerUserID,
|
ViewerUserId: viewerUserID,
|
||||||
})
|
})
|
||||||
@ -408,19 +411,19 @@ func (h *Handler) listRoomOnlineUsers(writer http.ResponseWriter, request *http.
|
|||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page, ok := parsePositiveInt32Query(request, "page", 1)
|
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
|
||||||
if !ok {
|
if !ok {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", 50)
|
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 50)
|
||||||
if !ok {
|
if !ok {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomQueryClient.ListRoomOnlineUsers(request.Context(), &roomv1.ListRoomOnlineUsersRequest{
|
resp, err := h.roomQueryClient.ListRoomOnlineUsers(request.Context(), &roomv1.ListRoomOnlineUsersRequest{
|
||||||
Meta: meta(request, roomID, ""),
|
Meta: httpkit.RoomMeta(request, roomID, ""),
|
||||||
RoomId: roomID,
|
RoomId: roomID,
|
||||||
ViewerUserId: auth.UserIDFromContext(request.Context()),
|
ViewerUserId: auth.UserIDFromContext(request.Context()),
|
||||||
Page: page,
|
Page: page,
|
||||||
@ -451,7 +454,7 @@ func (h *Handler) getRoomGiftPanel(writer http.ResponseWriter, request *http.Req
|
|||||||
|
|
||||||
viewerUserID := auth.UserIDFromContext(request.Context())
|
viewerUserID := auth.UserIDFromContext(request.Context())
|
||||||
snapshotResp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
|
snapshotResp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
|
||||||
Meta: meta(request, roomID, ""),
|
Meta: httpkit.RoomMeta(request, roomID, ""),
|
||||||
RoomId: roomID,
|
RoomId: roomID,
|
||||||
ViewerUserId: viewerUserID,
|
ViewerUserId: viewerUserID,
|
||||||
})
|
})
|
||||||
@ -497,6 +500,48 @@ func (h *Handler) getRoomGiftPanel(writer http.ResponseWriter, request *http.Req
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleRoomFollow 建立或取消当前用户对房间的关注关系。
|
||||||
|
// 关注关系由 room-service 保存,gateway 只负责鉴权用户和 path room_id 的协议转换。
|
||||||
|
func (h *Handler) handleRoomFollow(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if h.roomClient == 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
|
||||||
|
}
|
||||||
|
|
||||||
|
viewerUserID := auth.UserIDFromContext(request.Context())
|
||||||
|
switch request.Method {
|
||||||
|
case http.MethodPost:
|
||||||
|
resp, err := h.roomClient.FollowRoom(request.Context(), &roomv1.FollowRoomRequest{
|
||||||
|
Meta: httpkit.RoomMeta(request, roomID, ""),
|
||||||
|
RoomId: roomID,
|
||||||
|
UserId: viewerUserID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, roomFollowDataFromProto(resp))
|
||||||
|
case http.MethodDelete:
|
||||||
|
resp, err := h.roomClient.UnfollowRoom(request.Context(), &roomv1.UnfollowRoomRequest{
|
||||||
|
Meta: httpkit.RoomMeta(request, roomID, ""),
|
||||||
|
RoomId: roomID,
|
||||||
|
UserId: viewerUserID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, roomUnfollowDataFromProto(resp))
|
||||||
|
default:
|
||||||
|
httpkit.WriteError(writer, request, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// createRoom 把创建房间 HTTP JSON 请求转换为 room-service CreateRoom 命令。
|
// createRoom 把创建房间 HTTP JSON 请求转换为 room-service CreateRoom 命令。
|
||||||
func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request) {
|
||||||
var body struct {
|
var body struct {
|
||||||
@ -508,7 +553,7 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
|
|||||||
RoomDescription string `json:"room_description"`
|
RoomDescription string `json:"room_description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
body.CommandID = strings.TrimSpace(body.CommandID)
|
body.CommandID = strings.TrimSpace(body.CommandID)
|
||||||
@ -527,7 +572,7 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -538,7 +583,7 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
|
|||||||
roomID := appcode.NewScopedID(appcode.FromContext(request.Context()))
|
roomID := appcode.NewScopedID(appcode.FromContext(request.Context()))
|
||||||
roomShortID := userResp.GetUser().GetDisplayUserId()
|
roomShortID := userResp.GetUser().GetDisplayUserId()
|
||||||
resp, err := h.roomClient.CreateRoom(request.Context(), &roomv1.CreateRoomRequest{
|
resp, err := h.roomClient.CreateRoom(request.Context(), &roomv1.CreateRoomRequest{
|
||||||
Meta: meta(request, roomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, roomID, body.CommandID),
|
||||||
SeatCount: body.SeatCount,
|
SeatCount: body.SeatCount,
|
||||||
Mode: body.Mode,
|
Mode: body.Mode,
|
||||||
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
||||||
@ -547,7 +592,7 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
|
|||||||
RoomDescription: body.RoomDescription,
|
RoomDescription: body.RoomDescription,
|
||||||
RoomShortId: roomShortID,
|
RoomShortId: roomShortID,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateRoomProfile 把 App 房间资料编辑请求转换为 room-service UpdateRoomProfile 命令。
|
// updateRoomProfile 把 App 房间资料编辑请求转换为 room-service UpdateRoomProfile 命令。
|
||||||
@ -561,7 +606,7 @@ func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Re
|
|||||||
SeatCount *int32 `json:"seat_count"`
|
SeatCount *int32 `json:"seat_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
body.RoomID = strings.TrimSpace(body.RoomID)
|
body.RoomID = strings.TrimSpace(body.RoomID)
|
||||||
@ -571,7 +616,7 @@ func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Re
|
|||||||
trimOptionalString(body.RoomDescription)
|
trimOptionalString(body.RoomDescription)
|
||||||
|
|
||||||
resp, err := h.roomClient.UpdateRoomProfile(request.Context(), &roomv1.UpdateRoomProfileRequest{
|
resp, err := h.roomClient.UpdateRoomProfile(request.Context(), &roomv1.UpdateRoomProfileRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
RoomName: body.RoomName,
|
RoomName: body.RoomName,
|
||||||
RoomAvatar: body.RoomAvatar,
|
RoomAvatar: body.RoomAvatar,
|
||||||
RoomDescription: body.RoomDescription,
|
RoomDescription: body.RoomDescription,
|
||||||
@ -597,15 +642,17 @@ func (h *Handler) joinRoom(writer http.ResponseWriter, request *http.Request) {
|
|||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
CommandID string `json:"command_id"`
|
CommandID string `json:"command_id"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.JoinRoom(request.Context(), &roomv1.JoinRoomRequest{
|
resp, err := h.roomClient.JoinRoom(request.Context(), &roomv1.JoinRoomRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
Role: body.Role,
|
Role: body.Role,
|
||||||
|
Password: body.Password,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
@ -662,7 +709,7 @@ func (h *Handler) roomInitialProfiles(request *http.Request, snapshot *roomv1.Ro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserIds: userIDs,
|
UserIds: userIDs,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -693,14 +740,14 @@ func (h *Handler) roomHeartbeat(writer http.ResponseWriter, request *http.Reques
|
|||||||
CommandID string `json:"command_id"`
|
CommandID string `json:"command_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.RoomHeartbeat(request.Context(), &roomv1.RoomHeartbeatRequest{
|
resp, err := h.roomClient.RoomHeartbeat(request.Context(), &roomv1.RoomHeartbeatRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// leaveRoom 把退房 HTTP JSON 请求转换为 room-service LeaveRoom 命令。
|
// leaveRoom 把退房 HTTP JSON 请求转换为 room-service LeaveRoom 命令。
|
||||||
@ -710,14 +757,14 @@ func (h *Handler) leaveRoom(writer http.ResponseWriter, request *http.Request) {
|
|||||||
CommandID string `json:"command_id"`
|
CommandID string `json:"command_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.LeaveRoom(request.Context(), &roomv1.LeaveRoomRequest{
|
resp, err := h.roomClient.LeaveRoom(request.Context(), &roomv1.LeaveRoomRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// closeRoom 是设计稿右上角电源按钮入口:这里只退出当前用户,不关闭房间生命周期。
|
// closeRoom 是设计稿右上角电源按钮入口:这里只退出当前用户,不关闭房间生命周期。
|
||||||
@ -733,15 +780,15 @@ func (h *Handler) micUp(writer http.ResponseWriter, request *http.Request) {
|
|||||||
SeatNo int32 `json:"seat_no"`
|
SeatNo int32 `json:"seat_no"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.MicUp(request.Context(), &roomv1.MicUpRequest{
|
resp, err := h.roomClient.MicUp(request.Context(), &roomv1.MicUpRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
SeatNo: body.SeatNo,
|
SeatNo: body.SeatNo,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// micDown 把下麦 HTTP JSON 请求转换为 room-service MicDown 命令。
|
// micDown 把下麦 HTTP JSON 请求转换为 room-service MicDown 命令。
|
||||||
@ -753,16 +800,16 @@ func (h *Handler) micDown(writer http.ResponseWriter, request *http.Request) {
|
|||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.MicDown(request.Context(), &roomv1.MicDownRequest{
|
resp, err := h.roomClient.MicDown(request.Context(), &roomv1.MicDownRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
TargetUserId: body.TargetUserID,
|
TargetUserId: body.TargetUserID,
|
||||||
Reason: body.Reason,
|
Reason: body.Reason,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// changeMicSeat 把换麦位 HTTP JSON 请求转换为 room-service ChangeMicSeat 命令。
|
// changeMicSeat 把换麦位 HTTP JSON 请求转换为 room-service ChangeMicSeat 命令。
|
||||||
@ -774,16 +821,16 @@ func (h *Handler) changeMicSeat(writer http.ResponseWriter, request *http.Reques
|
|||||||
SeatNo int32 `json:"seat_no"`
|
SeatNo int32 `json:"seat_no"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.ChangeMicSeat(request.Context(), &roomv1.ChangeMicSeatRequest{
|
resp, err := h.roomClient.ChangeMicSeat(request.Context(), &roomv1.ChangeMicSeatRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
TargetUserId: body.TargetUserID,
|
TargetUserId: body.TargetUserID,
|
||||||
SeatNo: body.SeatNo,
|
SeatNo: body.SeatNo,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// confirmMicPublishing 把客户端 SDK 发流成功回调转换为 room-service 发流确认命令。
|
// confirmMicPublishing 把客户端 SDK 发流成功回调转换为 room-service 发流确认命令。
|
||||||
@ -798,19 +845,19 @@ func (h *Handler) confirmMicPublishing(writer http.ResponseWriter, request *http
|
|||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.ConfirmMicPublishing(request.Context(), &roomv1.ConfirmMicPublishingRequest{
|
resp, err := h.roomClient.ConfirmMicPublishing(request.Context(), &roomv1.ConfirmMicPublishingRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
TargetUserId: body.TargetUserID,
|
TargetUserId: body.TargetUserID,
|
||||||
MicSessionId: body.MicSessionID,
|
MicSessionId: body.MicSessionID,
|
||||||
RoomVersion: body.RoomVersion,
|
RoomVersion: body.RoomVersion,
|
||||||
EventTimeMs: body.EventTimeMS,
|
EventTimeMs: body.EventTimeMS,
|
||||||
Source: body.Source,
|
Source: body.Source,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setMicMute 把麦克风静音 HTTP 请求转换为 room-service SetMicMute 命令。
|
// setMicMute 把麦克风静音 HTTP 请求转换为 room-service SetMicMute 命令。
|
||||||
@ -822,16 +869,16 @@ func (h *Handler) setMicMute(writer http.ResponseWriter, request *http.Request)
|
|||||||
Muted bool `json:"muted"`
|
Muted bool `json:"muted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.SetMicMute(request.Context(), &roomv1.SetMicMuteRequest{
|
resp, err := h.roomClient.SetMicMute(request.Context(), &roomv1.SetMicMuteRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
TargetUserId: body.TargetUserID,
|
TargetUserId: body.TargetUserID,
|
||||||
Muted: body.Muted,
|
Muted: body.Muted,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setMicSeatLock 把锁麦 HTTP JSON 请求转换为 room-service SetMicSeatLock 命令。
|
// setMicSeatLock 把锁麦 HTTP JSON 请求转换为 room-service SetMicSeatLock 命令。
|
||||||
@ -843,16 +890,16 @@ func (h *Handler) setMicSeatLock(writer http.ResponseWriter, request *http.Reque
|
|||||||
Locked bool `json:"locked"`
|
Locked bool `json:"locked"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.SetMicSeatLock(request.Context(), &roomv1.SetMicSeatLockRequest{
|
resp, err := h.roomClient.SetMicSeatLock(request.Context(), &roomv1.SetMicSeatLockRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
SeatNo: body.SeatNo,
|
SeatNo: body.SeatNo,
|
||||||
Locked: body.Locked,
|
Locked: body.Locked,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setChatEnabled 把公屏开关 HTTP JSON 请求转换为 room-service SetChatEnabled 命令。
|
// setChatEnabled 把公屏开关 HTTP JSON 请求转换为 room-service SetChatEnabled 命令。
|
||||||
@ -863,15 +910,46 @@ func (h *Handler) setChatEnabled(writer http.ResponseWriter, request *http.Reque
|
|||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.SetChatEnabled(request.Context(), &roomv1.SetChatEnabledRequest{
|
resp, err := h.roomClient.SetChatEnabled(request.Context(), &roomv1.SetChatEnabledRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
Enabled: body.Enabled,
|
Enabled: body.Enabled,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setRoomPassword 把房主锁房 HTTP JSON 请求转换为 room-service SetRoomPassword 命令。
|
||||||
|
func (h *Handler) setRoomPassword(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
var body struct {
|
||||||
|
RoomID string `json:"room_id"`
|
||||||
|
CommandID string `json:"command_id"`
|
||||||
|
Locked bool `json:"locked"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body.RoomID = strings.TrimSpace(body.RoomID)
|
||||||
|
body.CommandID = strings.TrimSpace(body.CommandID)
|
||||||
|
body.Password = strings.TrimSpace(body.Password)
|
||||||
|
|
||||||
|
resp, err := h.roomClient.SetRoomPassword(request.Context(), &roomv1.SetRoomPasswordRequest{
|
||||||
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
|
Locked: body.Locked,
|
||||||
|
Password: body.Password,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, updateRoomProfileDataFromProto(&roomv1.UpdateRoomProfileResponse{
|
||||||
|
Result: resp.GetResult(),
|
||||||
|
Room: resp.GetRoom(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// setRoomAdmin 把管理员增删 HTTP JSON 请求转换为 room-service SetRoomAdmin 命令。
|
// setRoomAdmin 把管理员增删 HTTP JSON 请求转换为 room-service SetRoomAdmin 命令。
|
||||||
@ -883,16 +961,16 @@ func (h *Handler) setRoomAdmin(writer http.ResponseWriter, request *http.Request
|
|||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.SetRoomAdmin(request.Context(), &roomv1.SetRoomAdminRequest{
|
resp, err := h.roomClient.SetRoomAdmin(request.Context(), &roomv1.SetRoomAdminRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
TargetUserId: body.TargetUserID,
|
TargetUserId: body.TargetUserID,
|
||||||
Enabled: body.Enabled,
|
Enabled: body.Enabled,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// muteUser 把禁言 HTTP JSON 请求转换为 room-service MuteUser 命令。
|
// muteUser 把禁言 HTTP JSON 请求转换为 room-service MuteUser 命令。
|
||||||
@ -904,16 +982,16 @@ func (h *Handler) muteUser(writer http.ResponseWriter, request *http.Request) {
|
|||||||
Muted bool `json:"muted"`
|
Muted bool `json:"muted"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.MuteUser(request.Context(), &roomv1.MuteUserRequest{
|
resp, err := h.roomClient.MuteUser(request.Context(), &roomv1.MuteUserRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
TargetUserId: body.TargetUserID,
|
TargetUserId: body.TargetUserID,
|
||||||
Muted: body.Muted,
|
Muted: body.Muted,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// kickUser 把踢人 HTTP JSON 请求转换为 room-service KickUser 命令。
|
// kickUser 把踢人 HTTP JSON 请求转换为 room-service KickUser 命令。
|
||||||
@ -924,15 +1002,15 @@ func (h *Handler) kickUser(writer http.ResponseWriter, request *http.Request) {
|
|||||||
TargetUserID int64 `json:"target_user_id"`
|
TargetUserID int64 `json:"target_user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.KickUser(request.Context(), &roomv1.KickUserRequest{
|
resp, err := h.roomClient.KickUser(request.Context(), &roomv1.KickUserRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
TargetUserId: body.TargetUserID,
|
TargetUserId: body.TargetUserID,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// unbanUser 把解封 HTTP JSON 请求转换为 room-service UnbanUser 命令。
|
// unbanUser 把解封 HTTP JSON 请求转换为 room-service UnbanUser 命令。
|
||||||
@ -943,15 +1021,15 @@ func (h *Handler) unbanUser(writer http.ResponseWriter, request *http.Request) {
|
|||||||
TargetUserID int64 `json:"target_user_id"`
|
TargetUserID int64 `json:"target_user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.UnbanUser(request.Context(), &roomv1.UnbanUserRequest{
|
resp, err := h.roomClient.UnbanUser(request.Context(), &roomv1.UnbanUserRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
TargetUserId: body.TargetUserID,
|
TargetUserId: body.TargetUserID,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendGift 把送礼 HTTP JSON 请求转换为 room-service SendGift 命令。
|
// sendGift 把送礼 HTTP JSON 请求转换为 room-service SendGift 命令。
|
||||||
@ -967,7 +1045,7 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
|
|||||||
GiftCount int32 `json:"gift_count"`
|
GiftCount int32 `json:"gift_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetType := strings.TrimSpace(body.TargetType)
|
targetType := strings.TrimSpace(body.TargetType)
|
||||||
@ -981,14 +1059,14 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.roomClient.SendGift(request.Context(), &roomv1.SendGiftRequest{
|
resp, err := h.roomClient.SendGift(request.Context(), &roomv1.SendGiftRequest{
|
||||||
Meta: meta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
TargetType: targetType,
|
TargetType: targetType,
|
||||||
TargetUserIds: targetUserIDs,
|
TargetUserIds: targetUserIDs,
|
||||||
TargetUserId: firstUserID(targetUserIDs),
|
TargetUserId: firstUserID(targetUserIDs),
|
||||||
GiftId: body.GiftID,
|
GiftId: body.GiftID,
|
||||||
GiftCount: body.GiftCount,
|
GiftCount: body.GiftCount,
|
||||||
})
|
})
|
||||||
write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// batchRoomDisplayProfiles 返回房间和公屏专用展示资料。
|
// batchRoomDisplayProfiles 返回房间和公屏专用展示资料。
|
||||||
@ -1005,6 +1083,31 @@ func (h *Handler) batchRoomDisplayProfiles(writer http.ResponseWriter, request *
|
|||||||
httpkit.WriteOK(writer, request, map[string]any{"profiles": h.roomDisplayProfiles(request, userIDs)})
|
httpkit.WriteOK(writer, request, map[string]any{"profiles": h.roomDisplayProfiles(request, userIDs)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseBatchUserIDs(request *http.Request) ([]int64, bool) {
|
||||||
|
rawValues := append([]string(nil), request.URL.Query()["user_ids"]...)
|
||||||
|
rawValues = append(rawValues, request.URL.Query()["user_id"]...)
|
||||||
|
seen := make(map[int64]bool)
|
||||||
|
result := make([]int64, 0, len(rawValues))
|
||||||
|
for _, raw := range rawValues {
|
||||||
|
for piece := range strings.SplitSeq(raw, ",") {
|
||||||
|
piece = strings.TrimSpace(piece)
|
||||||
|
if piece == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userID, err := strconv.ParseInt(piece, 10, 64)
|
||||||
|
if err != nil || userID <= 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if seen[userID] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[userID] = true
|
||||||
|
result = append(result, userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, len(result) > 0
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) roomDisplayProfiles(request *http.Request, userIDs []int64) []roomDisplayProfileData {
|
func (h *Handler) roomDisplayProfiles(request *http.Request, userIDs []int64) []roomDisplayProfileData {
|
||||||
profileMap := h.roomDisplayProfileMap(request, userIDs)
|
profileMap := h.roomDisplayProfileMap(request, userIDs)
|
||||||
profiles := make([]roomDisplayProfileData, 0, len(userIDs))
|
profiles := make([]roomDisplayProfileData, 0, len(userIDs))
|
||||||
@ -1025,7 +1128,7 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserIds: userIDs,
|
UserIds: userIDs,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package roomapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -29,6 +29,7 @@ type roomListItemData struct {
|
|||||||
CoverURL string `json:"cover_url,omitempty"`
|
CoverURL string `json:"cover_url,omitempty"`
|
||||||
Mode string `json:"mode,omitempty"`
|
Mode string `json:"mode,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
|
Locked bool `json:"locked"`
|
||||||
Heat int64 `json:"heat"`
|
Heat int64 `json:"heat"`
|
||||||
OnlineCount int32 `json:"online_count"`
|
OnlineCount int32 `json:"online_count"`
|
||||||
SeatCount int32 `json:"seat_count"`
|
SeatCount int32 `json:"seat_count"`
|
||||||
@ -53,6 +54,7 @@ type joinRoomData struct {
|
|||||||
type roomDetailData struct {
|
type roomDetailData struct {
|
||||||
Room roomInitialData `json:"room"`
|
Room roomInitialData `json:"room"`
|
||||||
Viewer roomViewerData `json:"viewer"`
|
Viewer roomViewerData `json:"viewer"`
|
||||||
|
IsFollowed bool `json:"is_followed"`
|
||||||
Seats []roomSeatData `json:"seats"`
|
Seats []roomSeatData `json:"seats"`
|
||||||
RankTop []roomRankItemData `json:"rank_top"`
|
RankTop []roomRankItemData `json:"rank_top"`
|
||||||
OnlineCount int32 `json:"online_count"`
|
OnlineCount int32 `json:"online_count"`
|
||||||
@ -137,9 +139,17 @@ type roomSnapshotData struct {
|
|||||||
Room roomInitialData `json:"room"`
|
Room roomInitialData `json:"room"`
|
||||||
Seats []roomSeatData `json:"seats"`
|
Seats []roomSeatData `json:"seats"`
|
||||||
ContributionRank []roomRankItemData `json:"contribution_rank"`
|
ContributionRank []roomRankItemData `json:"contribution_rank"`
|
||||||
|
IsFollowed bool `json:"is_followed"`
|
||||||
ServerTimeMS int64 `json:"server_time_ms"`
|
ServerTimeMS int64 `json:"server_time_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type roomFollowData struct {
|
||||||
|
RoomID string `json:"room_id"`
|
||||||
|
IsFollowed bool `json:"is_followed"`
|
||||||
|
FollowedAtMS int64 `json:"followed_at_ms,omitempty"`
|
||||||
|
ServerTimeMS int64 `json:"server_time_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
type roomCommandResultData struct {
|
type roomCommandResultData struct {
|
||||||
Applied bool `json:"applied"`
|
Applied bool `json:"applied"`
|
||||||
RoomVersion int64 `json:"room_version"`
|
RoomVersion int64 `json:"room_version"`
|
||||||
@ -157,6 +167,7 @@ type roomInitialData struct {
|
|||||||
Mode string `json:"mode,omitempty"`
|
Mode string `json:"mode,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
ChatEnabled bool `json:"chat_enabled"`
|
ChatEnabled bool `json:"chat_enabled"`
|
||||||
|
Locked bool `json:"locked"`
|
||||||
Heat int64 `json:"heat"`
|
Heat int64 `json:"heat"`
|
||||||
OnlineCount int32 `json:"online_count"`
|
OnlineCount int32 `json:"online_count"`
|
||||||
SeatCount int32 `json:"seat_count"`
|
SeatCount int32 `json:"seat_count"`
|
||||||
@ -244,6 +255,7 @@ func roomListItemDataFromProto(room *roomv1.RoomListItem) roomListItemData {
|
|||||||
CoverURL: room.GetCoverUrl(),
|
CoverURL: room.GetCoverUrl(),
|
||||||
Mode: room.GetMode(),
|
Mode: room.GetMode(),
|
||||||
Status: room.GetStatus(),
|
Status: room.GetStatus(),
|
||||||
|
Locked: room.GetLocked(),
|
||||||
Heat: room.GetHeat(),
|
Heat: room.GetHeat(),
|
||||||
OnlineCount: room.GetOnlineCount(),
|
OnlineCount: room.GetOnlineCount(),
|
||||||
SeatCount: room.GetSeatCount(),
|
SeatCount: room.GetSeatCount(),
|
||||||
@ -287,6 +299,7 @@ func roomSnapshotDataFromProto(resp *roomv1.GetRoomSnapshotResponse) roomSnapsho
|
|||||||
Room: roomInitialRoomDataFromSnapshot(snapshot, snapshot.GetRoomId()),
|
Room: roomInitialRoomDataFromSnapshot(snapshot, snapshot.GetRoomId()),
|
||||||
Seats: roomSeatDataFromSnapshot(snapshot),
|
Seats: roomSeatDataFromSnapshot(snapshot),
|
||||||
ContributionRank: roomRankDataFromSnapshot(snapshot, 0),
|
ContributionRank: roomRankDataFromSnapshot(snapshot, 0),
|
||||||
|
IsFollowed: resp.GetIsFollowed(),
|
||||||
ServerTimeMS: resp.GetServerTimeMs(),
|
ServerTimeMS: resp.GetServerTimeMs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,6 +328,7 @@ func roomInitialRoomDataFromSnapshot(snapshot *roomv1.RoomSnapshot, roomID strin
|
|||||||
Mode: snapshot.GetMode(),
|
Mode: snapshot.GetMode(),
|
||||||
Status: snapshot.GetStatus(),
|
Status: snapshot.GetStatus(),
|
||||||
ChatEnabled: snapshot.GetChatEnabled(),
|
ChatEnabled: snapshot.GetChatEnabled(),
|
||||||
|
Locked: snapshot.GetLocked(),
|
||||||
Heat: snapshot.GetHeat(),
|
Heat: snapshot.GetHeat(),
|
||||||
OnlineCount: int32(len(snapshot.GetOnlineUsers())),
|
OnlineCount: int32(len(snapshot.GetOnlineUsers())),
|
||||||
SeatCount: seatCount,
|
SeatCount: seatCount,
|
||||||
@ -364,6 +378,7 @@ func roomDetailDataFromSnapshot(resp *roomv1.GetRoomSnapshotResponse, viewerUser
|
|||||||
return roomDetailData{
|
return roomDetailData{
|
||||||
Room: roomInitialRoomDataFromSnapshot(snapshot, roomID),
|
Room: roomInitialRoomDataFromSnapshot(snapshot, roomID),
|
||||||
Viewer: roomViewerDataFromProto(viewer, viewerUserID),
|
Viewer: roomViewerDataFromProto(viewer, viewerUserID),
|
||||||
|
IsFollowed: resp.GetIsFollowed(),
|
||||||
Seats: roomSeatDataFromSnapshot(snapshot),
|
Seats: roomSeatDataFromSnapshot(snapshot),
|
||||||
RankTop: roomRankDataFromSnapshot(snapshot, joinRoomContributionRankLimit),
|
RankTop: roomRankDataFromSnapshot(snapshot, joinRoomContributionRankLimit),
|
||||||
OnlineCount: int32(len(snapshot.GetOnlineUsers())),
|
OnlineCount: int32(len(snapshot.GetOnlineUsers())),
|
||||||
@ -375,6 +390,29 @@ func roomDetailDataFromSnapshot(resp *roomv1.GetRoomSnapshotResponse, viewerUser
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func roomFollowDataFromProto(resp *roomv1.FollowRoomResponse) roomFollowData {
|
||||||
|
if resp == nil {
|
||||||
|
return roomFollowData{}
|
||||||
|
}
|
||||||
|
return roomFollowData{
|
||||||
|
RoomID: resp.GetRoomId(),
|
||||||
|
IsFollowed: resp.GetFollowed(),
|
||||||
|
FollowedAtMS: resp.GetFollowedAtMs(),
|
||||||
|
ServerTimeMS: resp.GetServerTimeMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomUnfollowDataFromProto(resp *roomv1.UnfollowRoomResponse) roomFollowData {
|
||||||
|
if resp == nil {
|
||||||
|
return roomFollowData{}
|
||||||
|
}
|
||||||
|
return roomFollowData{
|
||||||
|
RoomID: resp.GetRoomId(),
|
||||||
|
IsFollowed: resp.GetFollowed(),
|
||||||
|
ServerTimeMS: resp.GetServerTimeMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func roomPermissionsFromSnapshot(snapshot *roomv1.RoomSnapshot, viewerUserID int64) roomPermissionsData {
|
func roomPermissionsFromSnapshot(snapshot *roomv1.RoomSnapshot, viewerUserID int64) roomPermissionsData {
|
||||||
if snapshot == nil || viewerUserID <= 0 {
|
if snapshot == nil || viewerUserID <= 0 {
|
||||||
return roomPermissionsData{}
|
return roomPermissionsData{}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package roomapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
@ -21,7 +21,7 @@ func (h *Handler) issueTencentRTCToken(writer http.ResponseWriter, request *http
|
|||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !roomid.ValidStringID(body.RoomID) {
|
if !roomid.ValidStringID(body.RoomID) {
|
||||||
@ -105,24 +105,17 @@ func (h *Handler) writeRTCPresenceDenied(writer http.ResponseWriter, request *ht
|
|||||||
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeNow = defaultTimeNow
|
var TimeNow = defaultTimeNow
|
||||||
|
|
||||||
func defaultTimeNow() time.Time {
|
func defaultTimeNow() time.Time {
|
||||||
return time.Now().UTC()
|
return time.Now().UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) tencentRTCTokenConfig() tencentrtc.TokenConfig {
|
func (h *Handler) tencentRTCTokenConfig() tencentrtc.TokenConfig {
|
||||||
return tencentrtc.TokenConfig{
|
return h.rtcTokenConfig
|
||||||
Enabled: h.tencentRTC.Enabled,
|
|
||||||
SDKAppID: h.tencentRTC.SDKAppID,
|
|
||||||
SecretKey: h.tencentRTC.SecretKey,
|
|
||||||
TTL: h.tencentRTC.UserSigTTL,
|
|
||||||
RoomIDType: h.tencentRTC.RoomIDType,
|
|
||||||
AppScene: h.tencentRTC.AppScene,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) generateTencentRTCToken(userID int64, roomID string) (tencentrtc.TokenResult, error) {
|
func (h *Handler) generateTencentRTCToken(userID int64, roomID string) (tencentrtc.TokenResult, error) {
|
||||||
// JoinRoom 内嵌签发和独立 /rtc/token 使用同一套签名策略,避免两条入口出现 RTC 身份漂移。
|
// JoinRoom 内嵌签发和独立 /rtc/token 使用同一套签名策略,避免两条入口出现 RTC 身份漂移。
|
||||||
return tencentrtc.GenerateToken(h.tencentRTCTokenConfig(), userID, roomID, timeNow())
|
return tencentrtc.GenerateToken(h.tencentRTCTokenConfig(), userID, roomID, TimeNow())
|
||||||
}
|
}
|
||||||
@ -3,13 +3,102 @@ package http
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"hyapp/pkg/tencentrtc"
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/activityapi"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/appapi"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/callbackapi"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/gameapi"
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/managerapi"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/messageapi"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/resourceapi"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/roomapi"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/userapi"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/walletapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Routes 把 Handler 的私有方法适配成独立路由包需要的显式 handler 表。
|
// Routes 把 Handler 的私有方法适配成独立路由包需要的显式 handler 表。
|
||||||
// 路径注册集中在 httproutes,gateway 依赖装配和鉴权 wrapper 仍留在 transport 入口层。
|
// 路径注册集中在 httproutes,gateway 依赖装配和鉴权 wrapper 仍留在 transport 入口层。
|
||||||
func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
||||||
|
roomAPI := roomapi.New(roomapi.Config{
|
||||||
|
RoomClient: h.roomClient,
|
||||||
|
RoomGuardClient: h.roomGuardClient,
|
||||||
|
RoomQueryClient: h.roomQueryClient,
|
||||||
|
UserProfileClient: h.userProfileClient,
|
||||||
|
UserSocialClient: h.userSocialClient,
|
||||||
|
WalletClient: h.walletClient,
|
||||||
|
RTCTokenConfig: tencentrtc.TokenConfig{
|
||||||
|
Enabled: h.tencentRTC.Enabled,
|
||||||
|
SDKAppID: h.tencentRTC.SDKAppID,
|
||||||
|
SecretKey: h.tencentRTC.SecretKey,
|
||||||
|
TTL: h.tencentRTC.UserSigTTL,
|
||||||
|
RoomIDType: h.tencentRTC.RoomIDType,
|
||||||
|
AppScene: h.tencentRTC.AppScene,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
walletAPI := walletapi.New(walletapi.Config{
|
||||||
|
WalletClient: h.walletClient,
|
||||||
|
UserIdentityClient: h.userIdentityClient,
|
||||||
|
UserProfileClient: h.userProfileClient,
|
||||||
|
UserHostClient: h.userHostClient,
|
||||||
|
})
|
||||||
|
userAPI := userapi.New(userapi.Config{
|
||||||
|
UserIdentityClient: h.userIdentityClient,
|
||||||
|
UserProfileClient: h.userProfileClient,
|
||||||
|
UserSocialClient: h.userSocialClient,
|
||||||
|
UserCountryClient: h.userCountryClient,
|
||||||
|
UserHostClient: h.userHostClient,
|
||||||
|
WalletClient: h.walletClient,
|
||||||
|
BroadcastClient: h.broadcastClient,
|
||||||
|
})
|
||||||
|
messageAPI := messageapi.New(messageapi.Config{
|
||||||
|
MessageClient: h.messageClient,
|
||||||
|
UserProfileClient: h.userProfileClient,
|
||||||
|
})
|
||||||
|
activityAPI := activityapi.New(activityapi.Config{
|
||||||
|
TaskClient: h.taskClient,
|
||||||
|
RegistrationReward: h.registrationReward,
|
||||||
|
})
|
||||||
|
gameAPI := gameapi.New(gameapi.Config{
|
||||||
|
GameClient: h.gameClient,
|
||||||
|
UserProfileClient: h.userProfileClient,
|
||||||
|
})
|
||||||
|
resourceAPI := resourceapi.New(resourceapi.Config{
|
||||||
|
WalletClient: h.walletClient,
|
||||||
|
})
|
||||||
|
managerAPI := managerapi.New(managerapi.Config{
|
||||||
|
WalletClient: h.walletClient,
|
||||||
|
UserHostClient: h.userHostClient,
|
||||||
|
UserProfileClient: h.userProfileClient,
|
||||||
|
})
|
||||||
|
appAPI := appapi.New(appapi.Config{
|
||||||
|
AppConfigReader: h.appConfigReader,
|
||||||
|
UserDeviceClient: h.userDeviceClient,
|
||||||
|
ObjectUploader: h.objectUploader,
|
||||||
|
})
|
||||||
|
callbackAPI := callbackapi.New(callbackapi.Config{
|
||||||
|
RoomClient: h.roomClient,
|
||||||
|
RoomGuardClient: h.roomGuardClient,
|
||||||
|
UserProfileClient: h.userProfileClient,
|
||||||
|
TencentIM: callbackapi.TencentIMConfig{
|
||||||
|
SDKAppID: h.tencentIM.SDKAppID,
|
||||||
|
AdminIdentifier: h.tencentIM.AdminIdentifier,
|
||||||
|
CallbackAuthToken: h.tencentIM.CallbackAuthToken,
|
||||||
|
},
|
||||||
|
TencentRTC: callbackapi.TencentRTCConfig{
|
||||||
|
SDKAppID: h.tencentRTC.SDKAppID,
|
||||||
|
CallbackSignKey: h.tencentRTC.CallbackSignKey,
|
||||||
|
UserSigTTL: h.tencentRTC.UserSigTTL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
userHandlers := userAPI.UserHandlers()
|
||||||
|
userHandlers.BatchUserProfiles = messageAPI.BatchUserProfiles
|
||||||
|
userHandlers.BatchRoomDisplayProfiles = roomAPI.BatchRoomDisplayProfiles
|
||||||
|
userHandlers.GetMyGiftWall = walletAPI.GetMyGiftWall
|
||||||
|
userHandlers.ListMyResources = resourceAPI.ListMyResources
|
||||||
|
userHandlers.EquipMyResource = resourceAPI.EquipMyResource
|
||||||
|
|
||||||
return httproutes.New(httproutes.Config{
|
return httproutes.New(httproutes.Config{
|
||||||
PublicWrap: h.publicAPIHandler,
|
PublicWrap: h.publicAPIHandler,
|
||||||
AuthWrap: func(handler http.HandlerFunc) http.Handler {
|
AuthWrap: func(handler http.HandlerFunc) http.Handler {
|
||||||
@ -26,109 +115,29 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
|||||||
Logout: h.logout,
|
Logout: h.logout,
|
||||||
},
|
},
|
||||||
App: httproutes.AppHandlers{
|
App: httproutes.AppHandlers{
|
||||||
ListRegistrationCountries: h.listRegistrationCountries,
|
ListRegistrationCountries: userAPI.ListRegistrationCountries,
|
||||||
GetAppBootstrap: h.getAppBootstrap,
|
GetAppBootstrap: appAPI.GetAppBootstrap,
|
||||||
ListH5Links: h.listH5Links,
|
ListH5Links: appAPI.ListH5Links,
|
||||||
ListAppBanners: h.listAppBanners,
|
ListAppBanners: appAPI.ListAppBanners,
|
||||||
GetAppVersion: h.getAppVersion,
|
GetAppVersion: appAPI.GetAppVersion,
|
||||||
ListResources: h.listResources,
|
ListResources: resourceAPI.ListResources,
|
||||||
GetResourceGroup: h.getResourceGroup,
|
GetResourceGroup: resourceAPI.GetResourceGroup,
|
||||||
ListGifts: h.listGifts,
|
ListGifts: resourceAPI.ListGifts,
|
||||||
IssueTencentIMUserSig: h.issueTencentIMUserSig,
|
IssueTencentIMUserSig: h.issueTencentIMUserSig,
|
||||||
IssueTencentRTCToken: h.issueTencentRTCToken,
|
IssueTencentRTCToken: roomAPI.IssueTencentRTCToken,
|
||||||
HandleTencentIMCallback: h.handleTencentIMCallback,
|
HandleTencentIMCallback: callbackAPI.HandleTencentIMCallback,
|
||||||
HandleTencentRTCCallback: h.handleTencentRTCCallback,
|
HandleTencentRTCCallback: callbackAPI.HandleTencentRTCCallback,
|
||||||
UploadFile: h.uploadFile,
|
UploadFile: appAPI.UploadFile,
|
||||||
UploadAvatar: h.uploadAvatar,
|
UploadAvatar: appAPI.UploadAvatar,
|
||||||
HandlePushToken: h.handlePushToken,
|
HandlePushToken: appAPI.HandlePushToken,
|
||||||
},
|
|
||||||
User: httproutes.UserHandlers{
|
|
||||||
ResolveDisplayUserID: h.resolveDisplayUserID,
|
|
||||||
BatchUserProfiles: h.batchUserProfiles,
|
|
||||||
BatchRoomDisplayProfiles: h.batchRoomDisplayProfiles,
|
|
||||||
GetMyOverview: h.getMyOverview,
|
|
||||||
GetMyGiftWall: h.getMyGiftWall,
|
|
||||||
GetMyIdentity: h.getMyIdentity,
|
|
||||||
GetMyHostIdentity: h.getMyHostIdentity,
|
|
||||||
GetMyRoleSummary: h.getMyRoleSummary,
|
|
||||||
CompleteMyOnboarding: h.completeMyOnboarding,
|
|
||||||
UpdateMyProfile: h.updateMyProfile,
|
|
||||||
ChangeMyCountry: h.changeMyCountry,
|
|
||||||
ChangeMyDisplayUserID: h.changeMyDisplayUserID,
|
|
||||||
ApplyMyPrettyDisplayUserID: h.applyMyPrettyDisplayUserID,
|
|
||||||
ListMyProfileVisitors: h.listMyProfileVisitors,
|
|
||||||
ListMyFollowing: h.listMyFollowing,
|
|
||||||
ListMyFriends: h.listMyFriends,
|
|
||||||
ListMyFriendApplications: h.listMyFriendApplications,
|
|
||||||
GetMyProfile: h.getMyProfile,
|
|
||||||
ListMyResources: h.listMyResources,
|
|
||||||
EquipMyResource: h.equipMyResource,
|
|
||||||
UserSocialAction: h.userSocialAction,
|
|
||||||
},
|
|
||||||
Manager: httproutes.ManagerHandlers{
|
|
||||||
ListManagerGrantResources: h.listManagerGrantResources,
|
|
||||||
GrantManagerResource: h.grantManagerResource,
|
|
||||||
LookupBusinessUsers: h.lookupBusinessUsers,
|
|
||||||
},
|
|
||||||
Room: httproutes.RoomHandlers{
|
|
||||||
GetMyRoom: h.getMyRoom,
|
|
||||||
ListRoomFeeds: h.listRoomFeeds,
|
|
||||||
ListRooms: h.listRooms,
|
|
||||||
GetCurrentRoom: h.getCurrentRoom,
|
|
||||||
GetRoomSnapshot: h.getRoomSnapshot,
|
|
||||||
GetRoomDetail: h.getRoomDetail,
|
|
||||||
ListRoomOnlineUsers: h.listRoomOnlineUsers,
|
|
||||||
GetRoomGiftPanel: h.getRoomGiftPanel,
|
|
||||||
CreateRoom: h.createRoom,
|
|
||||||
UpdateRoomProfile: h.updateRoomProfile,
|
|
||||||
JoinRoom: h.joinRoom,
|
|
||||||
RoomHeartbeat: h.roomHeartbeat,
|
|
||||||
LeaveRoom: h.leaveRoom,
|
|
||||||
CloseRoom: h.closeRoom,
|
|
||||||
MicUp: h.micUp,
|
|
||||||
MicDown: h.micDown,
|
|
||||||
ChangeMicSeat: h.changeMicSeat,
|
|
||||||
ConfirmMicPublishing: h.confirmMicPublishing,
|
|
||||||
SetMicMute: h.setMicMute,
|
|
||||||
SetMicSeatLock: h.setMicSeatLock,
|
|
||||||
SetChatEnabled: h.setChatEnabled,
|
|
||||||
SetRoomAdmin: h.setRoomAdmin,
|
|
||||||
MuteUser: h.muteUser,
|
|
||||||
KickUser: h.kickUser,
|
|
||||||
UnbanUser: h.unbanUser,
|
|
||||||
SendGift: h.sendGift,
|
|
||||||
},
|
|
||||||
Message: httproutes.MessageHandlers{
|
|
||||||
ListMessageTabs: h.listMessageTabs,
|
|
||||||
MarkInboxSectionRead: h.markInboxSectionRead,
|
|
||||||
MarkInboxMessageReadPath: h.markInboxMessageReadByPath,
|
|
||||||
DeleteInboxMessagePath: h.deleteInboxMessageByPath,
|
|
||||||
ListInboxMessages: h.listInboxMessages,
|
|
||||||
},
|
|
||||||
Task: httproutes.TaskHandlers{
|
|
||||||
ListTaskTabs: h.listTaskTabs,
|
|
||||||
ClaimTaskReward: h.claimTaskReward,
|
|
||||||
GetRegistrationRewardEligibility: h.getRegistrationRewardEligibility,
|
|
||||||
},
|
|
||||||
Wallet: httproutes.WalletHandlers{
|
|
||||||
GetWalletOverview: h.getWalletOverview,
|
|
||||||
GetMyBalances: h.getMyBalances,
|
|
||||||
ListRechargeProducts: h.listRechargeProducts,
|
|
||||||
GetDiamondExchangeConfig: h.getDiamondExchangeConfig,
|
|
||||||
ApplyWithdrawal: h.applyWithdrawal,
|
|
||||||
ListCoinTransactions: h.listCoinTransactions,
|
|
||||||
ListWalletTransactions: h.listWalletTransactions,
|
|
||||||
TransferCoinFromSeller: h.transferCoinFromSeller,
|
|
||||||
},
|
|
||||||
VIP: httproutes.VIPHandlers{
|
|
||||||
GetMyVIP: h.getMyVIP,
|
|
||||||
ListVIPPackages: h.listVIPPackages,
|
|
||||||
PurchaseVIP: h.purchaseVIP,
|
|
||||||
},
|
|
||||||
Game: httproutes.GameHandlers{
|
|
||||||
ListGames: h.listGames,
|
|
||||||
LaunchGame: h.launchGame,
|
|
||||||
HandleCallback: h.handleGameCallback,
|
|
||||||
},
|
},
|
||||||
|
User: userHandlers,
|
||||||
|
Manager: managerAPI.ManagerHandlers(),
|
||||||
|
Room: roomAPI.RoomHandlers(),
|
||||||
|
Message: messageAPI.MessageHandlers(),
|
||||||
|
Task: activityAPI.TaskHandlers(),
|
||||||
|
Wallet: walletAPI.WalletHandlers(),
|
||||||
|
VIP: walletAPI.VIPHandlers(),
|
||||||
|
Game: gameAPI.Handlers(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
package userapi
|
||||||
|
|
||||||
|
import userv1 "hyapp.local/api/proto/user/v1"
|
||||||
|
|
||||||
|
// authTokenData 是完整资料补齐后返回给客户端替换 access token 的扁平格式。
|
||||||
|
// 字段保持和登录接口一致,避免客户端在 onboarding 后解析两套 token 结构。
|
||||||
|
type authTokenData struct {
|
||||||
|
AppCode string `json:"app_code"`
|
||||||
|
UserID string `json:"user_id,omitempty"`
|
||||||
|
DisplayUserID string `json:"display_user_id,omitempty"`
|
||||||
|
DefaultDisplayUserID string `json:"default_display_user_id,omitempty"`
|
||||||
|
DisplayUserIDKind string `json:"display_user_id_kind,omitempty"`
|
||||||
|
DisplayUserIDExpiresAtMs int64 `json:"display_user_id_expires_at_ms"`
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
|
ExpiresInSec int64 `json:"expires_in_sec"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
IsNewUser *bool `json:"is_new_user,omitempty"`
|
||||||
|
ProfileCompleted bool `json:"profile_completed"`
|
||||||
|
OnboardingStatus string `json:"onboarding_status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func authData(token *userv1.AuthToken, isNewUser *bool) authTokenData {
|
||||||
|
if token == nil {
|
||||||
|
return authTokenData{IsNewUser: isNewUser}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authTokenData{
|
||||||
|
UserID: userIDString(token.GetUserId()),
|
||||||
|
AppCode: token.GetAppCode(),
|
||||||
|
DisplayUserID: token.GetDisplayUserId(),
|
||||||
|
DefaultDisplayUserID: token.GetDefaultDisplayUserId(),
|
||||||
|
DisplayUserIDKind: token.GetDisplayUserIdKind(),
|
||||||
|
DisplayUserIDExpiresAtMs: token.GetDisplayUserIdExpiresAtMs(),
|
||||||
|
SessionID: token.GetSessionId(),
|
||||||
|
AccessToken: token.GetAccessToken(),
|
||||||
|
RefreshToken: token.GetRefreshToken(),
|
||||||
|
ExpiresInSec: token.GetExpiresInSec(),
|
||||||
|
TokenType: token.GetTokenType(),
|
||||||
|
IsNewUser: isNewUser,
|
||||||
|
ProfileCompleted: token.GetProfileCompleted(),
|
||||||
|
OnboardingStatus: token.GetOnboardingStatus(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
package userapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"hyapp/services/gateway-service/internal/client"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler owns user-facing HTTP endpoints and relationship endpoints.
|
||||||
|
// It delegates user facts to user-service and only translates HTTP protocol shape.
|
||||||
|
type Handler struct {
|
||||||
|
userIdentityClient client.UserIdentityClient
|
||||||
|
userProfileClient client.UserProfileClient
|
||||||
|
userSocialClient client.UserSocialClient
|
||||||
|
userCountryClient client.UserCountryQueryClient
|
||||||
|
userHostClient client.UserHostClient
|
||||||
|
walletClient client.WalletClient
|
||||||
|
broadcastClient client.BroadcastClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
UserIdentityClient client.UserIdentityClient
|
||||||
|
UserProfileClient client.UserProfileClient
|
||||||
|
UserSocialClient client.UserSocialClient
|
||||||
|
UserCountryClient client.UserCountryQueryClient
|
||||||
|
UserHostClient client.UserHostClient
|
||||||
|
WalletClient client.WalletClient
|
||||||
|
BroadcastClient client.BroadcastClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
userIdentityClient: config.UserIdentityClient,
|
||||||
|
userProfileClient: config.UserProfileClient,
|
||||||
|
userSocialClient: config.UserSocialClient,
|
||||||
|
userCountryClient: config.UserCountryClient,
|
||||||
|
userHostClient: config.UserHostClient,
|
||||||
|
walletClient: config.WalletClient,
|
||||||
|
broadcastClient: config.BroadcastClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
||||||
|
return httproutes.UserHandlers{
|
||||||
|
ResolveDisplayUserID: h.resolveDisplayUserID,
|
||||||
|
GetMyOverview: h.getMyOverview,
|
||||||
|
GetMyIdentity: h.getMyIdentity,
|
||||||
|
GetMyHostIdentity: h.getMyHostIdentity,
|
||||||
|
GetMyRoleSummary: h.getMyRoleSummary,
|
||||||
|
CompleteMyOnboarding: h.completeMyOnboarding,
|
||||||
|
UpdateMyProfile: h.updateMyProfile,
|
||||||
|
ChangeMyCountry: h.changeMyCountry,
|
||||||
|
ChangeMyDisplayUserID: h.changeMyDisplayUserID,
|
||||||
|
ApplyMyPrettyDisplayUserID: h.applyMyPrettyDisplayUserID,
|
||||||
|
ListMyProfileVisitors: h.listMyProfileVisitors,
|
||||||
|
ListMyFollowing: h.listMyFollowing,
|
||||||
|
ListMyFriends: h.listMyFriends,
|
||||||
|
ListMyFriendApplications: h.listMyFriendApplications,
|
||||||
|
GetMyProfile: h.getMyProfile,
|
||||||
|
UserSocialAction: h.userSocialAction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListRegistrationCountries(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.listRegistrationCountries(writer, request)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package userapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
@ -37,7 +37,7 @@ func (h *Handler) getMyHostIdentity(writer http.ResponseWriter, request *http.Re
|
|||||||
identity := userHostIdentityData{}
|
identity := userHostIdentityData{}
|
||||||
|
|
||||||
hostResp, err := h.userHostClient.GetHostProfile(request.Context(), &userv1.GetHostProfileRequest{
|
hostResp, err := h.userHostClient.GetHostProfile(request.Context(), &userv1.GetHostProfileRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -50,7 +50,7 @@ func (h *Handler) getMyHostIdentity(writer http.ResponseWriter, request *http.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
bdResp, err := h.userHostClient.GetBDProfile(request.Context(), &userv1.GetBDProfileRequest{
|
bdResp, err := h.userHostClient.GetBDProfile(request.Context(), &userv1.GetBDProfileRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -63,7 +63,7 @@ func (h *Handler) getMyHostIdentity(writer http.ResponseWriter, request *http.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
coinSellerResp, err := h.userHostClient.GetCoinSellerProfile(request.Context(), &userv1.GetCoinSellerProfileRequest{
|
coinSellerResp, err := h.userHostClient.GetCoinSellerProfile(request.Context(), &userv1.GetCoinSellerProfileRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -85,7 +85,7 @@ func (h *Handler) getMyRoleSummary(writer http.ResponseWriter, request *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userHostClient.GetUserRoleSummary(request.Context(), &userv1.GetUserRoleSummaryRequest{
|
resp, err := h.userHostClient.GetUserRoleSummary(request.Context(), &userv1.GetUserRoleSummaryRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package userapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -91,7 +91,7 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
|
|||||||
var profileErr error
|
var profileErr error
|
||||||
wg.Go(func() {
|
wg.Go(func() {
|
||||||
profileResp, profileErr = h.userProfileClient.GetUser(ctx, &userv1.GetUserRequest{
|
profileResp, profileErr = h.userProfileClient.GetUser(ctx, &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
Meta: httpkit.UserMeta(request.WithContext(ctx), ""),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -134,7 +134,7 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
|
|||||||
var statsErr error
|
var statsErr error
|
||||||
wg.Go(func() {
|
wg.Go(func() {
|
||||||
statsResp, statsErr = h.userProfileClient.GetMyProfileStats(ctx, &userv1.GetMyProfileStatsRequest{
|
statsResp, statsErr = h.userProfileClient.GetMyProfileStats(ctx, &userv1.GetMyProfileStatsRequest{
|
||||||
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
Meta: httpkit.UserMeta(request.WithContext(ctx), ""),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -173,7 +173,7 @@ func (h *Handler) getOverviewRoles(ctx context.Context, request *http.Request, u
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.userHostClient.GetUserRoleSummary(ctx, &userv1.GetUserRoleSummaryRequest{
|
resp, err := h.userHostClient.GetUserRoleSummary(ctx, &userv1.GetUserRoleSummaryRequest{
|
||||||
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
Meta: httpkit.UserMeta(request.WithContext(ctx), ""),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package userapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
@ -48,7 +48,7 @@ func (h *Handler) listMyProfileVisitors(writer http.ResponseWriter, request *htt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.ListProfileVisitors(request.Context(), &userv1.ListProfileVisitorsRequest{
|
resp, err := h.userSocialClient.ListProfileVisitors(request.Context(), &userv1.ListProfileVisitorsRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
@ -76,7 +76,7 @@ func (h *Handler) listMyFollowing(writer http.ResponseWriter, request *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{
|
resp, err := h.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
@ -104,7 +104,7 @@ func (h *Handler) listMyFriends(writer http.ResponseWriter, request *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{
|
resp, err := h.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
@ -132,7 +132,7 @@ func (h *Handler) listMyFriendApplications(writer http.ResponseWriter, request *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.ListFriendApplications(request.Context(), &userv1.ListFriendApplicationsRequest{
|
resp, err := h.userSocialClient.ListFriendApplications(request.Context(), &userv1.ListFriendApplicationsRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
Direction: strings.TrimSpace(request.URL.Query().Get("direction")),
|
Direction: strings.TrimSpace(request.URL.Query().Get("direction")),
|
||||||
Page: page,
|
Page: page,
|
||||||
@ -184,7 +184,7 @@ func (h *Handler) recordProfileVisit(writer http.ResponseWriter, request *http.R
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.RecordProfileVisit(request.Context(), &userv1.RecordProfileVisitRequest{
|
resp, err := h.userSocialClient.RecordProfileVisit(request.Context(), &userv1.RecordProfileVisitRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
VisitorUserId: auth.UserIDFromContext(request.Context()),
|
VisitorUserId: auth.UserIDFromContext(request.Context()),
|
||||||
TargetUserId: targetUserID,
|
TargetUserId: targetUserID,
|
||||||
})
|
})
|
||||||
@ -200,7 +200,7 @@ func (h *Handler) handleFollowAction(writer http.ResponseWriter, request *http.R
|
|||||||
switch request.Method {
|
switch request.Method {
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
resp, err := h.userSocialClient.FollowUser(request.Context(), &userv1.FollowUserRequest{
|
resp, err := h.userSocialClient.FollowUser(request.Context(), &userv1.FollowUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
FollowerUserId: userID,
|
FollowerUserId: userID,
|
||||||
FolloweeUserId: followeeUserID,
|
FolloweeUserId: followeeUserID,
|
||||||
})
|
})
|
||||||
@ -211,7 +211,7 @@ func (h *Handler) handleFollowAction(writer http.ResponseWriter, request *http.R
|
|||||||
httpkit.WriteOK(writer, request, map[string]any{"following": resp.GetFollowing(), "follower_stats": overviewStatsFromProto(resp.GetFollowerStats())})
|
httpkit.WriteOK(writer, request, map[string]any{"following": resp.GetFollowing(), "follower_stats": overviewStatsFromProto(resp.GetFollowerStats())})
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
resp, err := h.userSocialClient.UnfollowUser(request.Context(), &userv1.UnfollowUserRequest{
|
resp, err := h.userSocialClient.UnfollowUser(request.Context(), &userv1.UnfollowUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
FollowerUserId: userID,
|
FollowerUserId: userID,
|
||||||
FolloweeUserId: followeeUserID,
|
FolloweeUserId: followeeUserID,
|
||||||
})
|
})
|
||||||
@ -231,7 +231,7 @@ func (h *Handler) applyFriend(writer http.ResponseWriter, request *http.Request,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.ApplyFriend(request.Context(), &userv1.ApplyFriendRequest{
|
resp, err := h.userSocialClient.ApplyFriend(request.Context(), &userv1.ApplyFriendRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
RequesterUserId: auth.UserIDFromContext(request.Context()),
|
RequesterUserId: auth.UserIDFromContext(request.Context()),
|
||||||
TargetUserId: targetUserID,
|
TargetUserId: targetUserID,
|
||||||
})
|
})
|
||||||
@ -248,7 +248,7 @@ func (h *Handler) acceptFriendApplication(writer http.ResponseWriter, request *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.AcceptFriendApplication(request.Context(), &userv1.AcceptFriendApplicationRequest{
|
resp, err := h.userSocialClient.AcceptFriendApplication(request.Context(), &userv1.AcceptFriendApplicationRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
AccepterUserId: auth.UserIDFromContext(request.Context()),
|
AccepterUserId: auth.UserIDFromContext(request.Context()),
|
||||||
RequesterUserId: requesterUserID,
|
RequesterUserId: requesterUserID,
|
||||||
})
|
})
|
||||||
@ -265,7 +265,7 @@ func (h *Handler) deleteFriend(writer http.ResponseWriter, request *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userSocialClient.DeleteFriend(request.Context(), &userv1.DeleteFriendRequest{
|
resp, err := h.userSocialClient.DeleteFriend(request.Context(), &userv1.DeleteFriendRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
FriendUserId: friendUserID,
|
FriendUserId: friendUserID,
|
||||||
})
|
})
|
||||||
@ -277,11 +277,11 @@ func (h *Handler) deleteFriend(writer http.ResponseWriter, request *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
func socialPage(request *http.Request) (int32, int32, bool) {
|
func socialPage(request *http.Request) (int32, int32, bool) {
|
||||||
page, ok := parsePositiveInt32Query(request, "page", 1)
|
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
|
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package http
|
package userapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -11,6 +10,7 @@ import (
|
|||||||
userv1 "hyapp.local/api/proto/user/v1"
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
"hyapp/pkg/logx"
|
"hyapp/pkg/logx"
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// userIdentityData 是 gateway 对外返回的当前短号投影。
|
// userIdentityData 是 gateway 对外返回的当前短号投影。
|
||||||
@ -93,7 +93,7 @@ func (h *Handler) listRegistrationCountries(writer http.ResponseWriter, request
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.userCountryClient.ListRegistrationCountries(request.Context(), &userv1.ListRegistrationCountriesRequest{
|
resp, err := h.userCountryClient.ListRegistrationCountries(request.Context(), &userv1.ListRegistrationCountriesRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
@ -115,7 +115,7 @@ func (h *Handler) getMyIdentity(writer http.ResponseWriter, request *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.userIdentityClient.GetUserIdentity(request.Context(), &userv1.GetUserIdentityRequest{
|
resp, err := h.userIdentityClient.GetUserIdentity(request.Context(), &userv1.GetUserIdentityRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -139,7 +139,7 @@ func (h *Handler) resolveDisplayUserID(writer http.ResponseWriter, request *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.userIdentityClient.ResolveDisplayUserID(request.Context(), &userv1.ResolveDisplayUserIDRequest{
|
resp, err := h.userIdentityClient.ResolveDisplayUserID(request.Context(), &userv1.ResolveDisplayUserIDRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
DisplayUserId: displayUserID,
|
DisplayUserId: displayUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -160,7 +160,7 @@ func (h *Handler) completeMyOnboarding(writer http.ResponseWriter, request *http
|
|||||||
Country string `json:"country"`
|
Country string `json:"country"`
|
||||||
InviteCode string `json:"invite_code"`
|
InviteCode string `json:"invite_code"`
|
||||||
}
|
}
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
@ -169,7 +169,7 @@ func (h *Handler) completeMyOnboarding(writer http.ResponseWriter, request *http
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.userProfileClient.CompleteOnboarding(request.Context(), &userv1.CompleteOnboardingRequest{
|
resp, err := h.userProfileClient.CompleteOnboarding(request.Context(), &userv1.CompleteOnboardingRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
Username: body.Username,
|
Username: body.Username,
|
||||||
Avatar: body.Avatar,
|
Avatar: body.Avatar,
|
||||||
@ -197,7 +197,7 @@ func (h *Handler) getMyProfile(writer http.ResponseWriter, request *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -216,7 +216,7 @@ func (h *Handler) updateMyProfile(writer http.ResponseWriter, request *http.Requ
|
|||||||
Gender *string `json:"gender"`
|
Gender *string `json:"gender"`
|
||||||
Birth *string `json:"birth"`
|
Birth *string `json:"birth"`
|
||||||
}
|
}
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
@ -225,7 +225,7 @@ func (h *Handler) updateMyProfile(writer http.ResponseWriter, request *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.userProfileClient.UpdateUserProfile(request.Context(), &userv1.UpdateUserProfileRequest{
|
resp, err := h.userProfileClient.UpdateUserProfile(request.Context(), &userv1.UpdateUserProfileRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
Username: body.Username,
|
Username: body.Username,
|
||||||
Avatar: body.Avatar,
|
Avatar: body.Avatar,
|
||||||
@ -245,7 +245,7 @@ func (h *Handler) changeMyCountry(writer http.ResponseWriter, request *http.Requ
|
|||||||
var body struct {
|
var body struct {
|
||||||
Country string `json:"country"`
|
Country string `json:"country"`
|
||||||
}
|
}
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
@ -254,7 +254,7 @@ func (h *Handler) changeMyCountry(writer http.ResponseWriter, request *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.userProfileClient.ChangeUserCountry(request.Context(), &userv1.ChangeUserCountryRequest{
|
resp, err := h.userProfileClient.ChangeUserCountry(request.Context(), &userv1.ChangeUserCountryRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
Country: body.Country,
|
Country: body.Country,
|
||||||
})
|
})
|
||||||
@ -272,7 +272,7 @@ func (h *Handler) removeOldRegionBroadcastMemberBestEffort(request *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.broadcastClient.RemoveRegionBroadcastMember(request.Context(), &activityv1.RemoveRegionBroadcastMemberRequest{
|
resp, err := h.broadcastClient.RemoveRegionBroadcastMember(request.Context(), &activityv1.RemoveRegionBroadcastMemberRequest{
|
||||||
Meta: activityMeta(request),
|
Meta: httpkit.ActivityMeta(request),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
RegionId: oldRegionID,
|
RegionId: oldRegionID,
|
||||||
Reason: reason,
|
Reason: reason,
|
||||||
@ -303,7 +303,7 @@ func (h *Handler) changeMyDisplayUserID(writer http.ResponseWriter, request *htt
|
|||||||
NewDisplayUserID string `json:"new_display_user_id"`
|
NewDisplayUserID string `json:"new_display_user_id"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
}
|
}
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userIdentityClient == nil {
|
if h.userIdentityClient == nil {
|
||||||
@ -312,7 +312,7 @@ func (h *Handler) changeMyDisplayUserID(writer http.ResponseWriter, request *htt
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.userIdentityClient.ChangeDisplayUserID(request.Context(), &userv1.ChangeDisplayUserIDRequest{
|
resp, err := h.userIdentityClient.ChangeDisplayUserID(request.Context(), &userv1.ChangeDisplayUserIDRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
NewDisplayUserId: body.NewDisplayUserID,
|
NewDisplayUserId: body.NewDisplayUserID,
|
||||||
Reason: body.Reason,
|
Reason: body.Reason,
|
||||||
@ -333,7 +333,7 @@ func (h *Handler) applyMyPrettyDisplayUserID(writer http.ResponseWriter, request
|
|||||||
LeaseDurationMs int64 `json:"lease_duration_ms"`
|
LeaseDurationMs int64 `json:"lease_duration_ms"`
|
||||||
PaymentReceiptID string `json:"payment_receipt_id"`
|
PaymentReceiptID string `json:"payment_receipt_id"`
|
||||||
}
|
}
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h.userIdentityClient == nil {
|
if h.userIdentityClient == nil {
|
||||||
@ -342,7 +342,7 @@ func (h *Handler) applyMyPrettyDisplayUserID(writer http.ResponseWriter, request
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.userIdentityClient.ApplyPrettyDisplayUserID(request.Context(), &userv1.ApplyPrettyDisplayUserIDRequest{
|
resp, err := h.userIdentityClient.ApplyPrettyDisplayUserID(request.Context(), &userv1.ApplyPrettyDisplayUserIDRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
PrettyDisplayUserId: body.PrettyDisplayUserID,
|
PrettyDisplayUserId: body.PrettyDisplayUserID,
|
||||||
LeaseDurationMs: body.LeaseDurationMs,
|
LeaseDurationMs: body.LeaseDurationMs,
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package walletapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
@ -170,7 +170,7 @@ func (h *Handler) listRechargeProducts(writer http.ResponseWriter, request *http
|
|||||||
}
|
}
|
||||||
userID := auth.UserIDFromContext(request.Context())
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -182,7 +182,7 @@ func (h *Handler) listRechargeProducts(writer http.ResponseWriter, request *http
|
|||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
RegionId: profileResp.GetUser().GetRegionId(),
|
RegionId: profileResp.GetUser().GetRegionId(),
|
||||||
Platform: firstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"),
|
Platform: httpkit.FirstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
@ -250,12 +250,12 @@ func (h *Handler) listWalletTransactionsByAsset(writer http.ResponseWriter, requ
|
|||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page, ok := parsePositiveInt32Query(request, "page", 1)
|
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
|
||||||
if !ok {
|
if !ok {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", defaultPageSize)
|
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", defaultPageSize)
|
||||||
if !ok {
|
if !ok {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
@ -286,7 +286,7 @@ func (h *Handler) applyWithdrawal(writer http.ResponseWriter, request *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body withdrawalApplyRequestBody
|
var body withdrawalApplyRequestBody
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
commandID := strings.TrimSpace(body.CommandID)
|
commandID := strings.TrimSpace(body.CommandID)
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
package walletapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"hyapp/services/gateway-service/internal/client"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler owns wallet-facing HTTP endpoints.
|
||||||
|
// It keeps wallet semantics behind wallet-service and only handles HTTP protocol conversion.
|
||||||
|
type Handler struct {
|
||||||
|
walletClient client.WalletClient
|
||||||
|
userIdentityClient client.UserIdentityClient
|
||||||
|
userProfileClient client.UserProfileClient
|
||||||
|
userHostClient client.UserHostClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
WalletClient client.WalletClient
|
||||||
|
UserIdentityClient client.UserIdentityClient
|
||||||
|
UserProfileClient client.UserProfileClient
|
||||||
|
UserHostClient client.UserHostClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
walletClient: config.WalletClient,
|
||||||
|
userIdentityClient: config.UserIdentityClient,
|
||||||
|
userProfileClient: config.UserProfileClient,
|
||||||
|
userHostClient: config.UserHostClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) WalletHandlers() httproutes.WalletHandlers {
|
||||||
|
return httproutes.WalletHandlers{
|
||||||
|
GetWalletOverview: h.getWalletOverview,
|
||||||
|
GetMyBalances: h.getMyBalances,
|
||||||
|
ListRechargeProducts: h.listRechargeProducts,
|
||||||
|
GetDiamondExchangeConfig: h.getDiamondExchangeConfig,
|
||||||
|
ApplyWithdrawal: h.applyWithdrawal,
|
||||||
|
ListCoinTransactions: h.listCoinTransactions,
|
||||||
|
ListWalletTransactions: h.listWalletTransactions,
|
||||||
|
TransferCoinFromSeller: h.transferCoinFromSeller,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) VIPHandlers() httproutes.VIPHandlers {
|
||||||
|
return httproutes.VIPHandlers{
|
||||||
|
GetMyVIP: h.getMyVIP,
|
||||||
|
ListVIPPackages: h.listVIPPackages,
|
||||||
|
PurchaseVIP: h.purchaseVIP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetMyGiftWall(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.getMyGiftWall(writer, request)
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package walletapi
|
||||||
|
|
||||||
|
import walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
|
||||||
|
type overviewVIPData struct {
|
||||||
|
Level int32 `json:"level"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Active bool `json:"active"`
|
||||||
|
StartedAtMS int64 `json:"started_at_ms"`
|
||||||
|
ExpiresAtMS int64 `json:"expires_at_ms"`
|
||||||
|
Unavailable bool `json:"unavailable,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type resourceData struct {
|
||||||
|
ResourceID int64 `json:"resource_id"`
|
||||||
|
ResourceCode string `json:"resource_code"`
|
||||||
|
ResourceType string `json:"resource_type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Grantable bool `json:"grantable"`
|
||||||
|
GrantStrategy string `json:"grant_strategy"`
|
||||||
|
WalletAssetType string `json:"wallet_asset_type,omitempty"`
|
||||||
|
WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"`
|
||||||
|
UsageScopes []string `json:"usage_scopes"`
|
||||||
|
AssetURL string `json:"asset_url"`
|
||||||
|
PreviewURL string `json:"preview_url"`
|
||||||
|
AnimationURL string `json:"animation_url"`
|
||||||
|
MetadataJSON string `json:"metadata_json"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourcePointerFromProto(item *walletv1.Resource) *resourceData {
|
||||||
|
if item == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data := resourceFromProto(item)
|
||||||
|
return &data
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceFromProto(item *walletv1.Resource) resourceData {
|
||||||
|
if item == nil {
|
||||||
|
return resourceData{}
|
||||||
|
}
|
||||||
|
return resourceData{
|
||||||
|
ResourceID: item.GetResourceId(),
|
||||||
|
ResourceCode: item.GetResourceCode(),
|
||||||
|
ResourceType: item.GetResourceType(),
|
||||||
|
Name: item.GetName(),
|
||||||
|
Status: item.GetStatus(),
|
||||||
|
Grantable: item.GetGrantable(),
|
||||||
|
GrantStrategy: item.GetGrantStrategy(),
|
||||||
|
WalletAssetType: item.GetWalletAssetType(),
|
||||||
|
WalletAssetAmount: item.GetWalletAssetAmount(),
|
||||||
|
UsageScopes: item.GetUsageScopes(),
|
||||||
|
AssetURL: item.GetAssetUrl(),
|
||||||
|
PreviewURL: item.GetPreviewUrl(),
|
||||||
|
AnimationURL: item.GetAnimationUrl(),
|
||||||
|
MetadataJSON: item.GetMetadataJson(),
|
||||||
|
SortOrder: item.GetSortOrder(),
|
||||||
|
CreatedAtMS: item.GetCreatedAtMs(),
|
||||||
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package walletapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
@ -57,7 +57,7 @@ func (h *Handler) purchaseVIP(writer http.ResponseWriter, request *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body vipPurchaseRequestBody
|
var body vipPurchaseRequestBody
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
commandID := strings.TrimSpace(body.CommandID)
|
commandID := strings.TrimSpace(body.CommandID)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package http
|
package walletapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
@ -104,7 +104,7 @@ func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body coinSellerTransferRequestBody
|
var body coinSellerTransferRequestBody
|
||||||
if !decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
commandID := body.normalizedCommandID()
|
commandID := body.normalizedCommandID()
|
||||||
@ -117,7 +117,7 @@ func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *ht
|
|||||||
|
|
||||||
sellerUserID := auth.UserIDFromContext(request.Context())
|
sellerUserID := auth.UserIDFromContext(request.Context())
|
||||||
profileResp, err := h.userHostClient.GetCoinSellerProfile(request.Context(), &userv1.GetCoinSellerProfileRequest{
|
profileResp, err := h.userHostClient.GetCoinSellerProfile(request.Context(), &userv1.GetCoinSellerProfileRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: sellerUserID,
|
UserId: sellerUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -168,7 +168,7 @@ func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *ht
|
|||||||
|
|
||||||
func (h *Handler) resolveCoinSellerTransferTargetUserID(writer http.ResponseWriter, request *http.Request, targetDisplayUserID string) (int64, bool) {
|
func (h *Handler) resolveCoinSellerTransferTargetUserID(writer http.ResponseWriter, request *http.Request, targetDisplayUserID string) (int64, bool) {
|
||||||
resp, err := h.userIdentityClient.ResolveDisplayUserID(request.Context(), &userv1.ResolveDisplayUserIDRequest{
|
resp, err := h.userIdentityClient.ResolveDisplayUserID(request.Context(), &userv1.ResolveDisplayUserIDRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
DisplayUserId: targetDisplayUserID,
|
DisplayUserId: targetDisplayUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -186,7 +186,7 @@ func (h *Handler) resolveCoinSellerTransferTargetUserID(writer http.ResponseWrit
|
|||||||
|
|
||||||
func (h *Handler) coinSellerTransferRegions(writer http.ResponseWriter, request *http.Request, sellerUserID int64, targetUserID int64) (int64, int64, bool) {
|
func (h *Handler) coinSellerTransferRegions(writer http.ResponseWriter, request *http.Request, sellerUserID int64, targetUserID int64) (int64, int64, bool) {
|
||||||
sellerResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
sellerResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: sellerUserID,
|
UserId: sellerUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -194,7 +194,7 @@ func (h *Handler) coinSellerTransferRegions(writer http.ResponseWriter, request
|
|||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: authRequestMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: targetUserID,
|
UserId: targetUserID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -5,6 +5,7 @@ grpc_addr: ":13009"
|
|||||||
health_http_addr: ":13109"
|
health_http_addr: ":13109"
|
||||||
mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC"
|
mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||||
wallet_database: "hyapp_wallet"
|
wallet_database: "hyapp_wallet"
|
||||||
|
room_database: "hyapp_room"
|
||||||
mysql_auto_migrate: false
|
mysql_auto_migrate: false
|
||||||
|
|
||||||
tencent_im:
|
tencent_im:
|
||||||
@ -26,6 +27,16 @@ wallet_notice_worker:
|
|||||||
initial_backoff: 5s
|
initial_backoff: 5s
|
||||||
max_backoff: 5m
|
max_backoff: 5m
|
||||||
|
|
||||||
|
room_notice_worker:
|
||||||
|
enabled: false
|
||||||
|
poll_interval: 1s
|
||||||
|
batch_size: 100
|
||||||
|
lock_ttl: 30s
|
||||||
|
publish_timeout: 3s
|
||||||
|
max_retry_count: 10
|
||||||
|
initial_backoff: 5s
|
||||||
|
max_backoff: 5m
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: info
|
level: info
|
||||||
format: json
|
format: json
|
||||||
|
|||||||
@ -5,6 +5,7 @@ grpc_addr: ":13009"
|
|||||||
health_http_addr: ":13109"
|
health_http_addr: ":13109"
|
||||||
mysql_dsn: "hyapp:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:3306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC"
|
mysql_dsn: "hyapp:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:3306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||||
wallet_database: "hyapp_wallet"
|
wallet_database: "hyapp_wallet"
|
||||||
|
room_database: "hyapp_room"
|
||||||
mysql_auto_migrate: false
|
mysql_auto_migrate: false
|
||||||
|
|
||||||
tencent_im:
|
tencent_im:
|
||||||
@ -26,6 +27,16 @@ wallet_notice_worker:
|
|||||||
initial_backoff: 5s
|
initial_backoff: 5s
|
||||||
max_backoff: 5m
|
max_backoff: 5m
|
||||||
|
|
||||||
|
room_notice_worker:
|
||||||
|
enabled: true
|
||||||
|
poll_interval: 500ms
|
||||||
|
batch_size: 100
|
||||||
|
lock_ttl: 30s
|
||||||
|
publish_timeout: 3s
|
||||||
|
max_retry_count: 10
|
||||||
|
initial_backoff: 5s
|
||||||
|
max_backoff: 5m
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: info
|
level: info
|
||||||
format: json
|
format: json
|
||||||
|
|||||||
@ -5,6 +5,7 @@ grpc_addr: ":13009"
|
|||||||
health_http_addr: ":13109"
|
health_http_addr: ":13109"
|
||||||
mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC"
|
mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||||
wallet_database: "hyapp_wallet"
|
wallet_database: "hyapp_wallet"
|
||||||
|
room_database: "hyapp_room"
|
||||||
mysql_auto_migrate: false
|
mysql_auto_migrate: false
|
||||||
|
|
||||||
tencent_im:
|
tencent_im:
|
||||||
@ -26,6 +27,16 @@ wallet_notice_worker:
|
|||||||
initial_backoff: 5s
|
initial_backoff: 5s
|
||||||
max_backoff: 5m
|
max_backoff: 5m
|
||||||
|
|
||||||
|
room_notice_worker:
|
||||||
|
enabled: false
|
||||||
|
poll_interval: 1s
|
||||||
|
batch_size: 100
|
||||||
|
lock_ttl: 30s
|
||||||
|
publish_timeout: 3s
|
||||||
|
max_retry_count: 10
|
||||||
|
initial_backoff: 5s
|
||||||
|
max_backoff: 5m
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: info
|
level: info
|
||||||
format: json
|
format: json
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import (
|
|||||||
"hyapp/pkg/logx"
|
"hyapp/pkg/logx"
|
||||||
"hyapp/pkg/tencentim"
|
"hyapp/pkg/tencentim"
|
||||||
"hyapp/services/notice-service/internal/config"
|
"hyapp/services/notice-service/internal/config"
|
||||||
|
"hyapp/services/notice-service/internal/modules/roomnotice"
|
||||||
"hyapp/services/notice-service/internal/modules/walletnotice"
|
"hyapp/services/notice-service/internal/modules/walletnotice"
|
||||||
mysqlplatform "hyapp/services/notice-service/internal/platform/mysql"
|
mysqlplatform "hyapp/services/notice-service/internal/platform/mysql"
|
||||||
)
|
)
|
||||||
@ -30,6 +31,9 @@ type App struct {
|
|||||||
walletNoticeService *walletnotice.Service
|
walletNoticeService *walletnotice.Service
|
||||||
walletWorkerOptions walletnotice.WalletNoticeWorkerOptions
|
walletWorkerOptions walletnotice.WalletNoticeWorkerOptions
|
||||||
walletWorkerEnabled bool
|
walletWorkerEnabled bool
|
||||||
|
roomNoticeService *roomnotice.Service
|
||||||
|
roomWorkerOptions roomnotice.RoomNoticeWorkerOptions
|
||||||
|
roomWorkerEnabled bool
|
||||||
workerCtx context.Context
|
workerCtx context.Context
|
||||||
workerCancel context.CancelFunc
|
workerCancel context.CancelFunc
|
||||||
workerWG sync.WaitGroup
|
workerWG sync.WaitGroup
|
||||||
@ -57,6 +61,12 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
_ = store.Close()
|
_ = store.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
roomRepo, err := roomnotice.NewMySQLRepository(store.DB, cfg.RoomDatabase)
|
||||||
|
if err != nil {
|
||||||
|
_ = listener.Close()
|
||||||
|
_ = store.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var publisher walletnotice.UserMessagePublisher
|
var publisher walletnotice.UserMessagePublisher
|
||||||
if cfg.TencentIM.Enabled {
|
if cfg.TencentIM.Enabled {
|
||||||
tencentClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
|
tencentClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
|
||||||
@ -72,6 +82,11 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
_ = store.Close()
|
_ = store.Close()
|
||||||
return nil, errors.New("wallet notice worker requires tencent_im.enabled")
|
return nil, errors.New("wallet notice worker requires tencent_im.enabled")
|
||||||
}
|
}
|
||||||
|
if cfg.RoomNoticeWorker.Enabled && publisher == nil {
|
||||||
|
_ = listener.Close()
|
||||||
|
_ = store.Close()
|
||||||
|
return nil, errors.New("room notice worker requires tencent_im.enabled")
|
||||||
|
}
|
||||||
|
|
||||||
server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("notice-service")))
|
server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("notice-service")))
|
||||||
health := grpchealth.NewServingChecker("notice-service", grpchealth.Dependency{
|
health := grpchealth.NewServingChecker("notice-service", grpchealth.Dependency{
|
||||||
@ -96,6 +111,9 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
walletNoticeService: walletnotice.New(walletnotice.Config{NodeID: cfg.NodeID}, walletRepo, publisher),
|
walletNoticeService: walletnotice.New(walletnotice.Config{NodeID: cfg.NodeID}, walletRepo, publisher),
|
||||||
walletWorkerOptions: walletNoticeWorkerOptions(cfg.WalletNoticeWorker),
|
walletWorkerOptions: walletNoticeWorkerOptions(cfg.WalletNoticeWorker),
|
||||||
walletWorkerEnabled: cfg.WalletNoticeWorker.Enabled,
|
walletWorkerEnabled: cfg.WalletNoticeWorker.Enabled,
|
||||||
|
roomNoticeService: roomnotice.New(roomnotice.Config{NodeID: cfg.NodeID}, roomRepo, publisher),
|
||||||
|
roomWorkerOptions: roomNoticeWorkerOptions(cfg.RoomNoticeWorker),
|
||||||
|
roomWorkerEnabled: cfg.RoomNoticeWorker.Enabled,
|
||||||
workerCtx: workerCtx,
|
workerCtx: workerCtx,
|
||||||
workerCancel: workerCancel,
|
workerCancel: workerCancel,
|
||||||
}, nil
|
}, nil
|
||||||
@ -117,6 +135,11 @@ func (a *App) Run() error {
|
|||||||
a.walletNoticeService.RunWalletBalanceWorker(a.workerCtx, a.walletWorkerOptions)
|
a.walletNoticeService.RunWalletBalanceWorker(a.workerCtx, a.walletWorkerOptions)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if a.roomWorkerEnabled && a.roomNoticeService != nil {
|
||||||
|
a.workerWG.Go(func() {
|
||||||
|
a.roomNoticeService.RunRoomKickWorker(a.workerCtx, a.roomWorkerOptions)
|
||||||
|
})
|
||||||
|
}
|
||||||
err := a.server.Serve(a.listener)
|
err := a.server.Serve(a.listener)
|
||||||
if errors.Is(err, grpc.ErrServerStopped) {
|
if errors.Is(err, grpc.ErrServerStopped) {
|
||||||
return nil
|
return nil
|
||||||
@ -171,3 +194,15 @@ func walletNoticeWorkerOptions(cfg config.WalletNoticeWorkerConfig) walletnotice
|
|||||||
MaxBackoff: cfg.MaxBackoff,
|
MaxBackoff: cfg.MaxBackoff,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func roomNoticeWorkerOptions(cfg config.WalletNoticeWorkerConfig) roomnotice.RoomNoticeWorkerOptions {
|
||||||
|
return roomnotice.RoomNoticeWorkerOptions{
|
||||||
|
PollInterval: cfg.PollInterval,
|
||||||
|
BatchSize: cfg.BatchSize,
|
||||||
|
LockTTL: cfg.LockTTL,
|
||||||
|
PublishTimeout: cfg.PublishTimeout,
|
||||||
|
MaxRetryCount: cfg.MaxRetryCount,
|
||||||
|
InitialBackoff: cfg.InitialBackoff,
|
||||||
|
MaxBackoff: cfg.MaxBackoff,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -22,12 +22,16 @@ type Config struct {
|
|||||||
MySQLDSN string `yaml:"mysql_dsn"`
|
MySQLDSN string `yaml:"mysql_dsn"`
|
||||||
// WalletDatabase 是 wallet_outbox 所在库名;notice 只把它当 append-only 事实源读取。
|
// WalletDatabase 是 wallet_outbox 所在库名;notice 只把它当 append-only 事实源读取。
|
||||||
WalletDatabase string `yaml:"wallet_database"`
|
WalletDatabase string `yaml:"wallet_database"`
|
||||||
|
// RoomDatabase 是 room_outbox 所在库名;notice 只读取需要私有通知的房间事实。
|
||||||
|
RoomDatabase string `yaml:"room_database"`
|
||||||
// TencentIM 是私有 IM 投递通道配置;本地默认关闭,避免误发真实消息。
|
// TencentIM 是私有 IM 投递通道配置;本地默认关闭,避免误发真实消息。
|
||||||
TencentIM TencentIMConfig `yaml:"tencent_im"`
|
TencentIM TencentIMConfig `yaml:"tencent_im"`
|
||||||
// WalletNoticeWorker 消费 wallet_outbox 中的 WalletBalanceChanged 事件。
|
// WalletNoticeWorker 消费 wallet_outbox 中的 WalletBalanceChanged 事件。
|
||||||
WalletNoticeWorker WalletNoticeWorkerConfig `yaml:"wallet_notice_worker"`
|
WalletNoticeWorker WalletNoticeWorkerConfig `yaml:"wallet_notice_worker"`
|
||||||
MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
|
// RoomNoticeWorker 消费 room_outbox 中需要给单个用户发送的房间通知。
|
||||||
Log logx.Config `yaml:"log"`
|
RoomNoticeWorker WalletNoticeWorkerConfig `yaml:"room_notice_worker"`
|
||||||
|
MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
|
||||||
|
Log logx.Config `yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TencentIMConfig 描述服务端调用腾讯云 IM REST API 的配置。
|
// TencentIMConfig 描述服务端调用腾讯云 IM REST API 的配置。
|
||||||
@ -79,6 +83,7 @@ func Default() Config {
|
|||||||
HealthHTTPAddr: ":13109",
|
HealthHTTPAddr: ":13109",
|
||||||
MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC",
|
MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC",
|
||||||
WalletDatabase: "hyapp_wallet",
|
WalletDatabase: "hyapp_wallet",
|
||||||
|
RoomDatabase: "hyapp_room",
|
||||||
MySQLAutoMigrate: false,
|
MySQLAutoMigrate: false,
|
||||||
TencentIM: TencentIMConfig{
|
TencentIM: TencentIMConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
@ -97,6 +102,16 @@ func Default() Config {
|
|||||||
InitialBackoff: 5 * time.Second,
|
InitialBackoff: 5 * time.Second,
|
||||||
MaxBackoff: 5 * time.Minute,
|
MaxBackoff: 5 * time.Minute,
|
||||||
},
|
},
|
||||||
|
RoomNoticeWorker: WalletNoticeWorkerConfig{
|
||||||
|
Enabled: false,
|
||||||
|
PollInterval: time.Second,
|
||||||
|
BatchSize: 100,
|
||||||
|
LockTTL: 30 * time.Second,
|
||||||
|
PublishTimeout: 3 * time.Second,
|
||||||
|
MaxRetryCount: 10,
|
||||||
|
InitialBackoff: 5 * time.Second,
|
||||||
|
MaxBackoff: 5 * time.Minute,
|
||||||
|
},
|
||||||
Log: logx.Config{
|
Log: logx.Config{
|
||||||
Level: "info",
|
Level: "info",
|
||||||
Format: "json",
|
Format: "json",
|
||||||
@ -138,7 +153,12 @@ func Load(path string) (Config, error) {
|
|||||||
if cfg.WalletDatabase == "" {
|
if cfg.WalletDatabase == "" {
|
||||||
cfg.WalletDatabase = "hyapp_wallet"
|
cfg.WalletDatabase = "hyapp_wallet"
|
||||||
}
|
}
|
||||||
|
cfg.RoomDatabase = strings.TrimSpace(cfg.RoomDatabase)
|
||||||
|
if cfg.RoomDatabase == "" {
|
||||||
|
cfg.RoomDatabase = "hyapp_room"
|
||||||
|
}
|
||||||
cfg.WalletNoticeWorker = normalizeWalletNoticeWorker(cfg.WalletNoticeWorker)
|
cfg.WalletNoticeWorker = normalizeWalletNoticeWorker(cfg.WalletNoticeWorker)
|
||||||
|
cfg.RoomNoticeWorker = normalizeWalletNoticeWorker(cfg.RoomNoticeWorker)
|
||||||
if strings.TrimSpace(cfg.TencentIM.AdminIdentifier) == "" {
|
if strings.TrimSpace(cfg.TencentIM.AdminIdentifier) == "" {
|
||||||
cfg.TencentIM.AdminIdentifier = "administrator"
|
cfg.TencentIM.AdminIdentifier = "administrator"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,174 @@
|
|||||||
|
package roomnotice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMySQLRepositoryProcessesRealRoomKickOutbox(t *testing.T) {
|
||||||
|
dsn := strings.TrimSpace(os.Getenv("NOTICE_REAL_MYSQL_DSN"))
|
||||||
|
if dsn == "" {
|
||||||
|
t.Skip("set NOTICE_REAL_MYSQL_DSN to run real MySQL notice validation")
|
||||||
|
}
|
||||||
|
db, err := sql.Open("mysql", dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open mysql: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
t.Fatalf("ping mysql: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
suffix := fmt.Sprintf("%d", time.Now().UnixNano())
|
||||||
|
roomDatabase := strings.TrimSpace(os.Getenv("NOTICE_REAL_ROOM_DATABASE"))
|
||||||
|
if roomDatabase == "" {
|
||||||
|
roomDatabase = "hyapp_room_notice_test_" + suffix
|
||||||
|
if _, err := db.ExecContext(ctx, fmt.Sprintf(`CREATE DATABASE %s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`, quoteDB(roomDatabase))); err != nil {
|
||||||
|
t.Fatalf("create room test database: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_, _ = db.ExecContext(context.Background(), fmt.Sprintf(`DROP DATABASE %s`, quoteDB(roomDatabase)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := ensureRoomOutboxTable(ctx, db, roomDatabase); err != nil {
|
||||||
|
t.Fatalf("ensure room_outbox table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repository, err := NewMySQLRepository(db, roomDatabase)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewMySQLRepository: %v", err)
|
||||||
|
}
|
||||||
|
appCode := "notice_real"
|
||||||
|
roomID := "notice_real_room_" + suffix
|
||||||
|
eventID := "notice_real_room_evt_" + suffix
|
||||||
|
actorUserID := int64(990000001)
|
||||||
|
targetUserID := int64(990000002)
|
||||||
|
nowMS := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
body, err := proto.Marshal(&roomeventsv1.RoomUserKicked{
|
||||||
|
ActorUserId: actorUserID,
|
||||||
|
TargetUserId: targetUserID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal body: %v", err)
|
||||||
|
}
|
||||||
|
envelope, err := proto.Marshal(&roomeventsv1.EventEnvelope{
|
||||||
|
EventId: eventID,
|
||||||
|
RoomId: roomID,
|
||||||
|
EventType: "RoomUserKicked",
|
||||||
|
RoomVersion: 12,
|
||||||
|
OccurredAtMs: nowMS,
|
||||||
|
AppCode: appCode,
|
||||||
|
Body: body,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal envelope: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.ExecContext(ctx, fmt.Sprintf(`
|
||||||
|
INSERT INTO %s.room_outbox (
|
||||||
|
app_code, event_id, event_type, room_id, status, worker_id, lock_until_ms,
|
||||||
|
envelope, created_at_ms, retry_count, next_retry_at_ms, last_error, updated_at_ms
|
||||||
|
) VALUES (?, ?, 'RoomUserKicked', ?, 'delivered', '', 0, ?, ?, 0, 0, '', ?)`,
|
||||||
|
quoteDB(roomDatabase),
|
||||||
|
), appCode, eventID, roomID, envelope, nowMS, nowMS); err != nil {
|
||||||
|
t.Fatalf("insert room_outbox: %v", err)
|
||||||
|
}
|
||||||
|
if !keepRealRows() {
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_, _ = db.ExecContext(context.Background(), `DELETE FROM notice_delivery_events WHERE app_code = ? AND source_event_id = ?`, appCode, eventID)
|
||||||
|
_, _ = db.ExecContext(context.Background(), fmt.Sprintf(`DELETE FROM %s.room_outbox WHERE app_code = ? AND event_id = ?`, quoteDB(roomDatabase)), appCode, eventID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
publisher := &fakePublisher{}
|
||||||
|
service := New(Config{NodeID: "notice-real-test"}, repository, publisher)
|
||||||
|
processed, err := service.ProcessRoomKickNotices(ctx, RoomNoticeWorkerOptions{
|
||||||
|
WorkerID: "notice-real-room-test-worker",
|
||||||
|
BatchSize: 1,
|
||||||
|
LockTTL: 30 * time.Second,
|
||||||
|
PublishTimeout: time.Second,
|
||||||
|
MaxRetryCount: 3,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessRoomKickNotices: %v", err)
|
||||||
|
}
|
||||||
|
if processed != 1 || len(publisher.messages) != 1 {
|
||||||
|
t.Fatalf("unexpected process result: processed=%d messages=%d", processed, len(publisher.messages))
|
||||||
|
}
|
||||||
|
if publisher.messages[0].ToAccount != "990000002" || publisher.messages[0].Ext != "room_notice" {
|
||||||
|
t.Fatalf("unexpected private message: %+v", publisher.messages[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
var noticeStatus string
|
||||||
|
var deliveredAtMS int64
|
||||||
|
var storedPayload string
|
||||||
|
if err := db.QueryRowContext(ctx, `
|
||||||
|
SELECT status, delivered_at_ms, COALESCE(CAST(payload_json AS CHAR), '{}')
|
||||||
|
FROM notice_delivery_events
|
||||||
|
WHERE source_name = 'room_outbox' AND app_code = ? AND source_event_id = ? AND channel = 'tencent_im_c2c'`,
|
||||||
|
appCode, eventID,
|
||||||
|
).Scan(¬iceStatus, &deliveredAtMS, &storedPayload); err != nil {
|
||||||
|
t.Fatalf("query notice_delivery_events: %v", err)
|
||||||
|
}
|
||||||
|
if noticeStatus != "delivered" || deliveredAtMS <= 0 {
|
||||||
|
t.Fatalf("unexpected delivery marker: status=%s delivered_at_ms=%d payload=%s", noticeStatus, deliveredAtMS, storedPayload)
|
||||||
|
}
|
||||||
|
var noticePayload map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(storedPayload), ¬icePayload); err != nil {
|
||||||
|
t.Fatalf("decode stored notice payload: %v", err)
|
||||||
|
}
|
||||||
|
if noticePayload["event_id"] != eventID || noticePayload["room_id"] != roomID || noticePayload["event_type"] != "room_user_kicked" {
|
||||||
|
t.Fatalf("unexpected stored notice payload: %+v", noticePayload)
|
||||||
|
}
|
||||||
|
if noticePayload["target_user_id"].(float64) != float64(targetUserID) || noticePayload["actor_user_id"].(float64) != float64(actorUserID) {
|
||||||
|
t.Fatalf("unexpected stored user payload: %+v", noticePayload)
|
||||||
|
}
|
||||||
|
t.Logf("real room notice validation event_id=%s status=%s delivered_at_ms=%d", eventID, noticeStatus, deliveredAtMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func keepRealRows() bool {
|
||||||
|
value := strings.ToLower(strings.TrimSpace(os.Getenv("NOTICE_REAL_MYSQL_KEEP_ROWS")))
|
||||||
|
return value == "1" || value == "true" || value == "yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
func quoteDB(value string) string {
|
||||||
|
return "`" + strings.ReplaceAll(value, "`", "``") + "`"
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureRoomOutboxTable(ctx context.Context, db *sql.DB, roomDatabase string) error {
|
||||||
|
_, err := db.ExecContext(ctx, fmt.Sprintf(`
|
||||||
|
CREATE TABLE IF NOT EXISTS %s.room_outbox (
|
||||||
|
app_code VARCHAR(32) NOT NULL,
|
||||||
|
event_id VARCHAR(128) NOT NULL,
|
||||||
|
event_type VARCHAR(64) NOT NULL,
|
||||||
|
room_id VARCHAR(64) NOT NULL,
|
||||||
|
status VARCHAR(32) NOT NULL,
|
||||||
|
worker_id VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
lock_until_ms BIGINT NULL,
|
||||||
|
envelope LONGBLOB NOT NULL,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
retry_count INT NOT NULL DEFAULT 0,
|
||||||
|
next_retry_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
last_error TEXT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (app_code, event_id),
|
||||||
|
KEY idx_room_outbox_notice (app_code, event_type, created_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||||
|
quoteDB(roomDatabase),
|
||||||
|
))
|
||||||
|
return err
|
||||||
|
}
|
||||||
@ -0,0 +1,382 @@
|
|||||||
|
package roomnotice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sourceRoomOutbox = "room_outbox"
|
||||||
|
channelTencentIMC2C = "tencent_im_c2c"
|
||||||
|
eventRoomUserKicked = "RoomUserKicked"
|
||||||
|
deliveryStatusDelivering = "delivering"
|
||||||
|
deliveryStatusDelivered = "delivered"
|
||||||
|
deliveryStatusRetryable = "retryable"
|
||||||
|
deliveryStatusFailed = "failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MySQLRepository 只读取 room_outbox 事实,并把 notice 自己的消费位点写入 notice_delivery_events。
|
||||||
|
type MySQLRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
roomOutboxTable string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomKickEvent 是 notice-service 从 room_outbox 认领的一条踢人私有通知事实。
|
||||||
|
type RoomKickEvent struct {
|
||||||
|
AppCode string
|
||||||
|
EventID string
|
||||||
|
EventType string
|
||||||
|
RoomID string
|
||||||
|
RoomVersion int64
|
||||||
|
ActorUserID int64
|
||||||
|
TargetUserID int64
|
||||||
|
OccurredAtMS int64
|
||||||
|
CreatedAtMS int64
|
||||||
|
RetryCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMySQLRepository 创建房间私有通知仓储;room_outbox 被视作 append-only 事实源。
|
||||||
|
func NewMySQLRepository(db *sql.DB, roomDatabase string) (*MySQLRepository, error) {
|
||||||
|
if db == nil {
|
||||||
|
return nil, xerr.New(xerr.Unavailable, "mysql db is not configured")
|
||||||
|
}
|
||||||
|
roomDatabase = normalizeDatabaseName(roomDatabase)
|
||||||
|
if roomDatabase == "" {
|
||||||
|
return nil, xerr.New(xerr.InvalidArgument, "room_database is invalid")
|
||||||
|
}
|
||||||
|
return &MySQLRepository{
|
||||||
|
db: db,
|
||||||
|
roomOutboxTable: fmt.Sprintf("`%s`.`room_outbox`", roomDatabase),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimRoomKickEvents 抢占待投递给被踢用户的私有 C2C 通知。
|
||||||
|
// room_outbox 的 pending/delivered 状态属于房间群消息 bridge;这里不能修改它,避免多消费者互相覆盖。
|
||||||
|
func (r *MySQLRepository) ClaimRoomKickEvents(ctx context.Context, workerID string, limit int, lockTTL time.Duration) ([]RoomKickEvent, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return nil, xerr.New(xerr.Unavailable, "notice repository is not configured")
|
||||||
|
}
|
||||||
|
workerID = strings.TrimSpace(workerID)
|
||||||
|
if workerID == "" {
|
||||||
|
return nil, xerr.New(xerr.InvalidArgument, "worker_id is required")
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
if limit > 500 {
|
||||||
|
limit = 500
|
||||||
|
}
|
||||||
|
if lockTTL <= 0 {
|
||||||
|
lockTTL = 30 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates, err := r.listRoomKickCandidates(ctx, limit, time.Now().UnixMilli())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
claimed := make([]RoomKickEvent, 0, len(candidates))
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
event, ok, err := r.claimRoomKickCandidate(ctx, candidate, workerID, lockTTL)
|
||||||
|
if err != nil {
|
||||||
|
return claimed, err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
claimed = append(claimed, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return claimed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type roomKickCandidate struct {
|
||||||
|
AppCode string
|
||||||
|
EventID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MySQLRepository) listRoomKickCandidates(ctx context.Context, limit int, nowMs int64) ([]roomKickCandidate, error) {
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT ro.app_code, ro.event_id
|
||||||
|
FROM %s ro
|
||||||
|
LEFT JOIN notice_delivery_events nde
|
||||||
|
ON nde.source_name = ? AND nde.app_code = ro.app_code AND nde.source_event_id = ro.event_id AND nde.channel = ?
|
||||||
|
WHERE ro.event_type = ?
|
||||||
|
AND (
|
||||||
|
nde.source_event_id IS NULL
|
||||||
|
OR (nde.status = ? AND nde.next_retry_at_ms <= ?)
|
||||||
|
OR (nde.status = ? AND nde.lock_until_ms <= ?)
|
||||||
|
)
|
||||||
|
ORDER BY ro.created_at_ms ASC, ro.event_id ASC
|
||||||
|
LIMIT ?`, r.roomOutboxTable)
|
||||||
|
rows, err := r.db.QueryContext(ctx, query,
|
||||||
|
sourceRoomOutbox,
|
||||||
|
channelTencentIMC2C,
|
||||||
|
eventRoomUserKicked,
|
||||||
|
deliveryStatusRetryable,
|
||||||
|
nowMs,
|
||||||
|
deliveryStatusDelivering,
|
||||||
|
nowMs,
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
candidates := make([]roomKickCandidate, 0, limit)
|
||||||
|
for rows.Next() {
|
||||||
|
var candidate roomKickCandidate
|
||||||
|
if err := rows.Scan(&candidate.AppCode, &candidate.EventID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
candidates = append(candidates, candidate)
|
||||||
|
}
|
||||||
|
return candidates, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MySQLRepository) claimRoomKickCandidate(ctx context.Context, candidate roomKickCandidate, workerID string, lockTTL time.Duration) (RoomKickEvent, bool, error) {
|
||||||
|
nowMs := time.Now().UnixMilli()
|
||||||
|
lockUntilMS := time.Now().Add(lockTTL).UnixMilli()
|
||||||
|
ctx = appcode.WithContext(ctx, candidate.AppCode)
|
||||||
|
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return RoomKickEvent{}, false, err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
event, err := r.lockRoomKickEvent(ctx, tx, candidate)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
// 候选扫描和事务锁定之间可能被清理或事件类型变化,跳过该行即可。
|
||||||
|
return RoomKickEvent{}, false, nil
|
||||||
|
}
|
||||||
|
return RoomKickEvent{}, false, err
|
||||||
|
}
|
||||||
|
retryCount, claimed, err := r.claimDeliveryEvent(ctx, tx, event, workerID, lockUntilMS, nowMs)
|
||||||
|
if err != nil || !claimed {
|
||||||
|
return RoomKickEvent{}, false, err
|
||||||
|
}
|
||||||
|
event.RetryCount = retryCount
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return RoomKickEvent{}, false, err
|
||||||
|
}
|
||||||
|
return event, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MySQLRepository) lockRoomKickEvent(ctx context.Context, tx *sql.Tx, candidate roomKickCandidate) (RoomKickEvent, error) {
|
||||||
|
var (
|
||||||
|
event RoomKickEvent
|
||||||
|
envelopeBytes []byte
|
||||||
|
)
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
SELECT app_code, event_id, event_type, room_id, envelope, created_at_ms
|
||||||
|
FROM %s
|
||||||
|
WHERE app_code = ? AND event_id = ? AND event_type = ?
|
||||||
|
FOR UPDATE`, r.roomOutboxTable)
|
||||||
|
err := tx.QueryRowContext(ctx, query,
|
||||||
|
appcode.Normalize(candidate.AppCode),
|
||||||
|
candidate.EventID,
|
||||||
|
eventRoomUserKicked,
|
||||||
|
).Scan(
|
||||||
|
&event.AppCode,
|
||||||
|
&event.EventID,
|
||||||
|
&event.EventType,
|
||||||
|
&event.RoomID,
|
||||||
|
&envelopeBytes,
|
||||||
|
&event.CreatedAtMS,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return RoomKickEvent{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var envelope roomeventsv1.EventEnvelope
|
||||||
|
if err := proto.Unmarshal(envelopeBytes, &envelope); err != nil {
|
||||||
|
return RoomKickEvent{}, err
|
||||||
|
}
|
||||||
|
if envelope.GetEventType() != eventRoomUserKicked {
|
||||||
|
return RoomKickEvent{}, fmt.Errorf("unexpected room event type %q", envelope.GetEventType())
|
||||||
|
}
|
||||||
|
var body roomeventsv1.RoomUserKicked
|
||||||
|
if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil {
|
||||||
|
return RoomKickEvent{}, err
|
||||||
|
}
|
||||||
|
event.RoomVersion = envelope.GetRoomVersion()
|
||||||
|
event.OccurredAtMS = envelope.GetOccurredAtMs()
|
||||||
|
event.ActorUserID = body.GetActorUserId()
|
||||||
|
event.TargetUserID = body.GetTargetUserId()
|
||||||
|
if event.TargetUserID <= 0 {
|
||||||
|
return RoomKickEvent{}, fmt.Errorf("room kick event target_user_id is invalid")
|
||||||
|
}
|
||||||
|
return event, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MySQLRepository) claimDeliveryEvent(ctx context.Context, tx *sql.Tx, event RoomKickEvent, workerID string, lockUntilMS int64, nowMs int64) (int, bool, error) {
|
||||||
|
payload, err := roomKickNoticePayload(event)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false, err
|
||||||
|
}
|
||||||
|
_, err = tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO notice_delivery_events (
|
||||||
|
source_name, app_code, source_event_id, channel, notice_type, target_user_id,
|
||||||
|
status, retry_count, locked_by, lock_until_ms, next_retry_at_ms,
|
||||||
|
payload_json, last_error, delivered_at_ms, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 0, ?, '', 0, ?, ?)`,
|
||||||
|
sourceRoomOutbox,
|
||||||
|
event.AppCode,
|
||||||
|
event.EventID,
|
||||||
|
channelTencentIMC2C,
|
||||||
|
event.EventType,
|
||||||
|
event.TargetUserID,
|
||||||
|
deliveryStatusDelivering,
|
||||||
|
workerID,
|
||||||
|
lockUntilMS,
|
||||||
|
string(payload),
|
||||||
|
nowMs,
|
||||||
|
nowMs,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
return 0, true, nil
|
||||||
|
}
|
||||||
|
if !isDuplicateKey(err) {
|
||||||
|
return 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := tx.ExecContext(ctx, `
|
||||||
|
UPDATE notice_delivery_events
|
||||||
|
SET status = ?, locked_by = ?, lock_until_ms = ?, payload_json = ?, updated_at_ms = ?
|
||||||
|
WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?
|
||||||
|
AND (
|
||||||
|
(status = ? AND next_retry_at_ms <= ?)
|
||||||
|
OR (status = ? AND lock_until_ms <= ?)
|
||||||
|
)`,
|
||||||
|
deliveryStatusDelivering,
|
||||||
|
workerID,
|
||||||
|
lockUntilMS,
|
||||||
|
string(payload),
|
||||||
|
nowMs,
|
||||||
|
sourceRoomOutbox,
|
||||||
|
event.AppCode,
|
||||||
|
event.EventID,
|
||||||
|
channelTencentIMC2C,
|
||||||
|
deliveryStatusRetryable,
|
||||||
|
nowMs,
|
||||||
|
deliveryStatusDelivering,
|
||||||
|
nowMs,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false, err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil || affected == 0 {
|
||||||
|
return 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var retryCount int
|
||||||
|
err = tx.QueryRowContext(ctx, `
|
||||||
|
SELECT retry_count
|
||||||
|
FROM notice_delivery_events
|
||||||
|
WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?
|
||||||
|
FOR UPDATE`,
|
||||||
|
sourceRoomOutbox,
|
||||||
|
event.AppCode,
|
||||||
|
event.EventID,
|
||||||
|
channelTencentIMC2C,
|
||||||
|
).Scan(&retryCount)
|
||||||
|
return retryCount, err == nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkRoomKickDelivered 标记被踢私有通知已送达腾讯云 IM C2C 通道。
|
||||||
|
func (r *MySQLRepository) MarkRoomKickDelivered(ctx context.Context, event RoomKickEvent, deliveredPayload []byte, nowMs int64) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
UPDATE notice_delivery_events
|
||||||
|
SET status = ?, locked_by = '', lock_until_ms = 0, next_retry_at_ms = 0,
|
||||||
|
payload_json = ?, last_error = '', delivered_at_ms = ?, updated_at_ms = ?
|
||||||
|
WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
|
||||||
|
deliveryStatusDelivered,
|
||||||
|
string(deliveredPayload),
|
||||||
|
nowMs,
|
||||||
|
nowMs,
|
||||||
|
sourceRoomOutbox,
|
||||||
|
event.AppCode,
|
||||||
|
event.EventID,
|
||||||
|
channelTencentIMC2C,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkRoomKickRetryable 释放当前锁并进入退避重试;源 room_outbox 不受影响。
|
||||||
|
func (r *MySQLRepository) MarkRoomKickRetryable(ctx context.Context, event RoomKickEvent, retryCount int, nextRetryAtMS int64, lastErr string, nowMs int64) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
UPDATE notice_delivery_events
|
||||||
|
SET status = ?, retry_count = ?, locked_by = '', lock_until_ms = 0,
|
||||||
|
next_retry_at_ms = ?, last_error = ?, updated_at_ms = ?
|
||||||
|
WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
|
||||||
|
deliveryStatusRetryable,
|
||||||
|
retryCount,
|
||||||
|
nextRetryAtMS,
|
||||||
|
truncateError(lastErr),
|
||||||
|
nowMs,
|
||||||
|
sourceRoomOutbox,
|
||||||
|
event.AppCode,
|
||||||
|
event.EventID,
|
||||||
|
channelTencentIMC2C,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkRoomKickFailed 把无法投递的私有通知移入 notice 死信,等待后台人工重放。
|
||||||
|
func (r *MySQLRepository) MarkRoomKickFailed(ctx context.Context, event RoomKickEvent, retryCount int, lastErr string, nowMs int64) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
UPDATE notice_delivery_events
|
||||||
|
SET status = ?, retry_count = ?, locked_by = '', lock_until_ms = 0,
|
||||||
|
next_retry_at_ms = 0, last_error = ?, updated_at_ms = ?
|
||||||
|
WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
|
||||||
|
deliveryStatusFailed,
|
||||||
|
retryCount,
|
||||||
|
truncateError(lastErr),
|
||||||
|
nowMs,
|
||||||
|
sourceRoomOutbox,
|
||||||
|
event.AppCode,
|
||||||
|
event.EventID,
|
||||||
|
channelTencentIMC2C,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeDatabaseName(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, r := range value {
|
||||||
|
if r == '_' || unicode.IsDigit(r) || unicode.IsLetter(r) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDuplicateKey(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(err.Error(), "Error 1062") || strings.Contains(strings.ToLower(err.Error()), "duplicate")
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateError(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if len(value) <= 1000 {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value[:1000]
|
||||||
|
}
|
||||||
142
services/notice-service/internal/modules/roomnotice/service.go
Normal file
142
services/notice-service/internal/modules/roomnotice/service.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package roomnotice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/logx"
|
||||||
|
"hyapp/pkg/tencentim"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repository 是 room notice 模块需要的持久化边界。
|
||||||
|
type Repository interface {
|
||||||
|
ClaimRoomKickEvents(ctx context.Context, workerID string, limit int, lockTTL time.Duration) ([]RoomKickEvent, error)
|
||||||
|
MarkRoomKickDelivered(ctx context.Context, event RoomKickEvent, deliveredPayload []byte, nowMs int64) error
|
||||||
|
MarkRoomKickRetryable(ctx context.Context, event RoomKickEvent, retryCount int, nextRetryAtMS int64, lastErr string, nowMs int64) error
|
||||||
|
MarkRoomKickFailed(ctx context.Context, event RoomKickEvent, retryCount int, lastErr string, nowMs int64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserMessagePublisher 把房间私有 notice 投递到用户个人实时通道。
|
||||||
|
type UserMessagePublisher interface {
|
||||||
|
PublishUserCustomMessage(ctx context.Context, message tencentim.CustomUserMessage) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config 保存 room notice 的进程级配置。
|
||||||
|
type Config struct {
|
||||||
|
NodeID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service 消费 room_outbox 中需要私有通知的房间事实,不拥有房间状态。
|
||||||
|
type Service struct {
|
||||||
|
cfg Config
|
||||||
|
repository Repository
|
||||||
|
publisher UserMessagePublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 创建 room notice 服务。
|
||||||
|
func New(cfg Config, repository Repository, publisher UserMessagePublisher) *Service {
|
||||||
|
return &Service{cfg: cfg, repository: repository, publisher: publisher}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessRoomKickNotices 处理一批“被踢用户本人”的私有通知。
|
||||||
|
func (s *Service) ProcessRoomKickNotices(ctx context.Context, options RoomNoticeWorkerOptions) (int, error) {
|
||||||
|
options = normalizeRoomNoticeWorkerOptions(options, s.cfg.NodeID)
|
||||||
|
if s.repository == nil {
|
||||||
|
return 0, fmt.Errorf("notice repository is not configured")
|
||||||
|
}
|
||||||
|
if s.publisher == nil {
|
||||||
|
return 0, fmt.Errorf("notice publisher is not configured")
|
||||||
|
}
|
||||||
|
events, err := s.repository.ClaimRoomKickEvents(ctx, options.WorkerID, options.BatchSize, options.LockTTL)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
processed := 0
|
||||||
|
for _, event := range events {
|
||||||
|
if err := s.publishRoomKickEvent(ctx, event, options); err != nil {
|
||||||
|
return processed, err
|
||||||
|
}
|
||||||
|
processed++
|
||||||
|
}
|
||||||
|
return processed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) publishRoomKickEvent(ctx context.Context, event RoomKickEvent, options RoomNoticeWorkerOptions) error {
|
||||||
|
payload, err := roomKickNoticePayload(event)
|
||||||
|
if err != nil {
|
||||||
|
return s.markFailed(ctx, event, options, err)
|
||||||
|
}
|
||||||
|
publishCtx, cancel := context.WithTimeout(ctx, options.PublishTimeout)
|
||||||
|
defer cancel()
|
||||||
|
err = s.publisher.PublishUserCustomMessage(publishCtx, tencentim.CustomUserMessage{
|
||||||
|
ToAccount: tencentim.FormatUserID(event.TargetUserID),
|
||||||
|
EventID: event.EventID,
|
||||||
|
Desc: "room_user_kicked",
|
||||||
|
Ext: "room_notice",
|
||||||
|
PayloadJSON: payload,
|
||||||
|
})
|
||||||
|
nowMs := time.Now().UnixMilli()
|
||||||
|
if err == nil {
|
||||||
|
if markErr := s.repository.MarkRoomKickDelivered(ctx, event, payload, nowMs); markErr != nil {
|
||||||
|
return markErr
|
||||||
|
}
|
||||||
|
logx.Info(ctx, "notice_room_kick_delivered",
|
||||||
|
slog.String("event_id", event.EventID),
|
||||||
|
slog.String("app_code", event.AppCode),
|
||||||
|
slog.String("room_id", event.RoomID),
|
||||||
|
slog.Int64("target_user_id", event.TargetUserID),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.markFailed(ctx, event, options, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) markFailed(ctx context.Context, event RoomKickEvent, options RoomNoticeWorkerOptions, cause error) error {
|
||||||
|
nowMs := time.Now().UnixMilli()
|
||||||
|
nextRetryCount := event.RetryCount + 1
|
||||||
|
if nextRetryCount >= options.MaxRetryCount {
|
||||||
|
if err := s.repository.MarkRoomKickFailed(ctx, event, nextRetryCount, cause.Error(), nowMs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logx.Error(ctx, "notice_room_kick_dead_letter", cause,
|
||||||
|
slog.String("event_id", event.EventID),
|
||||||
|
slog.String("app_code", event.AppCode),
|
||||||
|
slog.String("room_id", event.RoomID),
|
||||||
|
slog.Int64("target_user_id", event.TargetUserID),
|
||||||
|
slog.Int("retry_count", nextRetryCount),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
nextRetryAtMS := nowMs + roomNoticeBackoff(nextRetryCount, options).Milliseconds()
|
||||||
|
if err := s.repository.MarkRoomKickRetryable(ctx, event, nextRetryCount, nextRetryAtMS, cause.Error(), nowMs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logx.Error(ctx, "notice_room_kick_retryable", cause,
|
||||||
|
slog.String("event_id", event.EventID),
|
||||||
|
slog.String("app_code", event.AppCode),
|
||||||
|
slog.String("room_id", event.RoomID),
|
||||||
|
slog.Int64("target_user_id", event.TargetUserID),
|
||||||
|
slog.Int("retry_count", nextRetryCount),
|
||||||
|
slog.Int64("next_retry_at_ms", nextRetryAtMS),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomKickNoticePayload(event RoomKickEvent) (json.RawMessage, error) {
|
||||||
|
payload := map[string]any{
|
||||||
|
"event_id": event.EventID,
|
||||||
|
"event_type": "room_user_kicked",
|
||||||
|
"source_event_type": event.EventType,
|
||||||
|
"app_code": event.AppCode,
|
||||||
|
"room_id": event.RoomID,
|
||||||
|
"room_version": event.RoomVersion,
|
||||||
|
"actor_user_id": event.ActorUserID,
|
||||||
|
"target_user_id": event.TargetUserID,
|
||||||
|
"occurred_at_ms": event.OccurredAtMS,
|
||||||
|
"source_created_at_ms": event.CreatedAtMS,
|
||||||
|
}
|
||||||
|
return json.Marshal(payload)
|
||||||
|
}
|
||||||
@ -0,0 +1,145 @@
|
|||||||
|
package roomnotice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/tencentim"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProcessRoomKickNoticesPublishesPrivateIM(t *testing.T) {
|
||||||
|
repo := &fakeRepository{
|
||||||
|
events: []RoomKickEvent{{
|
||||||
|
AppCode: "lalu",
|
||||||
|
EventID: "evt-room-kick-1",
|
||||||
|
EventType: "RoomUserKicked",
|
||||||
|
RoomID: "room_10001",
|
||||||
|
RoomVersion: 18,
|
||||||
|
ActorUserID: 10001,
|
||||||
|
TargetUserID: 20001,
|
||||||
|
OccurredAtMS: 1710000000123,
|
||||||
|
CreatedAtMS: 1710000000124,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
publisher := &fakePublisher{}
|
||||||
|
service := New(Config{NodeID: "notice-test"}, repo, publisher)
|
||||||
|
|
||||||
|
processed, err := service.ProcessRoomKickNotices(context.Background(), RoomNoticeWorkerOptions{
|
||||||
|
WorkerID: "room-notice-test",
|
||||||
|
BatchSize: 10,
|
||||||
|
LockTTL: time.Second,
|
||||||
|
PublishTimeout: time.Second,
|
||||||
|
MaxRetryCount: 3,
|
||||||
|
InitialBackoff: time.Second,
|
||||||
|
MaxBackoff: time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessRoomKickNotices failed: %v", err)
|
||||||
|
}
|
||||||
|
if processed != 1 {
|
||||||
|
t.Fatalf("processed=%d, want 1", processed)
|
||||||
|
}
|
||||||
|
if len(publisher.messages) != 1 {
|
||||||
|
t.Fatalf("published messages=%d, want 1", len(publisher.messages))
|
||||||
|
}
|
||||||
|
message := publisher.messages[0]
|
||||||
|
if message.ToAccount != "20001" || message.EventID != "evt-room-kick-1" || message.Ext != "room_notice" || message.Desc != "room_user_kicked" {
|
||||||
|
t.Fatalf("unexpected message: %+v", message)
|
||||||
|
}
|
||||||
|
if repo.deliveredEventID != "evt-room-kick-1" {
|
||||||
|
t.Fatalf("delivered event id=%q", repo.deliveredEventID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.Unmarshal(message.PayloadJSON, &payload); err != nil {
|
||||||
|
t.Fatalf("decode payload: %v", err)
|
||||||
|
}
|
||||||
|
if payload["event_type"] != "room_user_kicked" || payload["room_id"] != "room_10001" {
|
||||||
|
t.Fatalf("unexpected payload: %+v", payload)
|
||||||
|
}
|
||||||
|
if payload["target_user_id"].(float64) != 20001 || payload["actor_user_id"].(float64) != 10001 {
|
||||||
|
t.Fatalf("unexpected user payload: %+v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessRoomKickNoticesMarksRetryableOnPublishError(t *testing.T) {
|
||||||
|
repo := &fakeRepository{
|
||||||
|
events: []RoomKickEvent{{
|
||||||
|
AppCode: "lalu",
|
||||||
|
EventID: "evt-room-kick-2",
|
||||||
|
EventType: "RoomUserKicked",
|
||||||
|
RoomID: "room_10001",
|
||||||
|
ActorUserID: 10001,
|
||||||
|
TargetUserID: 20001,
|
||||||
|
RetryCount: 1,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
service := New(Config{NodeID: "notice-test"}, repo, &fakePublisher{err: errors.New("im timeout")})
|
||||||
|
|
||||||
|
processed, err := service.ProcessRoomKickNotices(context.Background(), RoomNoticeWorkerOptions{
|
||||||
|
WorkerID: "room-notice-test",
|
||||||
|
BatchSize: 10,
|
||||||
|
LockTTL: time.Second,
|
||||||
|
PublishTimeout: time.Second,
|
||||||
|
MaxRetryCount: 3,
|
||||||
|
InitialBackoff: time.Second,
|
||||||
|
MaxBackoff: time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessRoomKickNotices failed: %v", err)
|
||||||
|
}
|
||||||
|
if processed != 1 {
|
||||||
|
t.Fatalf("processed=%d, want 1", processed)
|
||||||
|
}
|
||||||
|
if repo.retryEventID != "evt-room-kick-2" || repo.retryCount != 2 {
|
||||||
|
t.Fatalf("retry marker event=%q count=%d", repo.retryEventID, repo.retryCount)
|
||||||
|
}
|
||||||
|
if repo.failedEventID != "" {
|
||||||
|
t.Fatalf("event should be retryable, got failed=%q", repo.failedEventID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeRepository struct {
|
||||||
|
events []RoomKickEvent
|
||||||
|
deliveredEventID string
|
||||||
|
retryEventID string
|
||||||
|
failedEventID string
|
||||||
|
retryCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fakeRepository) ClaimRoomKickEvents(context.Context, string, int, time.Duration) ([]RoomKickEvent, error) {
|
||||||
|
return r.events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fakeRepository) MarkRoomKickDelivered(_ context.Context, event RoomKickEvent, _ []byte, _ int64) error {
|
||||||
|
r.deliveredEventID = event.EventID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fakeRepository) MarkRoomKickRetryable(_ context.Context, event RoomKickEvent, retryCount int, _ int64, _ string, _ int64) error {
|
||||||
|
r.retryEventID = event.EventID
|
||||||
|
r.retryCount = retryCount
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fakeRepository) MarkRoomKickFailed(_ context.Context, event RoomKickEvent, retryCount int, _ string, _ int64) error {
|
||||||
|
r.failedEventID = event.EventID
|
||||||
|
r.retryCount = retryCount
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakePublisher struct {
|
||||||
|
messages []tencentim.CustomUserMessage
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *fakePublisher) PublishUserCustomMessage(_ context.Context, message tencentim.CustomUserMessage) error {
|
||||||
|
if p.err != nil {
|
||||||
|
return p.err
|
||||||
|
}
|
||||||
|
p.messages = append(p.messages, message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
package roomnotice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RoomNoticeWorkerOptions 控制房间私有通知 worker 的运行参数。
|
||||||
|
type RoomNoticeWorkerOptions struct {
|
||||||
|
WorkerID string
|
||||||
|
PollInterval time.Duration
|
||||||
|
BatchSize int
|
||||||
|
LockTTL time.Duration
|
||||||
|
PublishTimeout time.Duration
|
||||||
|
MaxRetryCount int
|
||||||
|
InitialBackoff time.Duration
|
||||||
|
MaxBackoff time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunRoomKickWorker 持续消费 room_outbox 中的 RoomUserKicked 事件。
|
||||||
|
func (s *Service) RunRoomKickWorker(ctx context.Context, options RoomNoticeWorkerOptions) {
|
||||||
|
options = normalizeRoomNoticeWorkerOptions(options, s.cfg.NodeID)
|
||||||
|
ticker := time.NewTicker(options.PollInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
processed, err := s.ProcessRoomKickNotices(ctx, options)
|
||||||
|
if err != nil && ctx.Err() == nil {
|
||||||
|
logx.Error(ctx, "notice_room_kick_batch_failed", err, slog.String("worker_id", options.WorkerID))
|
||||||
|
}
|
||||||
|
if processed >= options.BatchSize {
|
||||||
|
// 批次打满说明有积压,立即继续 drain,降低被踢用户退出提示延迟。
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRoomNoticeWorkerOptions(options RoomNoticeWorkerOptions, nodeID string) RoomNoticeWorkerOptions {
|
||||||
|
if strings.TrimSpace(options.WorkerID) == "" {
|
||||||
|
if strings.TrimSpace(nodeID) == "" {
|
||||||
|
nodeID = "notice"
|
||||||
|
}
|
||||||
|
options.WorkerID = "room-notice-" + nodeID
|
||||||
|
}
|
||||||
|
if options.PollInterval <= 0 {
|
||||||
|
options.PollInterval = time.Second
|
||||||
|
}
|
||||||
|
if options.BatchSize <= 0 {
|
||||||
|
options.BatchSize = 100
|
||||||
|
}
|
||||||
|
if options.LockTTL <= 0 {
|
||||||
|
options.LockTTL = 30 * time.Second
|
||||||
|
}
|
||||||
|
if options.PublishTimeout <= 0 {
|
||||||
|
options.PublishTimeout = 3 * time.Second
|
||||||
|
}
|
||||||
|
if options.MaxRetryCount <= 0 {
|
||||||
|
options.MaxRetryCount = 10
|
||||||
|
}
|
||||||
|
if options.InitialBackoff <= 0 {
|
||||||
|
options.InitialBackoff = 5 * time.Second
|
||||||
|
}
|
||||||
|
if options.MaxBackoff <= 0 {
|
||||||
|
options.MaxBackoff = 5 * time.Minute
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomNoticeBackoff(retryCount int, options RoomNoticeWorkerOptions) time.Duration {
|
||||||
|
if retryCount <= 0 {
|
||||||
|
return options.InitialBackoff
|
||||||
|
}
|
||||||
|
backoff := options.InitialBackoff
|
||||||
|
for i := 1; i < retryCount; i++ {
|
||||||
|
if backoff >= options.MaxBackoff/2 {
|
||||||
|
return options.MaxBackoff
|
||||||
|
}
|
||||||
|
backoff *= 2
|
||||||
|
}
|
||||||
|
if backoff > options.MaxBackoff {
|
||||||
|
return options.MaxBackoff
|
||||||
|
}
|
||||||
|
return backoff
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS rooms (
|
|||||||
seat_count INT NOT NULL,
|
seat_count INT NOT NULL,
|
||||||
mode VARCHAR(64) NOT NULL,
|
mode VARCHAR(64) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
|
room_password_hash VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
visible_region_id BIGINT NOT NULL DEFAULT 0,
|
visible_region_id BIGINT NOT NULL DEFAULT 0,
|
||||||
created_at_ms BIGINT NOT NULL,
|
created_at_ms BIGINT NOT NULL,
|
||||||
updated_at_ms BIGINT NOT NULL,
|
updated_at_ms BIGINT NOT NULL,
|
||||||
@ -33,6 +34,7 @@ CREATE TABLE IF NOT EXISTS room_list_entries (
|
|||||||
cover_url VARCHAR(512) NOT NULL DEFAULT '',
|
cover_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
mode VARCHAR(64) NOT NULL,
|
mode VARCHAR(64) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
|
locked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
heat BIGINT NOT NULL DEFAULT 0,
|
heat BIGINT NOT NULL DEFAULT 0,
|
||||||
online_count INT NOT NULL DEFAULT 0,
|
online_count INT NOT NULL DEFAULT 0,
|
||||||
seat_count INT NOT NULL DEFAULT 0,
|
seat_count INT NOT NULL DEFAULT 0,
|
||||||
@ -121,6 +123,38 @@ CREATE TABLE IF NOT EXISTS room_user_feed_entries (
|
|||||||
KEY idx_room_user_feed_list (app_code, user_id, feed_type, updated_at_ms DESC, room_id)
|
KEY idx_room_user_feed_list (app_code, user_id, feed_type, updated_at_ms DESC, room_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS room_follows (
|
||||||
|
app_code VARCHAR(32) NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
room_id VARCHAR(64) NOT NULL,
|
||||||
|
status VARCHAR(32) NOT NULL,
|
||||||
|
followed_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (app_code, user_id, room_id),
|
||||||
|
KEY idx_room_follows_user_list (app_code, user_id, status, followed_at_ms DESC, room_id),
|
||||||
|
KEY idx_room_follows_room (app_code, room_id, status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS room_region_pins (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
app_code VARCHAR(32) NOT NULL,
|
||||||
|
visible_region_id BIGINT NOT NULL,
|
||||||
|
room_id VARCHAR(64) NOT NULL,
|
||||||
|
weight BIGINT NOT NULL DEFAULT 0,
|
||||||
|
status VARCHAR(32) NOT NULL,
|
||||||
|
pinned_at_ms BIGINT NOT NULL,
|
||||||
|
expires_at_ms BIGINT NOT NULL,
|
||||||
|
cancelled_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
cancelled_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_room_region_pins_room (app_code, visible_region_id, room_id),
|
||||||
|
KEY idx_room_region_pins_active (app_code, visible_region_id, status, expires_at_ms, weight DESC, room_id),
|
||||||
|
KEY idx_room_region_pins_room (app_code, room_id, status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS room_seat_configs (
|
CREATE TABLE IF NOT EXISTS room_seat_configs (
|
||||||
app_code VARCHAR(32) NOT NULL,
|
app_code VARCHAR(32) NOT NULL,
|
||||||
allowed_seat_counts VARCHAR(128) NOT NULL,
|
allowed_seat_counts VARCHAR(128) NOT NULL,
|
||||||
|
|||||||
@ -185,6 +185,15 @@ func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.Room
|
|||||||
base.ActorUserID = body.GetActorUserId()
|
base.ActorUserID = body.GetActorUserId()
|
||||||
base.Attributes = map[string]string{"enabled": fmt.Sprintf("%t", body.GetEnabled())}
|
base.Attributes = map[string]string{"enabled": fmt.Sprintf("%t", body.GetEnabled())}
|
||||||
return base, true, nil
|
return base, true, nil
|
||||||
|
case "RoomPasswordChanged":
|
||||||
|
// 锁房事件只广播布尔状态,密码明文和哈希都不会进入 IM 自定义消息。
|
||||||
|
var body roomeventsv1.RoomPasswordChanged
|
||||||
|
if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil {
|
||||||
|
return tencentim.RoomEvent{}, false, err
|
||||||
|
}
|
||||||
|
base.ActorUserID = body.GetActorUserId()
|
||||||
|
base.Attributes = map[string]string{"locked": fmt.Sprintf("%t", body.GetLocked())}
|
||||||
|
return base, true, nil
|
||||||
case "RoomAdminChanged":
|
case "RoomAdminChanged":
|
||||||
// 管理员变更进入系统消息,客户端收到后仍应以最新 snapshot 修正本地权限 UI。
|
// 管理员变更进入系统消息,客户端收到后仍应以最新 snapshot 修正本地权限 UI。
|
||||||
var body roomeventsv1.RoomAdminChanged
|
var body roomeventsv1.RoomAdminChanged
|
||||||
@ -259,6 +268,8 @@ func eventTypeForClient(eventType string) string {
|
|||||||
return "room_profile_updated"
|
return "room_profile_updated"
|
||||||
case "RoomChatEnabledChanged":
|
case "RoomChatEnabledChanged":
|
||||||
return "room_chat_enabled_changed"
|
return "room_chat_enabled_changed"
|
||||||
|
case "RoomPasswordChanged":
|
||||||
|
return "room_password_changed"
|
||||||
case "RoomAdminChanged":
|
case "RoomAdminChanged":
|
||||||
return "room_admin_changed"
|
return "room_admin_changed"
|
||||||
case "RoomUserMuted":
|
case "RoomUserMuted":
|
||||||
|
|||||||
@ -241,6 +241,20 @@ type SetChatEnabled struct {
|
|||||||
// Type 返回命令类型。
|
// Type 返回命令类型。
|
||||||
func (SetChatEnabled) Type() string { return "set_chat_enabled" }
|
func (SetChatEnabled) Type() string { return "set_chat_enabled" }
|
||||||
|
|
||||||
|
// SetRoomPassword 定义房主设置或清空入房密码的请求。
|
||||||
|
type SetRoomPassword struct {
|
||||||
|
Base
|
||||||
|
// Locked 为 false 表示清空密码;true 表示开启入房密码校验。
|
||||||
|
Locked bool `json:"locked"`
|
||||||
|
// Password 只在当前进程内参与 command_id 冲突判断,序列化时必须省略,避免明文进入 command log。
|
||||||
|
Password string `json:"-"`
|
||||||
|
// PasswordHash 是服务端生成的密码哈希,command log 不能保存客户端明文。
|
||||||
|
PasswordHash string `json:"password_hash,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type 返回命令类型。
|
||||||
|
func (SetRoomPassword) Type() string { return "set_room_password" }
|
||||||
|
|
||||||
// SetRoomAdmin 定义添加或移除管理员请求。
|
// SetRoomAdmin 定义添加或移除管理员请求。
|
||||||
type SetRoomAdmin struct {
|
type SetRoomAdmin struct {
|
||||||
Base
|
Base
|
||||||
@ -360,6 +374,8 @@ func IdempotencyPayload(payload []byte) ([]byte, error) {
|
|||||||
delete(values, "gift_point_added")
|
delete(values, "gift_point_added")
|
||||||
delete(values, "heat_value")
|
delete(values, "heat_value")
|
||||||
delete(values, "price_version")
|
delete(values, "price_version")
|
||||||
|
// SetRoomPassword 的哈希带随机盐;通用 payload 比较先去掉哈希,service 层再用 transient 明文和已存 bcrypt 哈希确认是否同一密码。
|
||||||
|
delete(values, "password_hash")
|
||||||
// MicUp 的发流确认 deadline 由 room-service 生成,客户端重试同一 command_id 时不能因此冲突。
|
// MicUp 的发流确认 deadline 由 room-service 生成,客户端重试同一 command_id 时不能因此冲突。
|
||||||
delete(values, "publish_deadline_ms")
|
delete(values, "publish_deadline_ms")
|
||||||
|
|
||||||
@ -400,6 +416,8 @@ func Deserialize(commandType string, payload []byte) (Command, error) {
|
|||||||
cmd = &SetMicSeatLock{}
|
cmd = &SetMicSeatLock{}
|
||||||
case SetChatEnabled{}.Type():
|
case SetChatEnabled{}.Type():
|
||||||
cmd = &SetChatEnabled{}
|
cmd = &SetChatEnabled{}
|
||||||
|
case SetRoomPassword{}.Type():
|
||||||
|
cmd = &SetRoomPassword{}
|
||||||
case SetRoomAdmin{}.Type():
|
case SetRoomAdmin{}.Type():
|
||||||
cmd = &SetRoomAdmin{}
|
cmd = &SetRoomAdmin{}
|
||||||
case MuteUser{}.Type():
|
case MuteUser{}.Type():
|
||||||
|
|||||||
@ -188,14 +188,15 @@ func (s *Service) CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest)
|
|||||||
// 首次创建按 meta -> command log -> outbox -> snapshot 的顺序落盘,保证恢复有基础元数据和首版本命令。
|
// 首次创建按 meta -> command log -> outbox -> snapshot 的顺序落盘,保证恢复有基础元数据和首版本命令。
|
||||||
saveMetaStartedAt := time.Now()
|
saveMetaStartedAt := time.Now()
|
||||||
if err := s.repository.SaveRoomMeta(ctx, RoomMeta{
|
if err := s.repository.SaveRoomMeta(ctx, RoomMeta{
|
||||||
AppCode: cmd.AppCode,
|
AppCode: cmd.AppCode,
|
||||||
RoomID: cmd.RoomID(),
|
RoomID: cmd.RoomID(),
|
||||||
OwnerUserID: cmd.OwnerUserID,
|
OwnerUserID: cmd.OwnerUserID,
|
||||||
SeatCount: cmd.SeatCount,
|
SeatCount: cmd.SeatCount,
|
||||||
Mode: cmd.Mode,
|
Mode: cmd.Mode,
|
||||||
Status: state.RoomStatusActive,
|
Status: state.RoomStatusActive,
|
||||||
VisibleRegionID: cmd.VisibleRegionID,
|
RoomPasswordHash: "",
|
||||||
RoomShortID: cmd.RoomShortID,
|
VisibleRegionID: cmd.VisibleRegionID,
|
||||||
|
RoomShortID: cmd.RoomShortID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,9 +32,11 @@ func (s *Service) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoom
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
|
// 没有用户当前房间投影时直接返回空结果,不扫描房间列表或所有 Room Cell。
|
||||||
return emptyCurrentRoomResponse(serverTimeMS), nil
|
return emptyCurrentRoomResponse(serverTimeMS), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// presence 只是恢复入口的索引,必须用 Room Cell 快照确认房间仍 active 且用户仍在房间内。
|
||||||
snapshot, err := s.currentSnapshot(ctx, presence.RoomID)
|
snapshot, err := s.currentSnapshot(ctx, presence.RoomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -54,6 +56,7 @@ func (s *Service) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoom
|
|||||||
micSessionID := ""
|
micSessionID := ""
|
||||||
needRTCToken := false
|
needRTCToken := false
|
||||||
if seat != nil {
|
if seat != nil {
|
||||||
|
// 上麦用户回前台需要 RTC token 恢复媒体链路;普通房间用户只需要重新加 IM 群。
|
||||||
publishState = seat.GetPublishState()
|
publishState = seat.GetPublishState()
|
||||||
micSessionID = seat.GetMicSessionId()
|
micSessionID = seat.GetMicSessionId()
|
||||||
needRTCToken = publishState == state.MicPublishPending || publishState == state.MicPublishPublishing
|
needRTCToken = publishState == state.MicPublishPending || publishState == state.MicPublishPublishing
|
||||||
@ -72,15 +75,19 @@ func (s *Service) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoom
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// emptyCurrentRoomResponse 保留 server_time_ms,方便客户端用同一套时钟校正逻辑处理空结果。
|
||||||
func emptyCurrentRoomResponse(serverTimeMS int64) *roomv1.GetCurrentRoomResponse {
|
func emptyCurrentRoomResponse(serverTimeMS int64) *roomv1.GetCurrentRoomResponse {
|
||||||
return &roomv1.GetCurrentRoomResponse{ServerTimeMs: serverTimeMS}
|
return &roomv1.GetCurrentRoomResponse{ServerTimeMs: serverTimeMS}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// currentRoomSnapshotRestorable 判定读模型指向的房间是否仍可被客户端恢复。
|
||||||
func currentRoomSnapshotRestorable(snapshot *roomv1.RoomSnapshot, userID int64) bool {
|
func currentRoomSnapshotRestorable(snapshot *roomv1.RoomSnapshot, userID int64) bool {
|
||||||
if snapshot == nil || snapshot.GetRoomId() == "" || snapshot.GetStatus() != state.RoomStatusActive {
|
if snapshot == nil || snapshot.GetRoomId() == "" || snapshot.GetStatus() != state.RoomStatusActive {
|
||||||
|
// 非 active 房间不能恢复,即使用户级 presence 投影还没被清理。
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if containsUserID(snapshot.GetBanUserIds(), userID) {
|
if containsUserID(snapshot.GetBanUserIds(), userID) {
|
||||||
|
// ban 优先于旧 presence,避免被踢用户通过本地恢复入口重新收消息。
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return findProtoUser(snapshot, userID) != nil
|
return findProtoUser(snapshot, userID) != nil
|
||||||
@ -102,11 +109,14 @@ func (s *Service) projectRoomPresenceBestEffort(ctx context.Context, snapshot *r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// roomPresenceProjectionFromSnapshot 把 Room Cell 快照压缩成用户当前房间读模型。
|
||||||
|
// 读模型只保存恢复入口需要的字段,不复制完整 RoomState,也不作为权限权威来源。
|
||||||
func roomPresenceProjectionFromSnapshot(snapshot *roomv1.RoomSnapshot, updatedAtMS int64) RoomPresenceSnapshot {
|
func roomPresenceProjectionFromSnapshot(snapshot *roomv1.RoomSnapshot, updatedAtMS int64) RoomPresenceSnapshot {
|
||||||
if snapshot == nil || snapshot.GetRoomId() == "" {
|
if snapshot == nil || snapshot.GetRoomId() == "" {
|
||||||
return RoomPresenceSnapshot{}
|
return RoomPresenceSnapshot{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先按 user_id 建立麦位索引,投影用户时能补齐发流状态而不做双层循环。
|
||||||
seatsByUser := make(map[int64]*roomv1.SeatState, len(snapshot.GetMicSeats()))
|
seatsByUser := make(map[int64]*roomv1.SeatState, len(snapshot.GetMicSeats()))
|
||||||
for _, seat := range snapshot.GetMicSeats() {
|
for _, seat := range snapshot.GetMicSeats() {
|
||||||
if seat.GetUserId() > 0 {
|
if seat.GetUserId() > 0 {
|
||||||
@ -116,6 +126,7 @@ func roomPresenceProjectionFromSnapshot(snapshot *roomv1.RoomSnapshot, updatedAt
|
|||||||
|
|
||||||
users := make([]RoomPresence, 0, len(snapshot.GetOnlineUsers()))
|
users := make([]RoomPresence, 0, len(snapshot.GetOnlineUsers()))
|
||||||
if snapshot.GetStatus() == state.RoomStatusActive {
|
if snapshot.GetStatus() == state.RoomStatusActive {
|
||||||
|
// 只有 active 房间才投影 active 用户;关闭房间通过空 users 让 repository 收敛旧 presence。
|
||||||
for _, user := range snapshot.GetOnlineUsers() {
|
for _, user := range snapshot.GetOnlineUsers() {
|
||||||
if user.GetUserId() <= 0 {
|
if user.GetUserId() <= 0 {
|
||||||
continue
|
continue
|
||||||
@ -131,6 +142,7 @@ func roomPresenceProjectionFromSnapshot(snapshot *roomv1.RoomSnapshot, updatedAt
|
|||||||
UpdatedAtMS: updatedAtMS,
|
UpdatedAtMS: updatedAtMS,
|
||||||
}
|
}
|
||||||
if seat := seatsByUser[user.GetUserId()]; seat != nil {
|
if seat := seatsByUser[user.GetUserId()]; seat != nil {
|
||||||
|
// 发流状态只对当前上麦用户有意义,非上麦用户保持空字段。
|
||||||
entry.PublishState = seat.GetPublishState()
|
entry.PublishState = seat.GetPublishState()
|
||||||
entry.MicSessionID = seat.GetMicSessionId()
|
entry.MicSessionID = seat.GetMicSessionId()
|
||||||
}
|
}
|
||||||
@ -148,6 +160,7 @@ func roomPresenceProjectionFromSnapshot(snapshot *roomv1.RoomSnapshot, updatedAt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// containsUserID 是当前房间恢复入口的本地集合查询,独立保留便于后续替换为二分查找。
|
||||||
func containsUserID(values []int64, target int64) bool {
|
func containsUserID(values []int64, target int64) bool {
|
||||||
return slices.Contains(values, target)
|
return slices.Contains(values, target)
|
||||||
}
|
}
|
||||||
|
|||||||
85
services/room-service/internal/room/service/follow.go
Normal file
85
services/room-service/internal/room/service/follow.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
|
"hyapp/pkg/roomid"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FollowRoom 建立当前用户对房间的关注关系。
|
||||||
|
// 关注关系是用户个性化读模型事实,不改变 Room Cell 核心状态,也不需要 Redis lease 串行化。
|
||||||
|
func (s *Service) FollowRoom(ctx context.Context, req *roomv1.FollowRoomRequest) (*roomv1.FollowRoomResponse, error) {
|
||||||
|
ctx = contextFromMeta(ctx, req.GetMeta())
|
||||||
|
roomID, userID, err := normalizeRoomFollowInput(req.GetMeta(), req.GetRoomId(), req.GetUserId())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, exists, err := s.repository.GetRoomMeta(ctx, roomID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return nil, xerr.New(xerr.NotFound, "room not found")
|
||||||
|
}
|
||||||
|
if isClosedRoomStatus(meta.Status) {
|
||||||
|
// 已关闭房间不再建立新的关注关系;历史关注仍可通过 UnfollowRoom 幂等取消。
|
||||||
|
return nil, xerr.New(xerr.RoomClosed, "room closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
nowMS := s.clock.Now().UnixMilli()
|
||||||
|
record, err := s.repository.FollowRoom(ctx, userID, roomID, nowMS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &roomv1.FollowRoomResponse{
|
||||||
|
RoomId: roomID,
|
||||||
|
Followed: true,
|
||||||
|
FollowedAtMs: record.FollowedAtMS,
|
||||||
|
ServerTimeMs: nowMS,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnfollowRoom 取消当前用户对房间的关注关系。
|
||||||
|
// 未关注或历史房间已关闭都按幂等成功处理,方便客户端在本地状态不确定时直接收敛。
|
||||||
|
func (s *Service) UnfollowRoom(ctx context.Context, req *roomv1.UnfollowRoomRequest) (*roomv1.UnfollowRoomResponse, error) {
|
||||||
|
ctx = contextFromMeta(ctx, req.GetMeta())
|
||||||
|
roomID, userID, err := normalizeRoomFollowInput(req.GetMeta(), req.GetRoomId(), req.GetUserId())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nowMS := s.clock.Now().UnixMilli()
|
||||||
|
if err := s.repository.UnfollowRoom(ctx, userID, roomID, nowMS); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &roomv1.UnfollowRoomResponse{
|
||||||
|
RoomId: roomID,
|
||||||
|
Followed: false,
|
||||||
|
ServerTimeMs: nowMS,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRoomFollowInput(meta *roomv1.RequestMeta, rawRoomID string, userID int64) (string, int64, error) {
|
||||||
|
roomID := strings.TrimSpace(rawRoomID)
|
||||||
|
metaRoomID := strings.TrimSpace(meta.GetRoomId())
|
||||||
|
if roomID == "" {
|
||||||
|
roomID = metaRoomID
|
||||||
|
}
|
||||||
|
if metaRoomID != "" && roomID != metaRoomID {
|
||||||
|
// meta.room_id 是链路和审计字段;与 body/path room_id 不一致时必须拒绝,避免写错关系。
|
||||||
|
return "", 0, xerr.New(xerr.InvalidArgument, "room_id is invalid")
|
||||||
|
}
|
||||||
|
if !roomid.ValidStringID(roomID) {
|
||||||
|
return "", 0, xerr.New(xerr.InvalidArgument, "room_id is invalid")
|
||||||
|
}
|
||||||
|
if userID <= 0 {
|
||||||
|
return "", 0, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||||||
|
}
|
||||||
|
return roomID, userID, nil
|
||||||
|
}
|
||||||
128
services/room-service/internal/room/service/follow_test.go
Normal file
128
services/room-service/internal/room/service/follow_test.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package service_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/services/room-service/internal/integration"
|
||||||
|
roomservice "hyapp/services/room-service/internal/room/service"
|
||||||
|
"hyapp/services/room-service/internal/router"
|
||||||
|
"hyapp/services/room-service/internal/testutil/mysqltest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type followTestWallet struct{}
|
||||||
|
|
||||||
|
func (followTestWallet) DebitGift(context.Context, *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error) {
|
||||||
|
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-follow-test"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoomFollowAffectsSnapshotAndFollowedFeed(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
svc := roomservice.New(roomservice.Config{
|
||||||
|
NodeID: "node-follow-test",
|
||||||
|
LeaseTTL: 10 * time.Second,
|
||||||
|
RankLimit: 20,
|
||||||
|
SnapshotEveryN: 1,
|
||||||
|
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
||||||
|
|
||||||
|
roomID := "room-follow-flow"
|
||||||
|
ownerID := int64(1001)
|
||||||
|
viewerID := int64(2002)
|
||||||
|
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
||||||
|
SeatCount: 10,
|
||||||
|
Mode: "voice",
|
||||||
|
VisibleRegionId: 1001,
|
||||||
|
RoomName: "Follow Flow",
|
||||||
|
RoomShortId: "follow-flow",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("create room failed: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
||||||
|
Role: "audience",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("join room failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeFollow, err := svc.GetRoomSnapshot(ctx, &roomv1.GetRoomSnapshotRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
|
||||||
|
RoomId: roomID,
|
||||||
|
ViewerUserId: viewerID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("snapshot before follow failed: %v", err)
|
||||||
|
}
|
||||||
|
if beforeFollow.GetIsFollowed() {
|
||||||
|
t.Fatalf("new viewer must not be followed before FollowRoom")
|
||||||
|
}
|
||||||
|
|
||||||
|
followResp, err := svc.FollowRoom(ctx, &roomv1.FollowRoomRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
|
||||||
|
RoomId: roomID,
|
||||||
|
UserId: viewerID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("follow room failed: %v", err)
|
||||||
|
}
|
||||||
|
if !followResp.GetFollowed() || followResp.GetFollowedAtMs() <= 0 {
|
||||||
|
t.Fatalf("follow response mismatch: %+v", followResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterFollow, err := svc.GetRoomSnapshot(ctx, &roomv1.GetRoomSnapshotRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
|
||||||
|
RoomId: roomID,
|
||||||
|
ViewerUserId: viewerID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("snapshot after follow failed: %v", err)
|
||||||
|
}
|
||||||
|
if !afterFollow.GetIsFollowed() {
|
||||||
|
t.Fatalf("snapshot must expose is_followed=true after FollowRoom")
|
||||||
|
}
|
||||||
|
|
||||||
|
feedResp, err := svc.ListRoomFeeds(ctx, &roomv1.ListRoomFeedsRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
|
||||||
|
ViewerUserId: viewerID,
|
||||||
|
VisibleRegionId: 1001,
|
||||||
|
Tab: "followed",
|
||||||
|
Limit: 20,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("followed feed failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(feedResp.GetRooms()) != 1 || feedResp.GetRooms()[0].GetRoomId() != roomID {
|
||||||
|
t.Fatalf("followed feed must return followed room: %+v", feedResp.GetRooms())
|
||||||
|
}
|
||||||
|
|
||||||
|
unfollowResp, err := svc.UnfollowRoom(ctx, &roomv1.UnfollowRoomRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
|
||||||
|
RoomId: roomID,
|
||||||
|
UserId: viewerID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unfollow room failed: %v", err)
|
||||||
|
}
|
||||||
|
if unfollowResp.GetFollowed() {
|
||||||
|
t.Fatalf("unfollow response mismatch: %+v", unfollowResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyFeedResp, err := svc.ListRoomFeeds(ctx, &roomv1.ListRoomFeedsRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
|
||||||
|
ViewerUserId: viewerID,
|
||||||
|
VisibleRegionId: 1001,
|
||||||
|
Tab: "followed",
|
||||||
|
Limit: 20,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("followed feed after unfollow failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(emptyFeedResp.GetRooms()) != 0 {
|
||||||
|
t.Fatalf("followed feed must be empty after UnfollowRoom: %+v", emptyFeedResp.GetRooms())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,8 +15,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type loadedRoomRef struct {
|
type loadedRoomRef struct {
|
||||||
|
// AppCode 是运行态 Cell 所属租户,后台 worker 必须用它恢复 ctx。
|
||||||
AppCode string
|
AppCode string
|
||||||
RoomID string
|
// RoomID 是已装载 Room Cell 的业务房间 ID。
|
||||||
|
RoomID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) installCell(ctx context.Context, roomID string, roomCell *cell.RoomCell) {
|
func (s *Service) installCell(ctx context.Context, roomID string, roomCell *cell.RoomCell) {
|
||||||
@ -50,6 +52,7 @@ func (s *Service) loadedRoomRefs() []loadedRoomRef {
|
|||||||
|
|
||||||
func runtimeRoomKey(appCode string, roomID string) string {
|
func runtimeRoomKey(appCode string, roomID string) string {
|
||||||
appCode = appcode.Normalize(appCode)
|
appCode = appcode.Normalize(appCode)
|
||||||
|
// 使用 NUL 分隔 app_code 和 room_id,避免普通字符串拼接出现歧义。
|
||||||
return appCode + "\x00" + roomID
|
return appCode + "\x00" + roomID
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,10 +63,12 @@ func RuntimeRoomKey(appCode string, roomID string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runtimeRoomKeyFromContext(ctx context.Context, roomID string) string {
|
func runtimeRoomKeyFromContext(ctx context.Context, roomID string) string {
|
||||||
|
// 命令入口统一从 ctx 读取 app_code,避免 service 方法忘记把租户键传给 Redis lease。
|
||||||
return runtimeRoomKey(appcode.FromContext(ctx), roomID)
|
return runtimeRoomKey(appcode.FromContext(ctx), roomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) loadedRoomLease(ctx context.Context, roomRef loadedRoomRef, now time.Time) (router.Lease, bool, error) {
|
func (s *Service) loadedRoomLease(ctx context.Context, roomRef loadedRoomRef, now time.Time) (router.Lease, bool, error) {
|
||||||
|
// 后台 worker 通过 EnsureOwner 续租当前 Cell,不能在没有 lease 的情况下读取旧内存态。
|
||||||
lease, err := s.directory.EnsureOwner(ctx, runtimeRoomKey(roomRef.AppCode, roomRef.RoomID), s.nodeID, now, s.leaseTTL)
|
lease, err := s.directory.EnsureOwner(ctx, runtimeRoomKey(roomRef.AppCode, roomRef.RoomID), s.nodeID, now, s.leaseTTL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return router.Lease{}, false, err
|
return router.Lease{}, false, err
|
||||||
@ -78,10 +83,12 @@ func (s *Service) loadedRoomLease(ctx context.Context, roomRef loadedRoomRef, no
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) verifyLoadedRoomLease(ctx context.Context, roomRef loadedRoomRef, lease router.Lease, now time.Time) (bool, error) {
|
func (s *Service) verifyLoadedRoomLease(ctx context.Context, roomRef loadedRoomRef, lease router.Lease, now time.Time) (bool, error) {
|
||||||
|
// 使用同一个 lease token 做 fencing 校验,防止 worker 批处理期间 owner 已经切走。
|
||||||
return s.directory.VerifyOwner(ctx, runtimeRoomKey(roomRef.AppCode, roomRef.RoomID), s.nodeID, lease.LeaseToken, now)
|
return s.directory.VerifyOwner(ctx, runtimeRoomKey(roomRef.AppCode, roomRef.RoomID), s.nodeID, lease.LeaseToken, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runtimeRoomRef(key string) loadedRoomRef {
|
func runtimeRoomRef(key string) loadedRoomRef {
|
||||||
|
// runtime key 是内部格式;解析失败时保留 room_id,便于旧测试数据仍能被安全跳过或排查。
|
||||||
for index := 0; index < len(key); index++ {
|
for index := 0; index < len(key); index++ {
|
||||||
if key[index] == 0 {
|
if key[index] == 0 {
|
||||||
return loadedRoomRef{AppCode: appcode.Normalize(key[:index]), RoomID: key[index+1:]}
|
return loadedRoomRef{AppCode: appcode.Normalize(key[:index]), RoomID: key[index+1:]}
|
||||||
|
|||||||
108
services/room-service/internal/room/service/kick_test.go
Normal file
108
services/room-service/internal/room/service/kick_test.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package service_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/services/room-service/internal/integration"
|
||||||
|
roomservice "hyapp/services/room-service/internal/room/service"
|
||||||
|
"hyapp/services/room-service/internal/router"
|
||||||
|
"hyapp/services/room-service/internal/testutil/mysqltest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeRTCUserRemover struct {
|
||||||
|
roomID string
|
||||||
|
userID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *fakeRTCUserRemover) RemoveUserByStrRoomID(_ context.Context, roomID string, userID int64) error {
|
||||||
|
r.roomID = roomID
|
||||||
|
r.userID = userID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKickUserRemovesPresenceBanAndRTCConnection(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
rtc := &fakeRTCUserRemover{}
|
||||||
|
svc := roomservice.New(roomservice.Config{
|
||||||
|
NodeID: "node-kick-test",
|
||||||
|
LeaseTTL: 10 * time.Second,
|
||||||
|
RankLimit: 20,
|
||||||
|
SnapshotEveryN: 1,
|
||||||
|
RTCUserRemover: rtc,
|
||||||
|
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
||||||
|
|
||||||
|
roomID := "room-kick-flow"
|
||||||
|
ownerID := int64(7101)
|
||||||
|
viewerID := int64(7201)
|
||||||
|
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
||||||
|
SeatCount: 10,
|
||||||
|
Mode: "voice",
|
||||||
|
VisibleRegionId: 8101,
|
||||||
|
RoomName: "Kick Flow",
|
||||||
|
RoomShortId: "kick-flow",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("create kick room fixture failed: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
||||||
|
Role: "audience",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("join kick target failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
kickResp, err := svc.KickUser(ctx, &roomv1.KickUserRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
||||||
|
TargetUserId: viewerID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("kick user failed: %v", err)
|
||||||
|
}
|
||||||
|
if !kickResp.GetRtcKicked() || kickResp.GetRtcKickError() != "" || rtc.roomID != roomID || rtc.userID != viewerID {
|
||||||
|
t.Fatalf("kick must remove RTC user after Room Cell commit: resp=%+v rtc=%+v", kickResp, rtc)
|
||||||
|
}
|
||||||
|
if !containsInt64(kickResp.GetRoom().GetBanUserIds(), viewerID) || findRoomUser(kickResp.GetRoom(), viewerID) != nil {
|
||||||
|
t.Fatalf("kick response must ban and remove target presence: %+v", kickResp.GetRoom())
|
||||||
|
}
|
||||||
|
|
||||||
|
presenceResp, err := svc.VerifyRoomPresence(ctx, &roomv1.VerifyRoomPresenceRequest{
|
||||||
|
AppCode: appcode.Default,
|
||||||
|
RoomId: roomID,
|
||||||
|
UserId: viewerID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("verify room presence after kick failed: %v", err)
|
||||||
|
}
|
||||||
|
if presenceResp.GetPresent() || presenceResp.GetReason() != "user_banned" {
|
||||||
|
t.Fatalf("kicked user must not pass IM presence guard: %+v", presenceResp)
|
||||||
|
}
|
||||||
|
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
||||||
|
Role: "audience",
|
||||||
|
}); err == nil {
|
||||||
|
t.Fatalf("kicked user must not join before unban")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findRoomUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.RoomUser {
|
||||||
|
for _, user := range snapshot.GetOnlineUsers() {
|
||||||
|
if user.GetUserId() == userID {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsInt64(values []int64, target int64) bool {
|
||||||
|
for _, value := range values {
|
||||||
|
if value == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@ -20,10 +20,12 @@ func (s *Service) requireRoomOpenForEntry(ctx context.Context, roomID string) er
|
|||||||
if err != nil || !closed {
|
if err != nil || !closed {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// 进房入口使用专门错误码,gateway 可以稳定映射成客户端房间关闭态。
|
||||||
return xerr.New(xerr.RoomClosed, "room closed")
|
return xerr.New(xerr.RoomClosed, "room closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) isRoomClosed(ctx context.Context, roomID string) (bool, error) {
|
func (s *Service) isRoomClosed(ctx context.Context, roomID string) (bool, error) {
|
||||||
|
// 关闭状态先读 rooms 元数据,避免为了拒绝进房恢复完整 Room Cell。
|
||||||
roomMeta, exists, err := s.repository.GetRoomMeta(ctx, roomID)
|
roomMeta, exists, err := s.repository.GetRoomMeta(ctx, roomID)
|
||||||
if err != nil || !exists {
|
if err != nil || !exists {
|
||||||
return false, err
|
return false, err
|
||||||
@ -33,6 +35,7 @@ func (s *Service) isRoomClosed(ctx context.Context, roomID string) (bool, error)
|
|||||||
|
|
||||||
func isClosedRoomStatus(status string) bool {
|
func isClosedRoomStatus(status string) bool {
|
||||||
value := strings.TrimSpace(status)
|
value := strings.TrimSpace(status)
|
||||||
|
// 空状态兼容测试构造;非 active 一律按不可进入处理,closing 也不接收新用户。
|
||||||
return value != "" && !strings.EqualFold(value, state.RoomStatusActive)
|
return value != "" && !strings.EqualFold(value, state.RoomStatusActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,9 +55,11 @@ func (s *Service) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (
|
|||||||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
|
return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
|
||||||
}
|
}
|
||||||
if current.Status == state.RoomStatusClosed {
|
if current.Status == state.RoomStatusClosed {
|
||||||
|
// 关房命令幂等,重复请求只返回当前关闭快照,不重复写 RoomClosed 事件。
|
||||||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||||||
}
|
}
|
||||||
if current.Status != state.RoomStatusActive && current.Status != state.RoomStatusClosing {
|
if current.Status != state.RoomStatusActive && current.Status != state.RoomStatusClosing {
|
||||||
|
// creating 等非开放状态不允许用 CloseRoom 直接跳过创建收尾语义。
|
||||||
return mutationResult{}, nil, xerr.New(xerr.Conflict, "room is not active")
|
return mutationResult{}, nil, xerr.New(xerr.Conflict, "room is not active")
|
||||||
}
|
}
|
||||||
if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
|
if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
|
||||||
@ -69,6 +74,7 @@ func (s *Service) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (
|
|||||||
occupiedSeats := make([]occupiedMicSeat, 0, len(current.MicSeats))
|
occupiedSeats := make([]occupiedMicSeat, 0, len(current.MicSeats))
|
||||||
for _, seat := range current.MicSeats {
|
for _, seat := range current.MicSeats {
|
||||||
if seat.UserID != 0 && seat.MicSessionID != "" {
|
if seat.UserID != 0 && seat.MicSessionID != "" {
|
||||||
|
// 清场前先保存占麦会话,后续 ClearMicSession 会抹掉 mic_session_id。
|
||||||
occupiedSeats = append(occupiedSeats, occupiedMicSeat{
|
occupiedSeats = append(occupiedSeats, occupiedMicSeat{
|
||||||
userID: seat.UserID,
|
userID: seat.UserID,
|
||||||
seatNo: seat.SeatNo,
|
seatNo: seat.SeatNo,
|
||||||
@ -77,6 +83,7 @@ func (s *Service) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关房是 Room Cell 的终态变更,presence 和麦位都在同一版本下清空。
|
||||||
current.Status = state.RoomStatusClosed
|
current.Status = state.RoomStatusClosed
|
||||||
current.OnlineUsers = map[int64]*state.RoomUserState{}
|
current.OnlineUsers = map[int64]*state.RoomUserState{}
|
||||||
for index := range current.MicSeats {
|
for index := range current.MicSeats {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user