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、用户主数据、默认短号和临时靓号基础能力。
|
||||
- `activity-service` 当前承接每日任务、活动/系统消息基础能力和房间事件 consumer 边界;`room_outbox` 到 activity/audit 的实际消费链路按独立消费位点接入。
|
||||
- `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`。
|
||||
- 多 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` 由前端按用户动作生成,用于服务端幂等。
|
||||
- `room-service` 在 `CreateRoom` 成功后写 `room_outbox`,由 outbox worker 异步创建或补偿同名腾讯云 IM 群组。
|
||||
- `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`。
|
||||
|
||||
腾讯云 IM 配置分别在:
|
||||
@ -197,7 +197,7 @@ services/notice-service/ async user notice delivery workers
|
||||
- 腾讯云 IM 回调入口已经落在 gateway;线上仍必须在腾讯云 IM 控制台启用 `Group.CallbackBeforeApplyJoinGroup` 和 `Group.CallbackBeforeSendMsg`,并配置公网 callback URL 与鉴权 token。
|
||||
- 前端对同一次用户写动作的 HTTP 重试必须复用同一个 `command_id`;`X-Request-ID` 不需要也不应该由前端传入。
|
||||
- `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 的实际消费链路仍需按独立消费位点补齐。
|
||||
- `room-service` App 已启动 `room_outbox` 补偿 worker;该状态只表示腾讯云 IM 系统消息补偿结果,activity/audit 后续必须使用独立消费位点。
|
||||
- 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.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.3
|
||||
// - protoc v5.29.2
|
||||
// source: proto/activity/v1/activity.proto
|
||||
|
||||
package activityv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc v5.29.3
|
||||
// protoc-gen-go v1.34.2
|
||||
// protoc v5.29.2
|
||||
// source: proto/events/room/v1/events.proto
|
||||
|
||||
package roomeventsv1
|
||||
@ -38,9 +38,11 @@ type EventEnvelope struct {
|
||||
|
||||
func (x *EventEnvelope) Reset() {
|
||||
*x = EventEnvelope{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *EventEnvelope) String() string {
|
||||
@ -51,7 +53,7 @@ func (*EventEnvelope) ProtoMessage() {}
|
||||
|
||||
func (x *EventEnvelope) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -132,9 +134,11 @@ type RoomCreated struct {
|
||||
|
||||
func (x *RoomCreated) Reset() {
|
||||
*x = RoomCreated{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomCreated) String() string {
|
||||
@ -145,7 +149,7 @@ func (*RoomCreated) ProtoMessage() {}
|
||||
|
||||
func (x *RoomCreated) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -217,9 +221,11 @@ type RoomProfileUpdated struct {
|
||||
|
||||
func (x *RoomProfileUpdated) Reset() {
|
||||
*x = RoomProfileUpdated{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomProfileUpdated) String() string {
|
||||
@ -230,7 +236,7 @@ func (*RoomProfileUpdated) ProtoMessage() {}
|
||||
|
||||
func (x *RoomProfileUpdated) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -292,9 +298,11 @@ type RoomUserJoined struct {
|
||||
|
||||
func (x *RoomUserJoined) Reset() {
|
||||
*x = RoomUserJoined{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomUserJoined) String() string {
|
||||
@ -305,7 +313,7 @@ func (*RoomUserJoined) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserJoined) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -345,9 +353,11 @@ type RoomUserLeft struct {
|
||||
|
||||
func (x *RoomUserLeft) Reset() {
|
||||
*x = RoomUserLeft{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomUserLeft) String() string {
|
||||
@ -358,7 +368,7 @@ func (*RoomUserLeft) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserLeft) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -392,9 +402,11 @@ type RoomClosed struct {
|
||||
|
||||
func (x *RoomClosed) Reset() {
|
||||
*x = RoomClosed{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomClosed) String() string {
|
||||
@ -405,7 +417,7 @@ func (*RoomClosed) ProtoMessage() {}
|
||||
|
||||
func (x *RoomClosed) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -458,9 +470,11 @@ type RoomMicChanged struct {
|
||||
|
||||
func (x *RoomMicChanged) Reset() {
|
||||
*x = RoomMicChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomMicChanged) String() string {
|
||||
@ -471,7 +485,7 @@ func (*RoomMicChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomMicChanged) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -576,9 +590,11 @@ type RoomMicSeatLocked struct {
|
||||
|
||||
func (x *RoomMicSeatLocked) Reset() {
|
||||
*x = RoomMicSeatLocked{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomMicSeatLocked) String() string {
|
||||
@ -589,7 +605,7 @@ func (*RoomMicSeatLocked) ProtoMessage() {}
|
||||
|
||||
func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -637,9 +653,11 @@ type RoomChatEnabledChanged struct {
|
||||
|
||||
func (x *RoomChatEnabledChanged) Reset() {
|
||||
*x = RoomChatEnabledChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomChatEnabledChanged) String() string {
|
||||
@ -650,7 +668,7 @@ func (*RoomChatEnabledChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -679,6 +697,62 @@ func (x *RoomChatEnabledChanged) GetEnabled() bool {
|
||||
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 表达房间管理员集合变更。
|
||||
type RoomAdminChanged struct {
|
||||
state protoimpl.MessageState
|
||||
@ -692,9 +766,11 @@ type RoomAdminChanged struct {
|
||||
|
||||
func (x *RoomAdminChanged) Reset() {
|
||||
*x = RoomAdminChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomAdminChanged) String() string {
|
||||
@ -704,8 +780,8 @@ func (x *RoomAdminChanged) String() string {
|
||||
func (*RoomAdminChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -717,7 +793,7 @@ func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomAdminChanged.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@ -754,9 +830,11 @@ type RoomUserMuted struct {
|
||||
|
||||
func (x *RoomUserMuted) Reset() {
|
||||
*x = RoomUserMuted{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomUserMuted) String() string {
|
||||
@ -766,8 +844,8 @@ func (x *RoomUserMuted) String() string {
|
||||
func (*RoomUserMuted) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -779,7 +857,7 @@ func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomUserMuted.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@ -815,9 +893,11 @@ type RoomUserKicked struct {
|
||||
|
||||
func (x *RoomUserKicked) Reset() {
|
||||
*x = RoomUserKicked{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomUserKicked) String() string {
|
||||
@ -827,8 +907,8 @@ func (x *RoomUserKicked) String() string {
|
||||
func (*RoomUserKicked) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -840,7 +920,7 @@ func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomUserKicked.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@ -869,9 +949,11 @@ type RoomUserUnbanned struct {
|
||||
|
||||
func (x *RoomUserUnbanned) Reset() {
|
||||
*x = RoomUserUnbanned{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomUserUnbanned) String() string {
|
||||
@ -881,8 +963,8 @@ func (x *RoomUserUnbanned) String() string {
|
||||
func (*RoomUserUnbanned) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -894,7 +976,7 @@ func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomUserUnbanned.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@ -929,9 +1011,11 @@ type RoomGiftSent struct {
|
||||
|
||||
func (x *RoomGiftSent) Reset() {
|
||||
*x = RoomGiftSent{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomGiftSent) String() string {
|
||||
@ -941,8 +1025,8 @@ func (x *RoomGiftSent) String() string {
|
||||
func (*RoomGiftSent) ProtoMessage() {}
|
||||
|
||||
func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -954,7 +1038,7 @@ func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomGiftSent.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@ -1025,9 +1109,11 @@ type RoomHeatChanged struct {
|
||||
|
||||
func (x *RoomHeatChanged) Reset() {
|
||||
*x = RoomHeatChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomHeatChanged) String() string {
|
||||
@ -1037,8 +1123,8 @@ func (x *RoomHeatChanged) String() string {
|
||||
func (*RoomHeatChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||
if x != nil {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1050,7 +1136,7 @@ func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomHeatChanged.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@ -1080,9 +1166,11 @@ type RoomRankChanged struct {
|
||||
|
||||
func (x *RoomRankChanged) Reset() {
|
||||
*x = RoomRankChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RoomRankChanged) String() string {
|
||||
@ -1092,8 +1180,8 @@ func (x *RoomRankChanged) String() string {
|
||||
func (*RoomRankChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||
if x != nil {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1105,7 +1193,7 @@ func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomRankChanged.ProtoReflect.Descriptor instead.
|
||||
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 {
|
||||
@ -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,
|
||||
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,
|
||||
0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x76, 0x0a, 0x10, 0x52,
|
||||
0x6f, 0x6f, 0x6d, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 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, 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,
|
||||
0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x51, 0x0a, 0x13, 0x52,
|
||||
0x6f, 0x6f, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67,
|
||||
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, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x22, 0x76,
|
||||
0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67,
|
||||
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, 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,
|
||||
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, 0x52, 0x0c, 0x74,
|
||||
0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67,
|
||||
0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69,
|
||||
0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75,
|
||||
0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f,
|
||||
0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x65,
|
||||
0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10,
|
||||
0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x49, 0x64,
|
||||
0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69,
|
||||
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,
|
||||
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17,
|
||||
0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f,
|
||||
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66,
|
||||
0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74,
|
||||
0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67,
|
||||
0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x10, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70,
|
||||
0x74, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72,
|
||||
0x65, 0x67, 0x69, 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 (
|
||||
@ -1297,7 +1390,7 @@ func file_proto_events_room_v1_events_proto_rawDescGZIP() []byte {
|
||||
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{
|
||||
(*EventEnvelope)(nil), // 0: hyapp.events.room.v1.EventEnvelope
|
||||
(*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
|
||||
(*RoomMicSeatLocked)(nil), // 7: hyapp.events.room.v1.RoomMicSeatLocked
|
||||
(*RoomChatEnabledChanged)(nil), // 8: hyapp.events.room.v1.RoomChatEnabledChanged
|
||||
(*RoomAdminChanged)(nil), // 9: hyapp.events.room.v1.RoomAdminChanged
|
||||
(*RoomUserMuted)(nil), // 10: hyapp.events.room.v1.RoomUserMuted
|
||||
(*RoomUserKicked)(nil), // 11: hyapp.events.room.v1.RoomUserKicked
|
||||
(*RoomUserUnbanned)(nil), // 12: hyapp.events.room.v1.RoomUserUnbanned
|
||||
(*RoomGiftSent)(nil), // 13: hyapp.events.room.v1.RoomGiftSent
|
||||
(*RoomHeatChanged)(nil), // 14: hyapp.events.room.v1.RoomHeatChanged
|
||||
(*RoomRankChanged)(nil), // 15: hyapp.events.room.v1.RoomRankChanged
|
||||
(*RoomPasswordChanged)(nil), // 9: hyapp.events.room.v1.RoomPasswordChanged
|
||||
(*RoomAdminChanged)(nil), // 10: hyapp.events.room.v1.RoomAdminChanged
|
||||
(*RoomUserMuted)(nil), // 11: hyapp.events.room.v1.RoomUserMuted
|
||||
(*RoomUserKicked)(nil), // 12: hyapp.events.room.v1.RoomUserKicked
|
||||
(*RoomUserUnbanned)(nil), // 13: hyapp.events.room.v1.RoomUserUnbanned
|
||||
(*RoomGiftSent)(nil), // 14: hyapp.events.room.v1.RoomGiftSent
|
||||
(*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{
|
||||
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 {
|
||||
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{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_proto_events_room_v1_events_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 16,
|
||||
NumMessages: 17,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@ -84,6 +84,12 @@ message RoomChatEnabledChanged {
|
||||
bool enabled = 2;
|
||||
}
|
||||
|
||||
// RoomPasswordChanged 表达房间入房密码状态变更,不携带明文或哈希。
|
||||
message RoomPasswordChanged {
|
||||
int64 actor_user_id = 1;
|
||||
bool locked = 2;
|
||||
}
|
||||
|
||||
// RoomAdminChanged 表达房间管理员集合变更。
|
||||
message RoomAdminChanged {
|
||||
int64 actor_user_id = 1;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc v5.29.3
|
||||
// protoc-gen-go v1.34.2
|
||||
// protoc v5.29.2
|
||||
// source: proto/game/v1/game.proto
|
||||
|
||||
package gamev1
|
||||
@ -36,9 +36,11 @@ type RequestMeta struct {
|
||||
|
||||
func (x *RequestMeta) Reset() {
|
||||
*x = RequestMeta{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RequestMeta) String() string {
|
||||
@ -49,7 +51,7 @@ func (*RequestMeta) ProtoMessage() {}
|
||||
|
||||
func (x *RequestMeta) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -123,9 +125,11 @@ type GamePlatform struct {
|
||||
|
||||
func (x *GamePlatform) Reset() {
|
||||
*x = GamePlatform{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GamePlatform) String() string {
|
||||
@ -136,7 +140,7 @@ func (*GamePlatform) ProtoMessage() {}
|
||||
|
||||
func (x *GamePlatform) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -232,9 +236,11 @@ type GameCatalogItem struct {
|
||||
|
||||
func (x *GameCatalogItem) Reset() {
|
||||
*x = GameCatalogItem{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GameCatalogItem) String() string {
|
||||
@ -245,7 +251,7 @@ func (*GameCatalogItem) ProtoMessage() {}
|
||||
|
||||
func (x *GameCatalogItem) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -394,9 +400,11 @@ type AppGame struct {
|
||||
|
||||
func (x *AppGame) Reset() {
|
||||
*x = AppGame{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *AppGame) String() string {
|
||||
@ -407,7 +415,7 @@ func (*AppGame) ProtoMessage() {}
|
||||
|
||||
func (x *AppGame) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -529,9 +537,11 @@ type ListGamesRequest struct {
|
||||
|
||||
func (x *ListGamesRequest) Reset() {
|
||||
*x = ListGamesRequest{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ListGamesRequest) String() string {
|
||||
@ -542,7 +552,7 @@ func (*ListGamesRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListGamesRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -617,9 +627,11 @@ type ListGamesResponse struct {
|
||||
|
||||
func (x *ListGamesResponse) Reset() {
|
||||
*x = ListGamesResponse{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ListGamesResponse) String() string {
|
||||
@ -630,7 +642,7 @@ func (*ListGamesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListGamesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -674,9 +686,11 @@ type LaunchGameRequest struct {
|
||||
|
||||
func (x *LaunchGameRequest) Reset() {
|
||||
*x = LaunchGameRequest{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *LaunchGameRequest) String() string {
|
||||
@ -687,7 +701,7 @@ func (*LaunchGameRequest) ProtoMessage() {}
|
||||
|
||||
func (x *LaunchGameRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -758,9 +772,11 @@ type LaunchGameResponse struct {
|
||||
|
||||
func (x *LaunchGameResponse) Reset() {
|
||||
*x = LaunchGameResponse{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *LaunchGameResponse) String() string {
|
||||
@ -771,7 +787,7 @@ func (*LaunchGameResponse) ProtoMessage() {}
|
||||
|
||||
func (x *LaunchGameResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -837,9 +853,11 @@ type CallbackRequest struct {
|
||||
|
||||
func (x *CallbackRequest) Reset() {
|
||||
*x = CallbackRequest{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *CallbackRequest) String() string {
|
||||
@ -850,7 +868,7 @@ func (*CallbackRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CallbackRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -925,9 +943,11 @@ type CallbackResponse struct {
|
||||
|
||||
func (x *CallbackResponse) Reset() {
|
||||
*x = CallbackResponse{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *CallbackResponse) String() string {
|
||||
@ -938,7 +958,7 @@ func (*CallbackResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CallbackResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -978,9 +998,11 @@ type ListPlatformsRequest struct {
|
||||
|
||||
func (x *ListPlatformsRequest) Reset() {
|
||||
*x = ListPlatformsRequest{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ListPlatformsRequest) String() string {
|
||||
@ -991,7 +1013,7 @@ func (*ListPlatformsRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListPlatformsRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1031,9 +1053,11 @@ type ListPlatformsResponse struct {
|
||||
|
||||
func (x *ListPlatformsResponse) Reset() {
|
||||
*x = ListPlatformsResponse{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ListPlatformsResponse) String() string {
|
||||
@ -1044,7 +1068,7 @@ func (*ListPlatformsResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListPlatformsResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1084,9 +1108,11 @@ type UpsertPlatformRequest struct {
|
||||
|
||||
func (x *UpsertPlatformRequest) Reset() {
|
||||
*x = UpsertPlatformRequest{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *UpsertPlatformRequest) String() string {
|
||||
@ -1097,7 +1123,7 @@ func (*UpsertPlatformRequest) ProtoMessage() {}
|
||||
|
||||
func (x *UpsertPlatformRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1137,9 +1163,11 @@ type PlatformResponse struct {
|
||||
|
||||
func (x *PlatformResponse) Reset() {
|
||||
*x = PlatformResponse{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *PlatformResponse) String() string {
|
||||
@ -1150,7 +1178,7 @@ func (*PlatformResponse) ProtoMessage() {}
|
||||
|
||||
func (x *PlatformResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1193,9 +1221,11 @@ type ListCatalogRequest struct {
|
||||
|
||||
func (x *ListCatalogRequest) Reset() {
|
||||
*x = ListCatalogRequest{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ListCatalogRequest) String() string {
|
||||
@ -1206,7 +1236,7 @@ func (*ListCatalogRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListCatalogRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[14]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1268,9 +1298,11 @@ type ListCatalogResponse struct {
|
||||
|
||||
func (x *ListCatalogResponse) Reset() {
|
||||
*x = ListCatalogResponse{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ListCatalogResponse) String() string {
|
||||
@ -1281,7 +1313,7 @@ func (*ListCatalogResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListCatalogResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[15]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1328,9 +1360,11 @@ type UpsertCatalogRequest struct {
|
||||
|
||||
func (x *UpsertCatalogRequest) Reset() {
|
||||
*x = UpsertCatalogRequest{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[16]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[16]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *UpsertCatalogRequest) String() string {
|
||||
@ -1341,7 +1375,7 @@ func (*UpsertCatalogRequest) ProtoMessage() {}
|
||||
|
||||
func (x *UpsertCatalogRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[16]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1381,9 +1415,11 @@ type CatalogResponse struct {
|
||||
|
||||
func (x *CatalogResponse) Reset() {
|
||||
*x = CatalogResponse{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[17]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[17]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *CatalogResponse) String() string {
|
||||
@ -1394,7 +1430,7 @@ func (*CatalogResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CatalogResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[17]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1435,9 +1471,11 @@ type SetGameStatusRequest struct {
|
||||
|
||||
func (x *SetGameStatusRequest) Reset() {
|
||||
*x = SetGameStatusRequest{}
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[18]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[18]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SetGameStatusRequest) String() string {
|
||||
@ -1448,7 +1486,7 @@ func (*SetGameStatusRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SetGameStatusRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_game_v1_game_proto_msgTypes[18]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1862,6 +1900,236 @@ func file_proto_game_v1_game_proto_init() {
|
||||
if File_proto_game_v1_game_proto != nil {
|
||||
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{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.3
|
||||
// - protoc v5.29.2
|
||||
// source: proto/game/v1/game.proto
|
||||
|
||||
package gamev1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -80,6 +80,8 @@ message RoomSnapshot {
|
||||
string app_code = 16;
|
||||
string room_short_id = 17;
|
||||
int64 visible_region_id = 18;
|
||||
// locked 只表达房间是否需要入房密码;具体密码哈希不对外映射到 HTTP JSON。
|
||||
bool locked = 19;
|
||||
}
|
||||
|
||||
// CreateRoomRequest 创建一个新的房间执行单元。
|
||||
@ -129,6 +131,8 @@ message UpdateRoomProfileResponse {
|
||||
message JoinRoomRequest {
|
||||
RequestMeta meta = 1;
|
||||
string role = 2;
|
||||
// password 只用于本次入房校验;room-service 不把明文写入 command log 或快照。
|
||||
string password = 3;
|
||||
}
|
||||
|
||||
// JoinRoomResponse 返回加入后的房间快照。
|
||||
@ -295,6 +299,20 @@ message SetChatEnabledResponse {
|
||||
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 添加或移除房间管理员。
|
||||
message SetRoomAdminRequest {
|
||||
RequestMeta meta = 1;
|
||||
@ -331,6 +349,10 @@ message KickUserRequest {
|
||||
message KickUserResponse {
|
||||
CommandResult result = 1;
|
||||
RoomSnapshot room = 2;
|
||||
// rtc_kicked 表示 room-service 已通过 TRTC 服务端接口移除目标用户;失败不回滚房间踢人事实。
|
||||
bool rtc_kicked = 3;
|
||||
// rtc_kick_error 仅用于调用方排障和告警,客户端展示仍以统一错误码和房间状态为准。
|
||||
string rtc_kick_error = 4;
|
||||
}
|
||||
|
||||
// UnbanUserRequest 解除房间内 ban,用户仍需重新 JoinRoom。
|
||||
@ -429,7 +451,7 @@ message ListRoomsRequest {
|
||||
string query = 7;
|
||||
}
|
||||
|
||||
// ListRoomFeedsRequest 查询 Mine 页 visited/friend/following 用户房间流。
|
||||
// ListRoomFeedsRequest 查询 Mine 页 visited/friend/following/followed 房间流。
|
||||
// 它和公共房间发现列表分离,避免把用户关系流误建模成房间全集过滤。
|
||||
message ListRoomFeedsRequest {
|
||||
RequestMeta meta = 1;
|
||||
@ -464,6 +486,7 @@ message RoomListItem {
|
||||
int64 visible_region_id = 12;
|
||||
string app_code = 13;
|
||||
string room_short_id = 14;
|
||||
bool locked = 15;
|
||||
}
|
||||
|
||||
// ListRoomsResponse 返回一页房间卡片和下一页不透明 cursor。
|
||||
@ -520,6 +543,8 @@ message GetRoomSnapshotRequest {
|
||||
message GetRoomSnapshotResponse {
|
||||
RoomSnapshot room = 1;
|
||||
int64 server_time_ms = 2;
|
||||
// is_followed 表达 viewer 是否已经关注该房间;它来自 room_follows 关系表,不属于 Room Cell 核心快照。
|
||||
bool is_followed = 3;
|
||||
}
|
||||
|
||||
// ListRoomOnlineUsersRequest 分页查询房间在线用户,不让客户端拉完整 RoomSnapshot 做列表分页。
|
||||
@ -539,6 +564,34 @@ message ListRoomOnlineUsersResponse {
|
||||
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 承载所有会改变房间状态的命令。
|
||||
service RoomCommandService {
|
||||
rpc CreateRoom(CreateRoomRequest) returns (CreateRoomResponse);
|
||||
@ -555,12 +608,15 @@ service RoomCommandService {
|
||||
rpc ApplyRTCEvent(ApplyRTCEventRequest) returns (ApplyRTCEventResponse);
|
||||
rpc SetMicSeatLock(SetMicSeatLockRequest) returns (SetMicSeatLockResponse);
|
||||
rpc SetChatEnabled(SetChatEnabledRequest) returns (SetChatEnabledResponse);
|
||||
rpc SetRoomPassword(SetRoomPasswordRequest) returns (SetRoomPasswordResponse);
|
||||
rpc SetRoomAdmin(SetRoomAdminRequest) returns (SetRoomAdminResponse);
|
||||
rpc MuteUser(MuteUserRequest) returns (MuteUserResponse);
|
||||
rpc KickUser(KickUserRequest) returns (KickUserResponse);
|
||||
rpc UnbanUser(UnbanUserRequest) returns (UnbanUserResponse);
|
||||
rpc SystemEvictUser(SystemEvictUserRequest) returns (SystemEvictUserResponse);
|
||||
rpc SendGift(SendGiftRequest) returns (SendGiftResponse);
|
||||
rpc FollowRoom(FollowRoomRequest) returns (FollowRoomResponse);
|
||||
rpc UnfollowRoom(UnfollowRoomRequest) returns (UnfollowRoomResponse);
|
||||
}
|
||||
|
||||
// RoomGuardService 提供给腾讯云 IM 回调或 gateway 做实时守卫查询。
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.3
|
||||
// - protoc v5.29.2
|
||||
// source: proto/room/v1/room.proto
|
||||
|
||||
package roomv1
|
||||
@ -33,12 +33,15 @@ const (
|
||||
RoomCommandService_ApplyRTCEvent_FullMethodName = "/hyapp.room.v1.RoomCommandService/ApplyRTCEvent"
|
||||
RoomCommandService_SetMicSeatLock_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicSeatLock"
|
||||
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_MuteUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/MuteUser"
|
||||
RoomCommandService_KickUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/KickUser"
|
||||
RoomCommandService_UnbanUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/UnbanUser"
|
||||
RoomCommandService_SystemEvictUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/SystemEvictUser"
|
||||
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.
|
||||
@ -61,12 +64,15 @@ type RoomCommandServiceClient interface {
|
||||
ApplyRTCEvent(ctx context.Context, in *ApplyRTCEventRequest, opts ...grpc.CallOption) (*ApplyRTCEventResponse, error)
|
||||
SetMicSeatLock(ctx context.Context, in *SetMicSeatLockRequest, opts ...grpc.CallOption) (*SetMicSeatLockResponse, 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)
|
||||
MuteUser(ctx context.Context, in *MuteUserRequest, opts ...grpc.CallOption) (*MuteUserResponse, error)
|
||||
KickUser(ctx context.Context, in *KickUserRequest, opts ...grpc.CallOption) (*KickUserResponse, error)
|
||||
UnbanUser(ctx context.Context, in *UnbanUserRequest, opts ...grpc.CallOption) (*UnbanUserResponse, error)
|
||||
SystemEvictUser(ctx context.Context, in *SystemEvictUserRequest, opts ...grpc.CallOption) (*SystemEvictUserResponse, 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 {
|
||||
@ -217,6 +223,16 @@ func (c *roomCommandServiceClient) SetChatEnabled(ctx context.Context, in *SetCh
|
||||
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) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SetRoomAdminResponse)
|
||||
@ -277,6 +293,26 @@ func (c *roomCommandServiceClient) SendGift(ctx context.Context, in *SendGiftReq
|
||||
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.
|
||||
// All implementations must embed UnimplementedRoomCommandServiceServer
|
||||
// for forward compatibility.
|
||||
@ -297,12 +333,15 @@ type RoomCommandServiceServer interface {
|
||||
ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error)
|
||||
SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error)
|
||||
SetChatEnabled(context.Context, *SetChatEnabledRequest) (*SetChatEnabledResponse, error)
|
||||
SetRoomPassword(context.Context, *SetRoomPasswordRequest) (*SetRoomPasswordResponse, error)
|
||||
SetRoomAdmin(context.Context, *SetRoomAdminRequest) (*SetRoomAdminResponse, error)
|
||||
MuteUser(context.Context, *MuteUserRequest) (*MuteUserResponse, error)
|
||||
KickUser(context.Context, *KickUserRequest) (*KickUserResponse, error)
|
||||
UnbanUser(context.Context, *UnbanUserRequest) (*UnbanUserResponse, error)
|
||||
SystemEvictUser(context.Context, *SystemEvictUserRequest) (*SystemEvictUserResponse, error)
|
||||
SendGift(context.Context, *SendGiftRequest) (*SendGiftResponse, error)
|
||||
FollowRoom(context.Context, *FollowRoomRequest) (*FollowRoomResponse, error)
|
||||
UnfollowRoom(context.Context, *UnfollowRoomRequest) (*UnfollowRoomResponse, error)
|
||||
mustEmbedUnimplementedRoomCommandServiceServer()
|
||||
}
|
||||
|
||||
@ -355,6 +394,9 @@ func (UnimplementedRoomCommandServiceServer) SetMicSeatLock(context.Context, *Se
|
||||
func (UnimplementedRoomCommandServiceServer) SetChatEnabled(context.Context, *SetChatEnabledRequest) (*SetChatEnabledResponse, error) {
|
||||
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) {
|
||||
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) {
|
||||
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) testEmbeddedByValue() {}
|
||||
|
||||
@ -646,6 +694,24 @@ func _RoomCommandService_SetChatEnabled_Handler(srv interface{}, ctx context.Con
|
||||
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) {
|
||||
in := new(SetRoomAdminRequest)
|
||||
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)
|
||||
}
|
||||
|
||||
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.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -817,6 +919,10 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "SetChatEnabled",
|
||||
Handler: _RoomCommandService_SetChatEnabled_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SetRoomPassword",
|
||||
Handler: _RoomCommandService_SetRoomPassword_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SetRoomAdmin",
|
||||
Handler: _RoomCommandService_SetRoomAdmin_Handler,
|
||||
@ -841,6 +947,14 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "SendGift",
|
||||
Handler: _RoomCommandService_SendGift_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "FollowRoom",
|
||||
Handler: _RoomCommandService_FollowRoom_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UnfollowRoom",
|
||||
Handler: _RoomCommandService_UnfollowRoom_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/room/v1/room.proto",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc v5.29.3
|
||||
// protoc-gen-go v1.34.2
|
||||
// protoc v5.29.2
|
||||
// source: proto/user/v1/auth.proto
|
||||
|
||||
package userv1
|
||||
@ -33,9 +33,11 @@ type LoginPasswordRequest struct {
|
||||
|
||||
func (x *LoginPasswordRequest) Reset() {
|
||||
*x = LoginPasswordRequest{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *LoginPasswordRequest) String() string {
|
||||
@ -46,7 +48,7 @@ func (*LoginPasswordRequest) ProtoMessage() {}
|
||||
|
||||
func (x *LoginPasswordRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -112,9 +114,11 @@ type LoginThirdPartyRequest struct {
|
||||
|
||||
func (x *LoginThirdPartyRequest) Reset() {
|
||||
*x = LoginThirdPartyRequest{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *LoginThirdPartyRequest) String() string {
|
||||
@ -125,7 +129,7 @@ func (*LoginThirdPartyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *LoginThirdPartyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -294,9 +298,11 @@ type AuthResponse struct {
|
||||
|
||||
func (x *AuthResponse) Reset() {
|
||||
*x = AuthResponse{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *AuthResponse) String() string {
|
||||
@ -307,7 +313,7 @@ func (*AuthResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AuthResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -363,9 +369,11 @@ type SetPasswordRequest struct {
|
||||
|
||||
func (x *SetPasswordRequest) Reset() {
|
||||
*x = SetPasswordRequest{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SetPasswordRequest) String() string {
|
||||
@ -376,7 +384,7 @@ func (*SetPasswordRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SetPasswordRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -423,9 +431,11 @@ type SetPasswordResponse struct {
|
||||
|
||||
func (x *SetPasswordResponse) Reset() {
|
||||
*x = SetPasswordResponse{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SetPasswordResponse) String() string {
|
||||
@ -436,7 +446,7 @@ func (*SetPasswordResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SetPasswordResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -470,9 +480,11 @@ type RefreshTokenRequest struct {
|
||||
|
||||
func (x *RefreshTokenRequest) Reset() {
|
||||
*x = RefreshTokenRequest{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RefreshTokenRequest) String() string {
|
||||
@ -483,7 +495,7 @@ func (*RefreshTokenRequest) ProtoMessage() {}
|
||||
|
||||
func (x *RefreshTokenRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -523,9 +535,11 @@ type RefreshTokenResponse struct {
|
||||
|
||||
func (x *RefreshTokenResponse) Reset() {
|
||||
*x = RefreshTokenResponse{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RefreshTokenResponse) String() string {
|
||||
@ -536,7 +550,7 @@ func (*RefreshTokenResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RefreshTokenResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -571,9 +585,11 @@ type LogoutRequest struct {
|
||||
|
||||
func (x *LogoutRequest) Reset() {
|
||||
*x = LogoutRequest{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *LogoutRequest) String() string {
|
||||
@ -584,7 +600,7 @@ func (*LogoutRequest) ProtoMessage() {}
|
||||
|
||||
func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -631,9 +647,11 @@ type LogoutResponse struct {
|
||||
|
||||
func (x *LogoutResponse) Reset() {
|
||||
*x = LogoutResponse{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *LogoutResponse) String() string {
|
||||
@ -644,7 +662,7 @@ func (*LogoutResponse) ProtoMessage() {}
|
||||
|
||||
func (x *LogoutResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -682,9 +700,11 @@ type RecordLoginBlockedRequest struct {
|
||||
|
||||
func (x *RecordLoginBlockedRequest) Reset() {
|
||||
*x = RecordLoginBlockedRequest{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RecordLoginBlockedRequest) String() string {
|
||||
@ -695,7 +715,7 @@ func (*RecordLoginBlockedRequest) ProtoMessage() {}
|
||||
|
||||
func (x *RecordLoginBlockedRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -763,9 +783,11 @@ type RecordLoginBlockedResponse struct {
|
||||
|
||||
func (x *RecordLoginBlockedResponse) Reset() {
|
||||
*x = RecordLoginBlockedResponse{}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RecordLoginBlockedResponse) String() string {
|
||||
@ -776,7 +798,7 @@ func (*RecordLoginBlockedResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RecordLoginBlockedResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1017,6 +1039,140 @@ func file_proto_user_v1_auth_proto_init() {
|
||||
return
|
||||
}
|
||||
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{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.3
|
||||
// - protoc v5.29.2
|
||||
// source: proto/user/v1/auth.proto
|
||||
|
||||
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.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.3
|
||||
// - protoc v5.29.2
|
||||
// source: proto/user/v1/host.proto
|
||||
|
||||
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.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.3
|
||||
// - protoc v5.29.2
|
||||
// source: proto/user/v1/user.proto
|
||||
|
||||
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.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.3
|
||||
// - protoc v5.29.2
|
||||
// source: proto/wallet/v1/wallet.proto
|
||||
|
||||
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。
|
||||
|
||||
当前已实现的第一条链路是:
|
||||
当前已实现的链路是:
|
||||
|
||||
- `wallet-service` 在同一账务事务里写 `wallet_outbox.WalletBalanceChanged`。
|
||||
- `notice-service` 抢占该事件,写自己的 `notice_delivery_events` 投递位点。
|
||||
- `notice-service` 通过腾讯云 IM C2C `TIMCustomElem` 给用户发送 `wallet_notice`。
|
||||
- 客户端收到通知后按 `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、不关心客户端在线状态 |
|
||||
| `notice-service` | 通知投递位点、抢占锁、退避重试、死信、腾讯云 IM 单聊投递 |
|
||||
| `room-service` | 房间状态和房间群系统消息 outbox;不发送钱包私有余额 |
|
||||
| `room-service` | 房间状态、房间群系统消息 outbox、腾讯 IM 群组/成员桥接;不直接发送用户私有通知 |
|
||||
| `activity-service` | 活动、任务、系统消息业务事实;后续可新增 activity notice 模块 |
|
||||
| 腾讯云 IM | 客户端长连接、单聊/群消息、离线/漫游投递 |
|
||||
|
||||
`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
|
||||
@ -32,6 +38,7 @@ services/notice-service/
|
||||
deploy/mysql/initdb/ notice-service 自己的库表
|
||||
internal/app/ 进程装配、health、worker 生命周期
|
||||
internal/config/ 配置解析和默认值
|
||||
internal/modules/roomnotice/ 房间私有通知模块
|
||||
internal/modules/walletnotice/ 钱包余额通知模块
|
||||
internal/platform/mysql/ notice-service MySQL 连接池
|
||||
```
|
||||
@ -76,6 +83,34 @@ sequenceDiagram
|
||||
|
||||
主请求链路只等待钱包扣费和房间状态提交,不等待腾讯云 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 约定
|
||||
|
||||
`WalletBalanceChanged` 发送给客户端的核心字段:
|
||||
@ -126,16 +161,25 @@ sequenceDiagram
|
||||
本地默认关闭 worker,避免误发真实腾讯云 IM:
|
||||
|
||||
```yaml
|
||||
wallet_database: "hyapp_wallet"
|
||||
room_database: "hyapp_room"
|
||||
|
||||
tencent_im:
|
||||
enabled: false
|
||||
|
||||
wallet_notice_worker:
|
||||
enabled: false
|
||||
|
||||
room_notice_worker:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
线上启用钱包余额通知必须同时开启:
|
||||
线上启用私有通知必须同时开启腾讯云 IM,并配置对应 owner outbox 所在库名:
|
||||
|
||||
```yaml
|
||||
wallet_database: "hyapp_wallet"
|
||||
room_database: "hyapp_room"
|
||||
|
||||
tencent_im:
|
||||
enabled: true
|
||||
sdk_app_id: 1400000000
|
||||
@ -148,6 +192,13 @@ wallet_notice_worker:
|
||||
batch_size: 100
|
||||
lock_ttl: 30s
|
||||
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 端口。
|
||||
@ -155,10 +206,11 @@ wallet_notice_worker:
|
||||
## 后续扩展顺序
|
||||
|
||||
1. 钱包余额通知:已按 `walletnotice` 模块落地。
|
||||
2. 幸运礼物中奖:仍由 `wallet-service` 写余额 outbox;notice 不需要新增特殊账务逻辑,只要客户端按 `balance_version` 更新。
|
||||
3. 活动/系统私有通知:新增 `activitynotice` 模块,读取 activity outbox,复用 `notice_delivery_events` 或新增同语义位点表。
|
||||
4. 站内 inbox:新增 inbox 模块,写 notice-service 自己的 inbox 表,再按客户端分页接口读取。
|
||||
5. Push:新增 push 模块,和 IM 投递使用不同 `channel`,独立重试和死信。
|
||||
2. 房间踢人私有通知:已按 `roomnotice` 模块落地,只消费 `RoomUserKicked` 的 C2C 通知。
|
||||
3. 幸运礼物中奖:仍由 `wallet-service` 写余额 outbox;notice 不需要新增特殊账务逻辑,只要客户端按 `balance_version` 更新。
|
||||
4. 活动/系统私有通知:新增 `activitynotice` 模块,读取 activity outbox,复用 `notice_delivery_events` 或新增同语义位点表。
|
||||
5. 站内 inbox:新增 inbox 模块,写 notice-service 自己的 inbox 表,再按客户端分页接口读取。
|
||||
6. Push:新增 push 模块,和 IM 投递使用不同 `channel`,独立重试和死信。
|
||||
|
||||
## 真实库验证
|
||||
|
||||
|
||||
@ -959,7 +959,7 @@ paths:
|
||||
- rooms
|
||||
summary: Mine 房间关系流
|
||||
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:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
@ -971,6 +971,7 @@ paths:
|
||||
- visited
|
||||
- friend
|
||||
- following
|
||||
- followed
|
||||
- name: cursor
|
||||
in: query
|
||||
required: false
|
||||
@ -982,7 +983,7 @@ paths:
|
||||
format: int32
|
||||
responses:
|
||||
"200":
|
||||
description: 查询成功;`visited` 由 JoinRoom 成功写入,`friend/following` 同步使用 user-service 好友/关注关系,不依赖 Redis 缓存命中。
|
||||
description: 查询成功;`visited` 由 JoinRoom 成功写入,`friend/following` 同步使用 user-service 好友/关注关系,`followed` 同步使用 room-service 房间关注关系,不依赖 Redis 缓存命中。
|
||||
schema:
|
||||
$ref: "#/definitions/RoomListEnvelope"
|
||||
"400":
|
||||
@ -1068,6 +1069,63 @@ paths:
|
||||
$ref: "#/responses/Conflict"
|
||||
"502":
|
||||
$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:
|
||||
post:
|
||||
tags:
|
||||
@ -1435,6 +1493,40 @@ paths:
|
||||
$ref: "#/responses/Internal"
|
||||
"502":
|
||||
$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:
|
||||
post:
|
||||
tags:
|
||||
@ -3771,6 +3863,13 @@ definitions:
|
||||
description: 房间命令幂等键;客户端按用户动作生成,同一动作的 HTTP 重试必须复用。
|
||||
role:
|
||||
type: string
|
||||
enum:
|
||||
- audience
|
||||
description: 首版固定 audience;上麦成功后客户端再切换发布角色。
|
||||
password:
|
||||
type: string
|
||||
maxLength: 64
|
||||
description: 锁房时必填;只用于本次 JoinRoom 校验,服务端不保存明文。
|
||||
RoomHeartbeatRequest:
|
||||
type: object
|
||||
required:
|
||||
@ -3879,6 +3978,19 @@ definitions:
|
||||
description: 房间命令幂等键;客户端按用户动作生成,同一动作的 HTTP 重试必须复用。
|
||||
enabled:
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
@ -4030,6 +4142,8 @@ definitions:
|
||||
type: string
|
||||
chat_enabled:
|
||||
type: boolean
|
||||
locked:
|
||||
type: boolean
|
||||
mic_seats:
|
||||
type: array
|
||||
items:
|
||||
@ -4089,6 +4203,9 @@ definitions:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
locked:
|
||||
type: boolean
|
||||
description: true 表示该房间需要入房密码;列表和搜索只返回标识,不返回密码。
|
||||
heat:
|
||||
type: integer
|
||||
format: int64
|
||||
@ -4160,6 +4277,9 @@ definitions:
|
||||
type: string
|
||||
chat_enabled:
|
||||
type: boolean
|
||||
locked:
|
||||
type: boolean
|
||||
description: true 表示该房间需要入房密码。
|
||||
heat:
|
||||
type: integer
|
||||
format: int64
|
||||
@ -4303,6 +4423,27 @@ definitions:
|
||||
properties:
|
||||
room:
|
||||
$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:
|
||||
type: integer
|
||||
format: int64
|
||||
@ -4465,6 +4606,12 @@ definitions:
|
||||
$ref: "#/definitions/CommandResult"
|
||||
room:
|
||||
$ref: "#/definitions/RoomSnapshot"
|
||||
rtc_kicked:
|
||||
type: boolean
|
||||
description: room-service 是否已通过 TRTC 服务端接口移除目标用户;失败不回滚踢人事实。
|
||||
rtc_kick_error:
|
||||
type: string
|
||||
description: TRTC 服务端踢人失败原因,仅用于排障和告警。
|
||||
UnbanUserResponse:
|
||||
type: object
|
||||
properties:
|
||||
@ -4747,6 +4894,13 @@ definitions:
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/definitions/RoomSnapshotData"
|
||||
RoomFollowEnvelope:
|
||||
allOf:
|
||||
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/definitions/RoomFollowData"
|
||||
JoinRoomEnvelope:
|
||||
allOf:
|
||||
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||
@ -4810,6 +4964,13 @@ definitions:
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/definitions/SetChatEnabledResponse"
|
||||
SetRoomPasswordEnvelope:
|
||||
allOf:
|
||||
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/definitions/UpdateRoomProfileResponse"
|
||||
SetRoomAdminEnvelope:
|
||||
allOf:
|
||||
- $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` | 双向删除好友关系 |
|
||||
| GET | `/api/v1/rooms` | rooms | `listRooms` | 房间发现列表,支持 `tab=hot/new`,卡片返回 `im_group_id` |
|
||||
| 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/snapshot` | rooms | `getRoomSnapshot` | 查询房间完整快照 |
|
||||
| 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}/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/profile/update` | rooms | `updateRoomProfile` | 修改房间资料和座位数 |
|
||||
| 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/lock` | rooms | `setMicSeatLock` | 锁定或解锁麦位 |
|
||||
| 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/user/mute` | rooms | `muteUser` | 设置房间内禁言 |
|
||||
| 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}/enable` | admin | `enableResource` | enableResource |
|
||||
| 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 |
|
||||
| PATCH | `/api/v1/admin/rooms/{room_id}` | admin | `updateRoom` | updateRoom |
|
||||
| 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/UnbanUser` | room-command | `roomUnbanUser` | 解除房间 ban |
|
||||
| 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/VerifyRoomPresence` | room-guard | `roomVerifyRoomPresence` | 入群前 presence 守卫 |
|
||||
| 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=following` 由 gateway 调用 `ListFollowing` 读取关注用户,再把关系用户和 `followed_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
|
||||
|
||||
|
||||
@ -93,7 +93,7 @@ sequenceDiagram
|
||||
G->>U: GetUser(user_id)
|
||||
U-->>G: region_id
|
||||
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[]
|
||||
R-->>G: ListRoomsResponse
|
||||
G-->>C: {code,message,request_id,data}
|
||||
@ -107,6 +107,14 @@ gateway -> room-service.JoinRoom -> Tencent IM join group callback guard
|
||||
|
||||
因此列表允许秒级最终一致;真正能否进入房间由 `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
|
||||
|
||||
### CreateRoom
|
||||
@ -194,6 +202,7 @@ CREATE TABLE IF NOT EXISTS room_list_entries (
|
||||
cover_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
mode VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
locked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
heat BIGINT NOT NULL DEFAULT 0,
|
||||
online_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"]` | 创建房间 |
|
||||
| `mode` | RoomMeta/RoomState | 创建房间或模式变更 |
|
||||
| `status` | RoomState | 创建、关闭、封禁、恢复 |
|
||||
| `locked` | `RoomState.RoomPasswordHash != ""` | SetRoomPassword |
|
||||
| `heat` | RoomState | SendGift、热度刷新 |
|
||||
| `online_count` | RoomState presence | JoinRoom、LeaveRoom、stale cleanup |
|
||||
| `occupied_seat_count` | RoomState mic seats | MicUp、MicDown、ChangeMicSeat |
|
||||
@ -348,6 +358,7 @@ message RoomListItem {
|
||||
int64 visible_region_id = 12;
|
||||
string app_code = 13;
|
||||
string room_short_id = 14;
|
||||
bool locked = 15;
|
||||
}
|
||||
|
||||
message ListRoomsResponse {
|
||||
@ -384,6 +395,7 @@ message ListRoomsResponse {
|
||||
- 命令成功提交后调用 `RoomListProjector` 更新 MySQL 投影。
|
||||
- 投影更新失败时写 `room_outbox` 或独立 `room_projection_tasks` 等待补偿。
|
||||
- 列表查询只读 `room_list_entries`,不直接读取 Room Cell 内存。
|
||||
- 公共列表和带 `query` 的搜索列表都从同一投影读取 `locked`;它只表达是否需要入房密码,不携带密码明文或哈希。
|
||||
|
||||
## Consistency Model
|
||||
|
||||
@ -453,7 +465,7 @@ JoinRoom(room_id, user_id)
|
||||
|
||||
1. 新增管理端房间区域迁移命令。
|
||||
2. 新增区域迁移审计表。
|
||||
3. 支持按区域隐藏、封禁或置顶房间。
|
||||
3. 支持按区域隐藏、封禁或置顶房间;置顶由 `room_region_pins` 按区域独立维护。
|
||||
|
||||
## Tests
|
||||
|
||||
@ -468,6 +480,7 @@ JoinRoom(room_id, user_id)
|
||||
| 创建房间 | 写 `rooms.visible_region_id` 和 `room_list_entries` |
|
||||
| 房主改国家 | 已创建房间列表区域不变 |
|
||||
| Join/Leave | 列表在线人数最终更新 |
|
||||
| 区域置顶 | 置顶房间排在同区域普通房间前,翻页不重复 |
|
||||
| SendGift | 热度和排序分更新 |
|
||||
| Redis miss | MySQL 回源成功 |
|
||||
| 列表房间已关闭 | `JoinRoom` 拒绝,客户端刷新 |
|
||||
|
||||
@ -251,7 +251,7 @@ return snapshot
|
||||
|
||||
## 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_gift_sent` | `RoomGiftSent` |
|
||||
|
||||
`room_user_kicked` 还会由 notice-service 读取同一条 `RoomUserKicked` 事实,给被踢用户发送 C2C `room_notice`。room-service 仍负责把目标用户移出腾讯 IM 群;notice-service 不处理建群、退群或群成员权限。
|
||||
|
||||
## Recovery Rules
|
||||
|
||||
节点重启或接管时:
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
| Area | Owner | Rule |
|
||||
| --- | --- | --- |
|
||||
| HTTP 入口 | `gateway-service` | 鉴权、profile gate、协议转换、腾讯 IM/RTC token 签发 |
|
||||
| 房间业务状态 | `room-service` | Room Cell、presence、麦位、房管、snapshot、outbox |
|
||||
| 房间业务状态 | `room-service` | Room Cell、presence、麦位、房管、snapshot、outbox、房间关注关系 |
|
||||
| 房间长连接和公屏 | 腾讯云 IM SDK | 客户端登录、房间群、公屏、系统消息接收 |
|
||||
| 真实音频频道 | 腾讯云 RTC SDK | 进 RTC 房、切换 audience/anchor、发音频 |
|
||||
| 用户主数据 | `user-service` | 登录、资料完成状态、用户区域、push token |
|
||||
@ -40,7 +40,7 @@ sequenceDiagram
|
||||
R-->>G: owner room card from rooms + snapshot
|
||||
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
|
||||
G->>U: ListFriends/ListFollowing(cursor)
|
||||
U-->>G: related user ids + relation times
|
||||
@ -64,7 +64,14 @@ sequenceDiagram
|
||||
G-->>C: optional active-room resync
|
||||
|
||||
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
|
||||
C->>G: POST /api/v1/rooms/heartbeat
|
||||
@ -78,32 +85,48 @@ sequenceDiagram
|
||||
| 1 | `GET /api/v1/im/usersig` | yes | 用户级 IM 登录票据,可在 App 启动后预取并按 TTL 复用 |
|
||||
| 2 | Tencent IM SDK `login` | yes | 客户端先建立 IM 身份,房间群仍等 JoinRoom 成功后再进 |
|
||||
| 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 |
|
||||
| 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` 进入真实音频频道旁听 |
|
||||
| 8 | `POST /api/v1/rooms/heartbeat` | yes | 刷新业务 presence,避免 stale timeout |
|
||||
| 9 | `GET /api/v1/rooms/{room_id}/detail` | optional after join/restore | 详情页聚合包;用于 JoinRoom 后补偿刷新、回前台、设计稿首屏重绘 |
|
||||
| 10 | `GET /api/v1/rooms/{room_id}/gift-panel` | lazy on gift button | 打开礼物面板时再拉,不放进房间首屏 |
|
||||
| 11 | `GET /api/v1/rooms/{room_id}/online-users` | lazy on online count | 点击右上角在线人数时分页拉取 |
|
||||
| 9 | `GET /api/v1/rooms/{room_id}/detail` | optional after join/restore | 详情页聚合包;返回 `is_followed`,用于关注按钮初始状态 |
|
||||
| 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}/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。
|
||||
|
||||
### 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 的位置:
|
||||
|
||||
- `hot/new` 公共列表:room-service 用 Redis ZSET 缓存排序 room_id,miss 后回源 `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` 成功后必须主动失效。
|
||||
- `followed` 房间 ID 短 TTL 缓存:只能放在 room-service 读优化层,`FollowRoom/UnfollowRoom` 成功后必须主动失效。
|
||||
- `visited` room_id 列表缓存:可以由 JoinRoom 成功后刷新,Redis 写失败不能影响 JoinRoom。
|
||||
|
||||
不建议首版缓存 `friend/following` 完整房间卡片列表。这个列表同时依赖 user-service 关系事实、room-service 房间 active 状态、区域过滤和搜索条件,失效面较大;先保持同步查询,等 QPS 或延迟数据证明需要缓存再加。
|
||||
不建议首版缓存 `friend/following/followed` 完整房间卡片列表。这个列表同时依赖关系事实、room-service 房间 active 状态、区域过滤和搜索条件,失效面较大;先保持同步查询,等 QPS 或延迟数据证明需要缓存再加。
|
||||
|
||||
## 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
|
||||
|
||||
```http
|
||||
@ -118,7 +141,8 @@ Request:
|
||||
{
|
||||
"room_id": "lalu_xxx",
|
||||
"command_id": "cmd_join_<room_id>_<user_id>_<nonce>",
|
||||
"role": "audience"
|
||||
"role": "audience",
|
||||
"password": "1234"
|
||||
}
|
||||
```
|
||||
|
||||
@ -143,6 +167,7 @@ Response `data`:
|
||||
"mode": "voice",
|
||||
"status": "active",
|
||||
"chat_enabled": true,
|
||||
"locked": true,
|
||||
"heat": 1000,
|
||||
"online_count": 26,
|
||||
"seat_count": 15,
|
||||
@ -209,6 +234,8 @@ Rules:
|
||||
- `room_id` 必须来自房间列表、分享或恢复接口,不能由客户端拼业务含义。
|
||||
- `command_id` 由客户端按用户动作生成,用于房间命令幂等;重试同一次进房必须复用同一个值。
|
||||
- `role` 只作为展示默认值,不能通过自报 `admin/host/owner` 获得权限。
|
||||
- 带锁房间的新进入用户必须传正确 `password`;密码错误返回 403,不创建 presence,不签发 RTC token。
|
||||
- 房主不需要密码进入自己的房间;已在房内的用户不会因为房主重新设置密码被挤出。
|
||||
- `im.group_id` 当前等于 `room_id`,但客户端必须使用服务端返回值,不能硬编码映射规则。
|
||||
- JoinRoom 成功后才能调用腾讯 IM SDK `joinGroup`;列表里的 `im_group_id` 只用于准备,不代表入群授权。
|
||||
- JoinRoom 会尝试内嵌签发 RTC token;签发失败时返回 `rtc.available=false`,客户端可以继续展示房间并单独重试 `/api/v1/rtc/token`。
|
||||
@ -229,6 +256,7 @@ Response `data`:
|
||||
"seats": [],
|
||||
"rank_top": [],
|
||||
"online_count": 30,
|
||||
"is_followed": true,
|
||||
"permissions": {},
|
||||
"profiles": [],
|
||||
"im": {},
|
||||
@ -240,9 +268,35 @@ Rules:
|
||||
|
||||
- `detail` 只读 Room Cell/snapshot,不刷新 heartbeat,不隐式进房。
|
||||
- viewer 必须仍在 room-service presence 内;未进房、被踢或被 ban 返回 `PERMISSION_DENIED`。
|
||||
- `is_followed` 来自 room-service 的 `room_follows` 关系表,只表达当前 token 用户是否关注该房间,不写入 Room Cell 快照。
|
||||
- `permissions.can_close_room` 表示设计稿右上角电源按钮可用,但当前语义是“退出房间”,不是关闭房间生命周期。
|
||||
- `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
|
||||
|
||||
```http
|
||||
@ -638,6 +692,7 @@ Rules:
|
||||
| `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/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/host/transfer` | `room_id, command_id, target_user_id` | 转移主持人 |
|
||||
| `POST /api/v1/rooms/user/mute` | `room_id, command_id, target_user_id, muted, reason` | 禁言或解禁 |
|
||||
@ -648,7 +703,10 @@ Rules:
|
||||
Management event handling:
|
||||
|
||||
- 管理动作成功后,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` 为准。
|
||||
|
||||
## Gift And Interaction
|
||||
|
||||
@ -244,6 +244,9 @@
|
||||
| `room:view` | `menu` | 房间列表、详情、房间状态 |
|
||||
| `room:update` | `button` | 后台修改房间展示资料、模式、状态、区域 |
|
||||
| `room:delete` | `button` | 后台删除房间 |
|
||||
| `room-pin:view` | `menu` | 房间置顶菜单、置顶列表 |
|
||||
| `room-pin:create` | `button` | 新增或恢复区域房间置顶 |
|
||||
| `room-pin:cancel` | `button` | 取消区域房间置顶 |
|
||||
| `room-config:view` | `menu` | 房间配置页面、读取座位数配置 |
|
||||
| `room-config:update` | `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})
|
||||
}
|
||||
|
||||
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) {
|
||||
config, err := h.service.GetRoomConfig(c.Request.Context())
|
||||
if err != nil {
|
||||
@ -108,6 +150,21 @@ func parseListQuery(c *gin.Context) (listQuery, bool) {
|
||||
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 {
|
||||
for _, name := range names {
|
||||
if value := strings.TrimSpace(c.Query(name)); value != "" {
|
||||
@ -142,6 +199,16 @@ func parseRoomID(c *gin.Context) (string, bool) {
|
||||
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) {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
CoverURL *string `json:"coverUrl"`
|
||||
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/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.PUT("/admin/rooms/config", middleware.RequirePermission("room-config:update"), h.UpdateRoomConfig)
|
||||
protected.PATCH("/admin/rooms/:room_id", middleware.RequirePermission("room:update"), h.UpdateRoom)
|
||||
|
||||
@ -42,6 +42,7 @@ type Room struct {
|
||||
OwnerUserID string `json:"ownerUserId"`
|
||||
RegionName string `json:"regionName"`
|
||||
RoomID string `json:"roomId"`
|
||||
RoomShortID string `json:"roomShortId"`
|
||||
RoomContribution int64 `json:"roomContribution"`
|
||||
SeatCount int32 `json:"seatCount"`
|
||||
SortScore int64 `json:"sortScore"`
|
||||
@ -76,8 +77,8 @@ func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64
|
||||
}
|
||||
if query.Keyword != "" {
|
||||
like := "%" + query.Keyword + "%"
|
||||
whereSQL += " AND (rle.room_id LIKE ? OR rle.title LIKE ? OR CAST(rle.owner_user_id AS CHAR) LIKE ?)"
|
||||
args = append(args, like, like, 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, like)
|
||||
}
|
||||
total, err := countRows(ctx, s.db, whereSQL, args...)
|
||||
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(`
|
||||
SELECT rle.room_id,
|
||||
rle.room_short_id,
|
||||
rle.visible_region_id,
|
||||
rle.owner_user_id,
|
||||
rle.title,
|
||||
@ -131,6 +133,7 @@ func (s *Service) GetRoom(ctx context.Context, roomID string) (Room, error) {
|
||||
appCode := appctx.FromContext(ctx)
|
||||
row := s.db.QueryRowContext(ctx, `
|
||||
SELECT room_id,
|
||||
room_short_id,
|
||||
visible_region_id,
|
||||
owner_user_id,
|
||||
title,
|
||||
@ -389,6 +392,7 @@ func scanRoom(row roomScanner) (Room, error) {
|
||||
var ownerUserID int64
|
||||
err := row.Scan(
|
||||
&item.RoomID,
|
||||
&item.RoomShortID,
|
||||
&item.VisibleRegionID,
|
||||
&ownerUserID,
|
||||
&item.Title,
|
||||
|
||||
@ -23,6 +23,9 @@ var defaultPermissions = []model.Permission{
|
||||
{Name: "房间查看", Code: "room:view", Kind: "menu"},
|
||||
{Name: "房间更新", Code: "room:update", 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:update", Kind: "button"},
|
||||
{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-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-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: "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},
|
||||
@ -423,7 +427,7 @@ func defaultRolePermissionCodes(code string) []string {
|
||||
"overview:view",
|
||||
"user:view", "user:status",
|
||||
"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-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
||||
"resource:view", "resource:create", "resource:update",
|
||||
@ -444,13 +448,14 @@ func defaultRolePermissionCodes(code string) []string {
|
||||
"upload:create",
|
||||
}
|
||||
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":
|
||||
return []string{
|
||||
"overview:view",
|
||||
"user:view",
|
||||
"app-user:view",
|
||||
"room:view",
|
||||
"room-pin:view",
|
||||
"room-config:view",
|
||||
"app-config:view",
|
||||
"app-version:view",
|
||||
@ -485,7 +490,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
||||
case "ops-admin":
|
||||
return []string{
|
||||
"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-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
||||
"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",
|
||||
}
|
||||
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:
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -22,11 +22,14 @@ type RoomClient interface {
|
||||
ApplyRTCEvent(ctx context.Context, req *roomv1.ApplyRTCEventRequest) (*roomv1.ApplyRTCEventResponse, error)
|
||||
SetMicSeatLock(ctx context.Context, req *roomv1.SetMicSeatLockRequest) (*roomv1.SetMicSeatLockResponse, 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)
|
||||
MuteUser(ctx context.Context, req *roomv1.MuteUserRequest) (*roomv1.MuteUserResponse, error)
|
||||
KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*roomv1.KickUserResponse, error)
|
||||
UnbanUser(ctx context.Context, req *roomv1.UnbanUserRequest) (*roomv1.UnbanUserResponse, 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 的依赖。
|
||||
@ -131,6 +134,10 @@ func (c *grpcRoomClient) SetChatEnabled(ctx context.Context, req *roomv1.SetChat
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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 (
|
||||
"net/http"
|
||||
@ -29,7 +29,7 @@ func (h *Handler) getRegistrationRewardEligibility(writer http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
resp, err := h.registrationReward.GetRegistrationRewardEligibility(request.Context(), &activityv1.GetRegistrationRewardEligibilityRequest{
|
||||
Meta: activityMeta(request),
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
})
|
||||
if err != nil {
|
||||
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package activityapi
|
||||
|
||||
import (
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
@ -94,7 +94,7 @@ func (h *Handler) listTaskTabs(writer http.ResponseWriter, request *http.Request
|
||||
return
|
||||
}
|
||||
resp, err := h.taskClient.ListUserTasks(request.Context(), &activityv1.ListUserTasksRequest{
|
||||
Meta: activityMeta(request),
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
})
|
||||
if err != nil {
|
||||
@ -128,19 +128,20 @@ func (h *Handler) claimTaskReward(writer http.ResponseWriter, request *http.Requ
|
||||
return
|
||||
}
|
||||
var body taskClaimRequestBody
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
taskID := body.normalizedTaskID()
|
||||
taskType := body.normalizedTaskType()
|
||||
taskDay := body.normalizedTaskDay()
|
||||
commandID := normalizeCommandID(body.normalizedCommandID())
|
||||
// command_id 是客户端动作的幂等锚点,gateway 只裁剪空白,不替客户端补值。
|
||||
commandID := strings.TrimSpace(body.normalizedCommandID())
|
||||
if commandID == "" || taskID == "" || taskType == "" || taskDay == "" {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
resp, err := h.taskClient.ClaimTaskReward(request.Context(), &activityv1.ClaimTaskRewardRequest{
|
||||
Meta: activityMeta(request),
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
TaskId: taskID,
|
||||
TaskType: taskType,
|
||||
@ -1,7 +1,6 @@
|
||||
package http
|
||||
package appapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -12,13 +11,6 @@ import (
|
||||
"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 {
|
||||
Key string `json:"key"`
|
||||
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{
|
||||
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"),
|
||||
Country: firstQueryOrHeader(request, "country", "X-Country-Code", "X-App-Country"),
|
||||
Country: httpkit.FirstQueryOrHeader(request, "country", "X-Country-Code", "X-App-Country"),
|
||||
})
|
||||
if err != nil {
|
||||
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")
|
||||
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" {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "platform is invalid")
|
||||
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 {
|
||||
value := firstQueryOrHeader(request, queryKey, headerNames...)
|
||||
value := httpkit.FirstQueryOrHeader(request, queryKey, headerNames...)
|
||||
if value == "" {
|
||||
return 0
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package appapi
|
||||
|
||||
import (
|
||||
"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"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
body.DeviceID = pushDeviceID(request, body.DeviceID)
|
||||
body.Platform = pushPlatform(request, body.Platform)
|
||||
|
||||
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()),
|
||||
DeviceId: body.DeviceID,
|
||||
PushToken: strings.TrimSpace(body.PushToken),
|
||||
@ -71,13 +71,13 @@ func (h *Handler) deletePushToken(writer http.ResponseWriter, request *http.Requ
|
||||
DeviceID string `json:"device_id"`
|
||||
PushToken string `json:"push_token"`
|
||||
}
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
body.DeviceID = pushDeviceID(request, body.DeviceID)
|
||||
|
||||
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()),
|
||||
DeviceId: body.DeviceID,
|
||||
PushToken: strings.TrimSpace(body.PushToken),
|
||||
@ -120,12 +120,12 @@ func pushDeviceID(request *http.Request, bodyValue string) string {
|
||||
if value := strings.TrimSpace(bodyValue); 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 {
|
||||
if value := strings.TrimSpace(bodyValue); 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 (
|
||||
"bytes"
|
||||
@ -106,7 +106,7 @@ func (h *Handler) uploadObject(writer http.ResponseWriter, request *http.Request
|
||||
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)
|
||||
if err != nil {
|
||||
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{
|
||||
UserID: userIDString(token.GetUserId()),
|
||||
UserID: httpkit.UserIDString(token.GetUserId()),
|
||||
AppCode: token.GetAppCode(),
|
||||
DisplayUserID: token.GetDisplayUserId(),
|
||||
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 (
|
||||
"encoding/json"
|
||||
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package callbackapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package gameapi
|
||||
|
||||
import (
|
||||
"io"
|
||||
@ -21,7 +21,7 @@ func (h *Handler) listGames(writer http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
userID := auth.UserIDFromContext(request.Context())
|
||||
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: userID,
|
||||
})
|
||||
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")),
|
||||
RegionId: userResp.GetUser().GetRegionId(),
|
||||
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) {
|
||||
@ -54,12 +54,12 @@ func (h *Handler) launchGame(writer http.ResponseWriter, request *http.Request)
|
||||
Scene string `json:"scene"`
|
||||
RoomID string `json:"room_id"`
|
||||
}
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
userID := auth.UserIDFromContext(request.Context())
|
||||
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -74,7 +74,7 @@ func (h *Handler) launchGame(writer http.ResponseWriter, request *http.Request)
|
||||
Scene: body.Scene,
|
||||
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) {
|
||||
@ -122,7 +122,7 @@ func gameMeta(request *http.Request) *gamev1.RequestMeta {
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
value = request.Header.Get("Accept-Language")
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package gameapi
|
||||
|
||||
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"
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/idgen"
|
||||
"hyapp/pkg/imgroup"
|
||||
"hyapp/pkg/tencentim"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -39,7 +38,7 @@ type Handler struct {
|
||||
registrationReward client.RegistrationRewardClient
|
||||
broadcastClient client.BroadcastClient
|
||||
gameClient client.GameClient
|
||||
appConfigReader appConfigReader
|
||||
appConfigReader appapi.ConfigReader
|
||||
tencentIM TencentIMConfig
|
||||
tencentRTC TencentRTCConfig
|
||||
objectUploader ObjectUploader
|
||||
@ -190,25 +189,10 @@ func (h *Handler) SetGameClient(gameClient client.GameClient) {
|
||||
}
|
||||
|
||||
// SetAppConfigReader 注入后台 App 配置读取依赖。
|
||||
func (h *Handler) SetAppConfigReader(reader appConfigReader) {
|
||||
func (h *Handler) SetAppConfigReader(reader appapi.ConfigReader) {
|
||||
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 {
|
||||
// command_id 是客户端按用户动作生成的写命令幂等键;gateway 只裁剪空白,不自动补值。
|
||||
return strings.TrimSpace(commandID)
|
||||
|
||||
@ -4,14 +4,22 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/status"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/idgen"
|
||||
"hyapp/pkg/xerr"
|
||||
"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{}
|
||||
@ -67,6 +75,17 @@ func DecodeJSON(reader io.Reader, out any) error {
|
||||
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。
|
||||
func WriteOK(writer http.ResponseWriter, request *http.Request, data any) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
spec := xerr.SpecOf(reason)
|
||||
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
|
||||
}
|
||||
|
||||
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 进入命令链路。
|
||||
func RequireMethod(method string, next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
@ -153,3 +217,121 @@ func RequireCompletedProfile(next http.HandlerFunc) http.HandlerFunc {
|
||||
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
|
||||
ListRoomOnlineUsers http.HandlerFunc
|
||||
GetRoomGiftPanel http.HandlerFunc
|
||||
FollowRoom http.HandlerFunc
|
||||
CreateRoom http.HandlerFunc
|
||||
UpdateRoomProfile http.HandlerFunc
|
||||
JoinRoom http.HandlerFunc
|
||||
@ -104,6 +105,7 @@ type RoomHandlers struct {
|
||||
SetMicMute http.HandlerFunc
|
||||
SetMicSeatLock http.HandlerFunc
|
||||
SetChatEnabled http.HandlerFunc
|
||||
SetRoomPassword http.HandlerFunc
|
||||
SetRoomAdmin http.HandlerFunc
|
||||
MuteUser 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}/online-users", http.MethodGet, h.ListRoomOnlineUsers)
|
||||
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/profile/update", http.MethodPost, h.UpdateRoomProfile)
|
||||
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/lock", http.MethodPost, h.SetMicSeatLock)
|
||||
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/user/mute", http.MethodPost, h.MuteUser)
|
||||
r.profile("/rooms/user/kick", http.MethodPost, h.KickUser)
|
||||
|
||||
@ -50,7 +50,7 @@ func TestListManagerGrantResourcesChecksCapabilityAndFiltersWalletRequest(t *tes
|
||||
if recorder.Code != http.StatusOK {
|
||||
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)
|
||||
}
|
||||
if walletClient.lastListResources == nil {
|
||||
@ -65,10 +65,13 @@ func TestListManagerGrantResourcesChecksCapabilityAndFiltersWalletRequest(t *tes
|
||||
Code string `json:"code"`
|
||||
RequestID string `json:"request_id"`
|
||||
Data struct {
|
||||
Items []managerGrantResourceData `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int32 `json:"page"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
Items []struct {
|
||||
ResourceID string `json:"resource_id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int32 `json:"page"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
||||
@ -124,8 +127,11 @@ func TestBusinessUserLookupForwardsSceneAndActor(t *testing.T) {
|
||||
var envelope struct {
|
||||
Code string `json:"code"`
|
||||
Data struct {
|
||||
Items []businessUserLookupData `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Items []struct {
|
||||
UserID string `json:"user_id"`
|
||||
Status string `json:"status"`
|
||||
} `json:"items"`
|
||||
Total int `json:"total"`
|
||||
} `json:"data"`
|
||||
}
|
||||
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 (
|
||||
"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")
|
||||
return
|
||||
}
|
||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
|
||||
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
@ -97,7 +97,7 @@ func (h *Handler) lookupBusinessUsers(writer http.ResponseWriter, request *http.
|
||||
pageSize = 20
|
||||
}
|
||||
resp, err := h.userProfileClient.BusinessUserLookup(request.Context(), &userv1.BusinessUserLookupRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
ActorUserId: auth.UserIDFromContext(request.Context()),
|
||||
Scene: strings.TrimSpace(request.URL.Query().Get("scene")),
|
||||
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"`
|
||||
DurationMSAlt *int64 `json:"durationMs"`
|
||||
}
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
if body.Quantity != nil || body.DurationMS != nil || body.DurationMSAlt != nil {
|
||||
@ -152,7 +152,7 @@ func (h *Handler) grantManagerResource(writer http.ResponseWriter, request *http
|
||||
return
|
||||
}
|
||||
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: targetUserID,
|
||||
})
|
||||
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 {
|
||||
resp, err := h.userHostClient.CheckBusinessCapability(request.Context(), &userv1.CheckBusinessCapabilityRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
ActorUserId: auth.UserIDFromContext(request.Context()),
|
||||
Capability: managerResourceGrantCapability,
|
||||
})
|
||||
@ -221,7 +221,7 @@ func managerGrantResourceFromProto(resource *walletv1.Resource) managerGrantReso
|
||||
return managerGrantResourceData{}
|
||||
}
|
||||
return managerGrantResourceData{
|
||||
ResourceID: userIDString(resource.GetResourceId()),
|
||||
ResourceID: httpkit.UserIDString(resource.GetResourceId()),
|
||||
ResourceType: resource.GetResourceType(),
|
||||
Name: resource.GetName(),
|
||||
AssetURL: resource.GetAssetUrl(),
|
||||
@ -237,7 +237,7 @@ func businessUserLookupFromProto(user *userv1.BusinessUserLookupItem) businessUs
|
||||
return businessUserLookupData{}
|
||||
}
|
||||
return businessUserLookupData{
|
||||
UserID: userIDString(user.GetUserId()),
|
||||
UserID: httpkit.UserIDString(user.GetUserId()),
|
||||
DisplayUserID: user.GetDisplayUserId(),
|
||||
Username: user.GetUsername(),
|
||||
Avatar: user.GetAvatar(),
|
||||
@ -254,25 +254,36 @@ func managerResourceGrantFromProto(grant *walletv1.ResourceGrant) managerResourc
|
||||
}
|
||||
resourceID := grant.GetGrantSubjectId()
|
||||
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{
|
||||
GrantID: grant.GetGrantId(),
|
||||
CommandID: grant.GetCommandId(),
|
||||
TargetUserID: userIDString(grant.GetTargetUserId()),
|
||||
TargetUserID: httpkit.UserIDString(grant.GetTargetUserId()),
|
||||
ResourceID: resourceID,
|
||||
Status: grant.GetStatus(),
|
||||
GrantSource: grant.GetGrantSource(),
|
||||
OperatorUserID: userIDString(grant.GetOperatorUserId()),
|
||||
OperatorUserID: httpkit.UserIDString(grant.GetOperatorUserId()),
|
||||
CreatedAtMS: grant.GetCreatedAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func managerPage(request *http.Request) (int32, int32, bool) {
|
||||
page, pageSize, ok := resourcePage(request)
|
||||
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
|
||||
if !ok {
|
||||
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 {
|
||||
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 (
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
)
|
||||
|
||||
type messageTabSectionData struct {
|
||||
@ -51,7 +49,7 @@ func (h *Handler) listMessageTabs(writer http.ResponseWriter, request *http.Requ
|
||||
return
|
||||
}
|
||||
resp, err := h.messageClient.ListMessageTabs(request.Context(), &activityv1.ListMessageTabsRequest{
|
||||
Meta: activityMeta(request),
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
})
|
||||
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"))
|
||||
resp, err := h.messageClient.ListInboxMessages(request.Context(), &activityv1.ListInboxMessagesRequest{
|
||||
Meta: activityMeta(request),
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
Section: section,
|
||||
PageSize: pageSize,
|
||||
@ -120,11 +118,11 @@ func (h *Handler) markInboxSectionRead(writer http.ResponseWriter, request *http
|
||||
var body struct {
|
||||
Section string `json:"section"`
|
||||
}
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
resp, err := h.messageClient.MarkInboxSectionRead(request.Context(), &activityv1.MarkInboxSectionReadRequest{
|
||||
Meta: activityMeta(request),
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
Section: body.Section,
|
||||
})
|
||||
@ -173,7 +171,7 @@ func (h *Handler) markInboxMessageRead(writer http.ResponseWriter, request *http
|
||||
return
|
||||
}
|
||||
resp, err := h.messageClient.MarkInboxMessageRead(request.Context(), &activityv1.MarkInboxMessageReadRequest{
|
||||
Meta: activityMeta(request),
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
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) {
|
||||
resp, err := h.messageClient.DeleteInboxMessage(request.Context(), &activityv1.DeleteInboxMessageRequest{
|
||||
Meta: activityMeta(request),
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
MessageId: messageID,
|
||||
})
|
||||
@ -209,7 +207,7 @@ func (h *Handler) batchUserProfiles(writer http.ResponseWriter, request *http.Re
|
||||
return
|
||||
}
|
||||
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserIds: userIDs,
|
||||
})
|
||||
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})
|
||||
}
|
||||
|
||||
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) {
|
||||
raw = strings.TrimSpace(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 (
|
||||
"errors"
|
||||
@ -357,11 +357,11 @@ func userResourceFromProto(item *walletv1.UserResourceEntitlement) userResourceD
|
||||
}
|
||||
|
||||
func resourcePage(request *http.Request) (int32, int32, bool) {
|
||||
page, ok := parsePositiveInt32Query(request, "page", 1)
|
||||
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
|
||||
if !ok {
|
||||
return 0, 0, false
|
||||
}
|
||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
|
||||
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||
if !ok {
|
||||
return 0, 0, false
|
||||
}
|
||||
@ -375,18 +375,6 @@ func resourcePage(request *http.Request) (int32, int32, bool) {
|
||||
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) {
|
||||
raw := strings.TrimSpace(request.URL.Query().Get(name))
|
||||
if raw == "" {
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
"hyapp/services/gateway-service/internal/transport/http/roomapi"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@ -45,11 +46,14 @@ type fakeRoomClient struct {
|
||||
lastRTCEvent *roomv1.ApplyRTCEventRequest
|
||||
lastMicSeatLock *roomv1.SetMicSeatLockRequest
|
||||
lastChatEnabled *roomv1.SetChatEnabledRequest
|
||||
lastRoomPassword *roomv1.SetRoomPasswordRequest
|
||||
lastSetAdmin *roomv1.SetRoomAdminRequest
|
||||
lastMute *roomv1.MuteUserRequest
|
||||
lastKick *roomv1.KickUserRequest
|
||||
lastUnban *roomv1.UnbanUserRequest
|
||||
lastGift *roomv1.SendGiftRequest
|
||||
lastFollowRoom *roomv1.FollowRoomRequest
|
||||
lastUnfollowRoom *roomv1.UnfollowRoomRequest
|
||||
createErr error
|
||||
updateProfileErr 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
|
||||
}
|
||||
|
||||
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) {
|
||||
f.lastSetAdmin = req
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
lastLoginThirdParty *userv1.LoginThirdPartyRequest
|
||||
lastLoginPassword *userv1.LoginPasswordRequest
|
||||
@ -1242,6 +1274,15 @@ func TestRoomCommandsPropagateClientCommandID(t *testing.T) {
|
||||
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",
|
||||
path: "/api/v1/rooms/admin/set",
|
||||
@ -1323,9 +1364,9 @@ func TestRoomCommandsPropagateClientCommandID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T) {
|
||||
previousNow := timeNow
|
||||
timeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
||||
defer func() { timeNow = previousNow }()
|
||||
previousNow := roomapi.TimeNow
|
||||
roomapi.TimeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
||||
defer func() { roomapi.TimeNow = previousNow }()
|
||||
|
||||
roomClient := &fakeRoomClient{joinResp: &roomv1.JoinRoomResponse{
|
||||
Result: &roomv1.CommandResult{Applied: true, RoomVersion: 7, ServerTimeMs: 1_700_000_000_123},
|
||||
@ -1336,6 +1377,7 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
||||
Mode: "voice",
|
||||
Status: "active",
|
||||
ChatEnabled: true,
|
||||
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"}},
|
||||
GiftRank: []*roomv1.RankItem{{UserId: 301, Score: 900, GiftValue: 900}},
|
||||
@ -1356,7 +1398,7 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
||||
AppScene: "voice_chat_room",
|
||||
})
|
||||
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("X-Request-ID", "req-join-initial")
|
||||
recorder := httptest.NewRecorder()
|
||||
@ -1372,9 +1414,12 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
||||
}
|
||||
data := response.Data.(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)
|
||||
}
|
||||
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)
|
||||
if len(seats) != 2 || seats[0].(map[string]any)["user_id"] != "102" {
|
||||
t.Fatalf("join response must include first-screen seats: %+v", seats)
|
||||
@ -1401,7 +1446,7 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{regionID: 1001}
|
||||
queryClient := &fakeRoomQueryClient{resp: &roomv1.ListRoomsResponse{
|
||||
Rooms: []*roomv1.RoomListItem{
|
||||
{RoomId: "room-1", VisibleRegionId: 1001, Heat: 90},
|
||||
{RoomId: "room-1", VisibleRegionId: 1001, Heat: 90, Locked: true},
|
||||
},
|
||||
NextCursor: "cursor-2",
|
||||
}}
|
||||
@ -1437,13 +1482,43 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
|
||||
data := response.Data.(map[string]any)
|
||||
rooms := data["rooms"].([]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)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
for _, tab := range []string{"visited", "friend", "following", "me"} {
|
||||
for _, tab := range []string{"visited", "friend", "following", "followed", "me"} {
|
||||
t.Run(tab, func(t *testing.T) {
|
||||
queryClient := &fakeRoomQueryClient{}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})
|
||||
@ -1466,7 +1541,7 @@ func TestListRoomsRejectsMineFeedTabs(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) {
|
||||
profileClient := &fakeUserProfileClient{regionID: 1001}
|
||||
socialClient := &fakeUserSocialClient{}
|
||||
@ -1475,7 +1550,7 @@ func TestListRoomFeedsSupportsMineFeedTabs(t *testing.T) {
|
||||
}}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||
handler.SetRoomQueryClient(queryClient)
|
||||
if tab != "visited" {
|
||||
if tab == "friend" || tab == "following" {
|
||||
handler.SetUserSocialClient(socialClient)
|
||||
}
|
||||
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)
|
||||
}
|
||||
switch tab {
|
||||
case "visited":
|
||||
case "visited", "followed":
|
||||
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":
|
||||
if socialClient.lastFollowing == nil || socialClient.lastFollowing.GetUserId() != 42 {
|
||||
@ -1524,12 +1599,12 @@ func TestListRoomFeedsRelationCursorContinuesUserSocialScan(t *testing.T) {
|
||||
handler.SetRoomQueryClient(queryClient)
|
||||
handler.SetUserSocialClient(socialClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
cursorPayload, _ := json.Marshal(roomFeedRelationCursor{
|
||||
Tab: "following",
|
||||
Query: "music",
|
||||
UpdatedAtMS: 8000,
|
||||
SubjectUserID: 10002,
|
||||
RoomID: "room-feed",
|
||||
cursorPayload, _ := json.Marshal(map[string]any{
|
||||
"tab": "following",
|
||||
"query": "music",
|
||||
"updated_at_ms": int64(8000),
|
||||
"subject_user_id": int64(10002),
|
||||
"room_id": "room-feed",
|
||||
})
|
||||
cursor := base64.RawURLEncoding.EncodeToString(cursorPayload)
|
||||
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) {
|
||||
queryClient := &fakeRoomQueryClient{myRoomResp: &roomv1.GetMyRoomResponse{
|
||||
HasRoom: true,
|
||||
@ -1755,6 +1876,52 @@ func TestGetRoomSnapshotUsesAuthenticatedUser(t *testing.T) {
|
||||
if !ok || len(seats) != 2 {
|
||||
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) {
|
||||
@ -3481,9 +3648,9 @@ func TestTencentIMUserSigUsesAuthenticatedUserIDAndFailsClosed(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTencentRTCTokenUsesPresenceGuardAndInternalUserID(t *testing.T) {
|
||||
previousNow := timeNow
|
||||
timeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
||||
defer func() { timeNow = previousNow }()
|
||||
previousNow := roomapi.TimeNow
|
||||
roomapi.TimeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
||||
defer func() { roomapi.TimeNow = previousNow }()
|
||||
|
||||
guard := &fakeRoomGuardClient{presenceResp: &roomv1.VerifyRoomPresenceResponse{Present: true, RoomVersion: 9}}
|
||||
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())
|
||||
}
|
||||
|
||||
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 {
|
||||
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())
|
||||
}
|
||||
|
||||
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 {
|
||||
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 (
|
||||
"encoding/base64"
|
||||
@ -8,13 +8,14 @@ import (
|
||||
"strconv"
|
||||
"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/roomid"
|
||||
"hyapp/pkg/xerr"
|
||||
"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 (
|
||||
@ -51,7 +52,7 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
viewerUserID := auth.UserIDFromContext(request.Context())
|
||||
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: viewerUserID,
|
||||
})
|
||||
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{
|
||||
Meta: meta(request, "", ""),
|
||||
Meta: httpkit.RoomMeta(request, "", ""),
|
||||
ViewerUserId: viewerUserID,
|
||||
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
||||
Tab: tab,
|
||||
@ -76,7 +77,7 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
|
||||
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) {
|
||||
if h.userProfileClient == nil || h.roomQueryClient == nil {
|
||||
// feed 同样需要 user-service 区域归属,避免跨区域露出用户关系流房间。
|
||||
@ -97,7 +98,7 @@ func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Reques
|
||||
|
||||
viewerUserID := auth.UserIDFromContext(request.Context())
|
||||
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: viewerUserID,
|
||||
})
|
||||
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{
|
||||
Meta: meta(request, "", ""),
|
||||
Meta: httpkit.RoomMeta(request, "", ""),
|
||||
ViewerUserId: viewerUserID,
|
||||
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
||||
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) {
|
||||
if tab == "visited" {
|
||||
if tab == "visited" || tab == "followed" {
|
||||
return nil, true, nil
|
||||
}
|
||||
if h.userSocialClient == nil {
|
||||
@ -165,7 +166,7 @@ func (h *Handler) roomFeedRelatedUserBatch(request *http.Request, tab string, vi
|
||||
switch tab {
|
||||
case "following":
|
||||
resp, err := h.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: viewerUserID,
|
||||
Page: 1,
|
||||
PageSize: defaultRoomFeedRelationPageSize,
|
||||
@ -187,7 +188,7 @@ func (h *Handler) roomFeedRelatedUserBatch(request *http.Request, tab string, vi
|
||||
return relatedUsers, cursor, nil
|
||||
case "friend":
|
||||
resp, err := h.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: viewerUserID,
|
||||
Page: 1,
|
||||
PageSize: defaultRoomFeedRelationPageSize,
|
||||
@ -250,6 +251,8 @@ func parseRoomFeedTab(raw string) (string, bool) {
|
||||
return "friend", true
|
||||
case "following":
|
||||
return "following", true
|
||||
case "followed":
|
||||
return "followed", true
|
||||
default:
|
||||
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{
|
||||
Meta: meta(request, "", ""),
|
||||
Meta: httpkit.RoomMeta(request, "", ""),
|
||||
OwnerUserId: auth.UserIDFromContext(request.Context()),
|
||||
})
|
||||
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{
|
||||
Meta: meta(request, "", ""),
|
||||
Meta: httpkit.RoomMeta(request, "", ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
})
|
||||
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{
|
||||
Meta: meta(request, roomID, ""),
|
||||
Meta: httpkit.RoomMeta(request, roomID, ""),
|
||||
RoomId: roomID,
|
||||
ViewerUserId: auth.UserIDFromContext(request.Context()),
|
||||
})
|
||||
write(writer, request, roomSnapshotDataFromProto(resp), err)
|
||||
httpkit.Write(writer, request, roomSnapshotDataFromProto(resp), err)
|
||||
}
|
||||
|
||||
// getRoomDetail 返回语音房详情页首屏可直接渲染的数据。
|
||||
@ -385,7 +388,7 @@ func (h *Handler) getRoomDetail(writer http.ResponseWriter, request *http.Reques
|
||||
|
||||
viewerUserID := auth.UserIDFromContext(request.Context())
|
||||
resp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
|
||||
Meta: meta(request, roomID, ""),
|
||||
Meta: httpkit.RoomMeta(request, roomID, ""),
|
||||
RoomId: roomID,
|
||||
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")
|
||||
return
|
||||
}
|
||||
page, ok := parsePositiveInt32Query(request, "page", 1)
|
||||
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", 50)
|
||||
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 50)
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.roomQueryClient.ListRoomOnlineUsers(request.Context(), &roomv1.ListRoomOnlineUsersRequest{
|
||||
Meta: meta(request, roomID, ""),
|
||||
Meta: httpkit.RoomMeta(request, roomID, ""),
|
||||
RoomId: roomID,
|
||||
ViewerUserId: auth.UserIDFromContext(request.Context()),
|
||||
Page: page,
|
||||
@ -451,7 +454,7 @@ func (h *Handler) getRoomGiftPanel(writer http.ResponseWriter, request *http.Req
|
||||
|
||||
viewerUserID := auth.UserIDFromContext(request.Context())
|
||||
snapshotResp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
|
||||
Meta: meta(request, roomID, ""),
|
||||
Meta: httpkit.RoomMeta(request, roomID, ""),
|
||||
RoomId: roomID,
|
||||
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 命令。
|
||||
func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request) {
|
||||
var body struct {
|
||||
@ -508,7 +553,7 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
|
||||
RoomDescription string `json:"room_description"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
body.CommandID = strings.TrimSpace(body.CommandID)
|
||||
@ -527,7 +572,7 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
|
||||
return
|
||||
}
|
||||
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
})
|
||||
if err != nil {
|
||||
@ -538,7 +583,7 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
|
||||
roomID := appcode.NewScopedID(appcode.FromContext(request.Context()))
|
||||
roomShortID := userResp.GetUser().GetDisplayUserId()
|
||||
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,
|
||||
Mode: body.Mode,
|
||||
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
||||
@ -547,7 +592,7 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
|
||||
RoomDescription: body.RoomDescription,
|
||||
RoomShortId: roomShortID,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// updateRoomProfile 把 App 房间资料编辑请求转换为 room-service UpdateRoomProfile 命令。
|
||||
@ -561,7 +606,7 @@ func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Re
|
||||
SeatCount *int32 `json:"seat_count"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
body.RoomID = strings.TrimSpace(body.RoomID)
|
||||
@ -571,7 +616,7 @@ func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Re
|
||||
trimOptionalString(body.RoomDescription)
|
||||
|
||||
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,
|
||||
RoomAvatar: body.RoomAvatar,
|
||||
RoomDescription: body.RoomDescription,
|
||||
@ -597,15 +642,17 @@ func (h *Handler) joinRoom(writer http.ResponseWriter, request *http.Request) {
|
||||
RoomID string `json:"room_id"`
|
||||
CommandID string `json:"command_id"`
|
||||
Role string `json:"role"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.roomClient.JoinRoom(request.Context(), &roomv1.JoinRoomRequest{
|
||||
Meta: meta(request, body.RoomID, body.CommandID),
|
||||
Role: body.Role,
|
||||
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||
Role: body.Role,
|
||||
Password: body.Password,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
@ -662,7 +709,7 @@ func (h *Handler) roomInitialProfiles(request *http.Request, snapshot *roomv1.Ro
|
||||
return nil
|
||||
}
|
||||
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserIds: userIDs,
|
||||
})
|
||||
if err != nil {
|
||||
@ -693,14 +740,14 @@ func (h *Handler) roomHeartbeat(writer http.ResponseWriter, request *http.Reques
|
||||
CommandID string `json:"command_id"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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 命令。
|
||||
@ -710,14 +757,14 @@ func (h *Handler) leaveRoom(writer http.ResponseWriter, request *http.Request) {
|
||||
CommandID string `json:"command_id"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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 是设计稿右上角电源按钮入口:这里只退出当前用户,不关闭房间生命周期。
|
||||
@ -733,15 +780,15 @@ func (h *Handler) micUp(writer http.ResponseWriter, request *http.Request) {
|
||||
SeatNo int32 `json:"seat_no"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// micDown 把下麦 HTTP JSON 请求转换为 room-service MicDown 命令。
|
||||
@ -753,16 +800,16 @@ func (h *Handler) micDown(writer http.ResponseWriter, request *http.Request) {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
Reason: body.Reason,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
SeatNo: body.SeatNo,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// confirmMicPublishing 把客户端 SDK 发流成功回调转换为 room-service 发流确认命令。
|
||||
@ -798,19 +845,19 @@ func (h *Handler) confirmMicPublishing(writer http.ResponseWriter, request *http
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
MicSessionId: body.MicSessionID,
|
||||
RoomVersion: body.RoomVersion,
|
||||
EventTimeMs: body.EventTimeMS,
|
||||
Source: body.Source,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// setMicMute 把麦克风静音 HTTP 请求转换为 room-service SetMicMute 命令。
|
||||
@ -822,16 +869,16 @@ func (h *Handler) setMicMute(writer http.ResponseWriter, request *http.Request)
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
Muted: body.Muted,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// setMicSeatLock 把锁麦 HTTP JSON 请求转换为 room-service SetMicSeatLock 命令。
|
||||
@ -843,16 +890,16 @@ func (h *Handler) setMicSeatLock(writer http.ResponseWriter, request *http.Reque
|
||||
Locked bool `json:"locked"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
Locked: body.Locked,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// setChatEnabled 把公屏开关 HTTP JSON 请求转换为 room-service SetChatEnabled 命令。
|
||||
@ -863,15 +910,46 @@ func (h *Handler) setChatEnabled(writer http.ResponseWriter, request *http.Reque
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
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 命令。
|
||||
@ -883,16 +961,16 @@ func (h *Handler) setRoomAdmin(writer http.ResponseWriter, request *http.Request
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
Enabled: body.Enabled,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// muteUser 把禁言 HTTP JSON 请求转换为 room-service MuteUser 命令。
|
||||
@ -904,16 +982,16 @@ func (h *Handler) muteUser(writer http.ResponseWriter, request *http.Request) {
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
Muted: body.Muted,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
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{
|
||||
Meta: meta(request, body.RoomID, body.CommandID),
|
||||
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||
TargetType: targetType,
|
||||
TargetUserIds: targetUserIDs,
|
||||
TargetUserId: firstUserID(targetUserIDs),
|
||||
GiftId: body.GiftID,
|
||||
GiftCount: body.GiftCount,
|
||||
})
|
||||
write(writer, request, resp, err)
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
// 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)})
|
||||
}
|
||||
|
||||
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 {
|
||||
profileMap := h.roomDisplayProfileMap(request, userIDs)
|
||||
profiles := make([]roomDisplayProfileData, 0, len(userIDs))
|
||||
@ -1025,7 +1128,7 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
|
||||
return nil
|
||||
}
|
||||
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserIds: userIDs,
|
||||
})
|
||||
if err != nil {
|
||||
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package roomapi
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
@ -29,6 +29,7 @@ type roomListItemData struct {
|
||||
CoverURL string `json:"cover_url,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Locked bool `json:"locked"`
|
||||
Heat int64 `json:"heat"`
|
||||
OnlineCount int32 `json:"online_count"`
|
||||
SeatCount int32 `json:"seat_count"`
|
||||
@ -53,6 +54,7 @@ type joinRoomData struct {
|
||||
type roomDetailData struct {
|
||||
Room roomInitialData `json:"room"`
|
||||
Viewer roomViewerData `json:"viewer"`
|
||||
IsFollowed bool `json:"is_followed"`
|
||||
Seats []roomSeatData `json:"seats"`
|
||||
RankTop []roomRankItemData `json:"rank_top"`
|
||||
OnlineCount int32 `json:"online_count"`
|
||||
@ -137,9 +139,17 @@ type roomSnapshotData struct {
|
||||
Room roomInitialData `json:"room"`
|
||||
Seats []roomSeatData `json:"seats"`
|
||||
ContributionRank []roomRankItemData `json:"contribution_rank"`
|
||||
IsFollowed bool `json:"is_followed"`
|
||||
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 {
|
||||
Applied bool `json:"applied"`
|
||||
RoomVersion int64 `json:"room_version"`
|
||||
@ -157,6 +167,7 @@ type roomInitialData struct {
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
ChatEnabled bool `json:"chat_enabled"`
|
||||
Locked bool `json:"locked"`
|
||||
Heat int64 `json:"heat"`
|
||||
OnlineCount int32 `json:"online_count"`
|
||||
SeatCount int32 `json:"seat_count"`
|
||||
@ -244,6 +255,7 @@ func roomListItemDataFromProto(room *roomv1.RoomListItem) roomListItemData {
|
||||
CoverURL: room.GetCoverUrl(),
|
||||
Mode: room.GetMode(),
|
||||
Status: room.GetStatus(),
|
||||
Locked: room.GetLocked(),
|
||||
Heat: room.GetHeat(),
|
||||
OnlineCount: room.GetOnlineCount(),
|
||||
SeatCount: room.GetSeatCount(),
|
||||
@ -287,6 +299,7 @@ func roomSnapshotDataFromProto(resp *roomv1.GetRoomSnapshotResponse) roomSnapsho
|
||||
Room: roomInitialRoomDataFromSnapshot(snapshot, snapshot.GetRoomId()),
|
||||
Seats: roomSeatDataFromSnapshot(snapshot),
|
||||
ContributionRank: roomRankDataFromSnapshot(snapshot, 0),
|
||||
IsFollowed: resp.GetIsFollowed(),
|
||||
ServerTimeMS: resp.GetServerTimeMs(),
|
||||
}
|
||||
}
|
||||
@ -315,6 +328,7 @@ func roomInitialRoomDataFromSnapshot(snapshot *roomv1.RoomSnapshot, roomID strin
|
||||
Mode: snapshot.GetMode(),
|
||||
Status: snapshot.GetStatus(),
|
||||
ChatEnabled: snapshot.GetChatEnabled(),
|
||||
Locked: snapshot.GetLocked(),
|
||||
Heat: snapshot.GetHeat(),
|
||||
OnlineCount: int32(len(snapshot.GetOnlineUsers())),
|
||||
SeatCount: seatCount,
|
||||
@ -364,6 +378,7 @@ func roomDetailDataFromSnapshot(resp *roomv1.GetRoomSnapshotResponse, viewerUser
|
||||
return roomDetailData{
|
||||
Room: roomInitialRoomDataFromSnapshot(snapshot, roomID),
|
||||
Viewer: roomViewerDataFromProto(viewer, viewerUserID),
|
||||
IsFollowed: resp.GetIsFollowed(),
|
||||
Seats: roomSeatDataFromSnapshot(snapshot),
|
||||
RankTop: roomRankDataFromSnapshot(snapshot, joinRoomContributionRankLimit),
|
||||
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 {
|
||||
if snapshot == nil || viewerUserID <= 0 {
|
||||
return roomPermissionsData{}
|
||||
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package roomapi
|
||||
|
||||
import (
|
||||
"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"`
|
||||
}
|
||||
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
var timeNow = defaultTimeNow
|
||||
var TimeNow = defaultTimeNow
|
||||
|
||||
func defaultTimeNow() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
func (h *Handler) tencentRTCTokenConfig() tencentrtc.TokenConfig {
|
||||
return 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,
|
||||
}
|
||||
return h.rtcTokenConfig
|
||||
}
|
||||
|
||||
func (h *Handler) generateTencentRTCToken(userID int64, roomID string) (tencentrtc.TokenResult, error) {
|
||||
// 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 (
|
||||
"net/http"
|
||||
|
||||
"hyapp/pkg/tencentrtc"
|
||||
"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/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 表。
|
||||
// 路径注册集中在 httproutes,gateway 依赖装配和鉴权 wrapper 仍留在 transport 入口层。
|
||||
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{
|
||||
PublicWrap: h.publicAPIHandler,
|
||||
AuthWrap: func(handler http.HandlerFunc) http.Handler {
|
||||
@ -26,109 +115,29 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
||||
Logout: h.logout,
|
||||
},
|
||||
App: httproutes.AppHandlers{
|
||||
ListRegistrationCountries: h.listRegistrationCountries,
|
||||
GetAppBootstrap: h.getAppBootstrap,
|
||||
ListH5Links: h.listH5Links,
|
||||
ListAppBanners: h.listAppBanners,
|
||||
GetAppVersion: h.getAppVersion,
|
||||
ListResources: h.listResources,
|
||||
GetResourceGroup: h.getResourceGroup,
|
||||
ListGifts: h.listGifts,
|
||||
ListRegistrationCountries: userAPI.ListRegistrationCountries,
|
||||
GetAppBootstrap: appAPI.GetAppBootstrap,
|
||||
ListH5Links: appAPI.ListH5Links,
|
||||
ListAppBanners: appAPI.ListAppBanners,
|
||||
GetAppVersion: appAPI.GetAppVersion,
|
||||
ListResources: resourceAPI.ListResources,
|
||||
GetResourceGroup: resourceAPI.GetResourceGroup,
|
||||
ListGifts: resourceAPI.ListGifts,
|
||||
IssueTencentIMUserSig: h.issueTencentIMUserSig,
|
||||
IssueTencentRTCToken: h.issueTencentRTCToken,
|
||||
HandleTencentIMCallback: h.handleTencentIMCallback,
|
||||
HandleTencentRTCCallback: h.handleTencentRTCCallback,
|
||||
UploadFile: h.uploadFile,
|
||||
UploadAvatar: h.uploadAvatar,
|
||||
HandlePushToken: h.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,
|
||||
IssueTencentRTCToken: roomAPI.IssueTencentRTCToken,
|
||||
HandleTencentIMCallback: callbackAPI.HandleTencentIMCallback,
|
||||
HandleTencentRTCCallback: callbackAPI.HandleTencentRTCCallback,
|
||||
UploadFile: appAPI.UploadFile,
|
||||
UploadAvatar: appAPI.UploadAvatar,
|
||||
HandlePushToken: appAPI.HandlePushToken,
|
||||
},
|
||||
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 (
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
@ -37,7 +37,7 @@ func (h *Handler) getMyHostIdentity(writer http.ResponseWriter, request *http.Re
|
||||
identity := userHostIdentityData{}
|
||||
|
||||
hostResp, err := h.userHostClient.GetHostProfile(request.Context(), &userv1.GetHostProfileRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: userID,
|
||||
})
|
||||
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{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: userID,
|
||||
})
|
||||
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{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -85,7 +85,7 @@ func (h *Handler) getMyRoleSummary(writer http.ResponseWriter, request *http.Req
|
||||
return
|
||||
}
|
||||
resp, err := h.userHostClient.GetUserRoleSummary(request.Context(), &userv1.GetUserRoleSummaryRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
})
|
||||
if err != nil {
|
||||
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package userapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -91,7 +91,7 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
|
||||
var profileErr error
|
||||
wg.Go(func() {
|
||||
profileResp, profileErr = h.userProfileClient.GetUser(ctx, &userv1.GetUserRequest{
|
||||
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
||||
Meta: httpkit.UserMeta(request.WithContext(ctx), ""),
|
||||
UserId: userID,
|
||||
})
|
||||
})
|
||||
@ -134,7 +134,7 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
|
||||
var statsErr error
|
||||
wg.Go(func() {
|
||||
statsResp, statsErr = h.userProfileClient.GetMyProfileStats(ctx, &userv1.GetMyProfileStatsRequest{
|
||||
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
||||
Meta: httpkit.UserMeta(request.WithContext(ctx), ""),
|
||||
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{
|
||||
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
||||
Meta: httpkit.UserMeta(request.WithContext(ctx), ""),
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package userapi
|
||||
|
||||
import (
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
@ -48,7 +48,7 @@ func (h *Handler) listMyProfileVisitors(writer http.ResponseWriter, request *htt
|
||||
return
|
||||
}
|
||||
resp, err := h.userSocialClient.ListProfileVisitors(request.Context(), &userv1.ListProfileVisitorsRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
@ -76,7 +76,7 @@ func (h *Handler) listMyFollowing(writer http.ResponseWriter, request *http.Requ
|
||||
return
|
||||
}
|
||||
resp, err := h.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
@ -104,7 +104,7 @@ func (h *Handler) listMyFriends(writer http.ResponseWriter, request *http.Reques
|
||||
return
|
||||
}
|
||||
resp, err := h.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
@ -132,7 +132,7 @@ func (h *Handler) listMyFriendApplications(writer http.ResponseWriter, request *
|
||||
return
|
||||
}
|
||||
resp, err := h.userSocialClient.ListFriendApplications(request.Context(), &userv1.ListFriendApplicationsRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
Direction: strings.TrimSpace(request.URL.Query().Get("direction")),
|
||||
Page: page,
|
||||
@ -184,7 +184,7 @@ func (h *Handler) recordProfileVisit(writer http.ResponseWriter, request *http.R
|
||||
return
|
||||
}
|
||||
resp, err := h.userSocialClient.RecordProfileVisit(request.Context(), &userv1.RecordProfileVisitRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
VisitorUserId: auth.UserIDFromContext(request.Context()),
|
||||
TargetUserId: targetUserID,
|
||||
})
|
||||
@ -200,7 +200,7 @@ func (h *Handler) handleFollowAction(writer http.ResponseWriter, request *http.R
|
||||
switch request.Method {
|
||||
case http.MethodPost:
|
||||
resp, err := h.userSocialClient.FollowUser(request.Context(), &userv1.FollowUserRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
FollowerUserId: userID,
|
||||
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())})
|
||||
case http.MethodDelete:
|
||||
resp, err := h.userSocialClient.UnfollowUser(request.Context(), &userv1.UnfollowUserRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
FollowerUserId: userID,
|
||||
FolloweeUserId: followeeUserID,
|
||||
})
|
||||
@ -231,7 +231,7 @@ func (h *Handler) applyFriend(writer http.ResponseWriter, request *http.Request,
|
||||
return
|
||||
}
|
||||
resp, err := h.userSocialClient.ApplyFriend(request.Context(), &userv1.ApplyFriendRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
RequesterUserId: auth.UserIDFromContext(request.Context()),
|
||||
TargetUserId: targetUserID,
|
||||
})
|
||||
@ -248,7 +248,7 @@ func (h *Handler) acceptFriendApplication(writer http.ResponseWriter, request *h
|
||||
return
|
||||
}
|
||||
resp, err := h.userSocialClient.AcceptFriendApplication(request.Context(), &userv1.AcceptFriendApplicationRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
AccepterUserId: auth.UserIDFromContext(request.Context()),
|
||||
RequesterUserId: requesterUserID,
|
||||
})
|
||||
@ -265,7 +265,7 @@ func (h *Handler) deleteFriend(writer http.ResponseWriter, request *http.Request
|
||||
return
|
||||
}
|
||||
resp, err := h.userSocialClient.DeleteFriend(request.Context(), &userv1.DeleteFriendRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
FriendUserId: friendUserID,
|
||||
})
|
||||
@ -277,11 +277,11 @@ func (h *Handler) deleteFriend(writer http.ResponseWriter, request *http.Request
|
||||
}
|
||||
|
||||
func socialPage(request *http.Request) (int32, int32, bool) {
|
||||
page, ok := parsePositiveInt32Query(request, "page", 1)
|
||||
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
|
||||
if !ok {
|
||||
return 0, 0, false
|
||||
}
|
||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
|
||||
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||
if !ok {
|
||||
return 0, 0, false
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
package http
|
||||
package userapi
|
||||
|
||||
import (
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@ -11,6 +10,7 @@ import (
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
"hyapp/pkg/logx"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
)
|
||||
|
||||
// userIdentityData 是 gateway 对外返回的当前短号投影。
|
||||
@ -93,7 +93,7 @@ func (h *Handler) listRegistrationCountries(writer http.ResponseWriter, request
|
||||
}
|
||||
|
||||
resp, err := h.userCountryClient.ListRegistrationCountries(request.Context(), &userv1.ListRegistrationCountriesRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
})
|
||||
if err != nil {
|
||||
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{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
})
|
||||
if err != nil {
|
||||
@ -139,7 +139,7 @@ func (h *Handler) resolveDisplayUserID(writer http.ResponseWriter, request *http
|
||||
return
|
||||
}
|
||||
resp, err := h.userIdentityClient.ResolveDisplayUserID(request.Context(), &userv1.ResolveDisplayUserIDRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
DisplayUserId: displayUserID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -160,7 +160,7 @@ func (h *Handler) completeMyOnboarding(writer http.ResponseWriter, request *http
|
||||
Country string `json:"country"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
}
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
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{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
Username: body.Username,
|
||||
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{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
})
|
||||
if err != nil {
|
||||
@ -216,7 +216,7 @@ func (h *Handler) updateMyProfile(writer http.ResponseWriter, request *http.Requ
|
||||
Gender *string `json:"gender"`
|
||||
Birth *string `json:"birth"`
|
||||
}
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
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{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
Username: body.Username,
|
||||
Avatar: body.Avatar,
|
||||
@ -245,7 +245,7 @@ func (h *Handler) changeMyCountry(writer http.ResponseWriter, request *http.Requ
|
||||
var body struct {
|
||||
Country string `json:"country"`
|
||||
}
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
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{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
Country: body.Country,
|
||||
})
|
||||
@ -272,7 +272,7 @@ func (h *Handler) removeOldRegionBroadcastMemberBestEffort(request *http.Request
|
||||
return
|
||||
}
|
||||
resp, err := h.broadcastClient.RemoveRegionBroadcastMember(request.Context(), &activityv1.RemoveRegionBroadcastMemberRequest{
|
||||
Meta: activityMeta(request),
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: userID,
|
||||
RegionId: oldRegionID,
|
||||
Reason: reason,
|
||||
@ -303,7 +303,7 @@ func (h *Handler) changeMyDisplayUserID(writer http.ResponseWriter, request *htt
|
||||
NewDisplayUserID string `json:"new_display_user_id"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
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{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
NewDisplayUserId: body.NewDisplayUserID,
|
||||
Reason: body.Reason,
|
||||
@ -333,7 +333,7 @@ func (h *Handler) applyMyPrettyDisplayUserID(writer http.ResponseWriter, request
|
||||
LeaseDurationMs int64 `json:"lease_duration_ms"`
|
||||
PaymentReceiptID string `json:"payment_receipt_id"`
|
||||
}
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
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{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
PrettyDisplayUserId: body.PrettyDisplayUserID,
|
||||
LeaseDurationMs: body.LeaseDurationMs,
|
||||
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package walletapi
|
||||
|
||||
import (
|
||||
"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())
|
||||
profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -182,7 +182,7 @@ func (h *Handler) listRechargeProducts(writer http.ResponseWriter, request *http
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
UserId: userID,
|
||||
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 {
|
||||
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")
|
||||
return
|
||||
}
|
||||
page, ok := parsePositiveInt32Query(request, "page", 1)
|
||||
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
pageSize, ok := parsePositiveInt32Query(request, "page_size", defaultPageSize)
|
||||
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", defaultPageSize)
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
@ -286,7 +286,7 @@ func (h *Handler) applyWithdrawal(writer http.ResponseWriter, request *http.Requ
|
||||
return
|
||||
}
|
||||
var body withdrawalApplyRequestBody
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
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 (
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
@ -57,7 +57,7 @@ func (h *Handler) purchaseVIP(writer http.ResponseWriter, request *http.Request)
|
||||
return
|
||||
}
|
||||
var body vipPurchaseRequestBody
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
commandID := strings.TrimSpace(body.CommandID)
|
||||
@ -1,4 +1,4 @@
|
||||
package http
|
||||
package walletapi
|
||||
|
||||
import (
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
@ -104,7 +104,7 @@ func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *ht
|
||||
return
|
||||
}
|
||||
var body coinSellerTransferRequestBody
|
||||
if !decode(writer, request, &body) {
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
commandID := body.normalizedCommandID()
|
||||
@ -117,7 +117,7 @@ func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *ht
|
||||
|
||||
sellerUserID := auth.UserIDFromContext(request.Context())
|
||||
profileResp, err := h.userHostClient.GetCoinSellerProfile(request.Context(), &userv1.GetCoinSellerProfileRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: sellerUserID,
|
||||
})
|
||||
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) {
|
||||
resp, err := h.userIdentityClient.ResolveDisplayUserID(request.Context(), &userv1.ResolveDisplayUserIDRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
DisplayUserId: targetDisplayUserID,
|
||||
})
|
||||
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) {
|
||||
sellerResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: sellerUserID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -194,7 +194,7 @@ func (h *Handler) coinSellerTransferRegions(writer http.ResponseWriter, request
|
||||
return 0, 0, false
|
||||
}
|
||||
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||
Meta: authRequestMeta(request, ""),
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: targetUserID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -5,6 +5,7 @@ grpc_addr: ":13009"
|
||||
health_http_addr: ":13109"
|
||||
mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||
wallet_database: "hyapp_wallet"
|
||||
room_database: "hyapp_room"
|
||||
mysql_auto_migrate: false
|
||||
|
||||
tencent_im:
|
||||
@ -26,6 +27,16 @@ wallet_notice_worker:
|
||||
initial_backoff: 5s
|
||||
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:
|
||||
level: info
|
||||
format: json
|
||||
|
||||
@ -5,6 +5,7 @@ grpc_addr: ":13009"
|
||||
health_http_addr: ":13109"
|
||||
mysql_dsn: "hyapp:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:3306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||
wallet_database: "hyapp_wallet"
|
||||
room_database: "hyapp_room"
|
||||
mysql_auto_migrate: false
|
||||
|
||||
tencent_im:
|
||||
@ -26,6 +27,16 @@ wallet_notice_worker:
|
||||
initial_backoff: 5s
|
||||
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:
|
||||
level: info
|
||||
format: json
|
||||
|
||||
@ -5,6 +5,7 @@ grpc_addr: ":13009"
|
||||
health_http_addr: ":13109"
|
||||
mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||
wallet_database: "hyapp_wallet"
|
||||
room_database: "hyapp_room"
|
||||
mysql_auto_migrate: false
|
||||
|
||||
tencent_im:
|
||||
@ -26,6 +27,16 @@ wallet_notice_worker:
|
||||
initial_backoff: 5s
|
||||
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:
|
||||
level: info
|
||||
format: json
|
||||
|
||||
@ -16,6 +16,7 @@ import (
|
||||
"hyapp/pkg/logx"
|
||||
"hyapp/pkg/tencentim"
|
||||
"hyapp/services/notice-service/internal/config"
|
||||
"hyapp/services/notice-service/internal/modules/roomnotice"
|
||||
"hyapp/services/notice-service/internal/modules/walletnotice"
|
||||
mysqlplatform "hyapp/services/notice-service/internal/platform/mysql"
|
||||
)
|
||||
@ -30,6 +31,9 @@ type App struct {
|
||||
walletNoticeService *walletnotice.Service
|
||||
walletWorkerOptions walletnotice.WalletNoticeWorkerOptions
|
||||
walletWorkerEnabled bool
|
||||
roomNoticeService *roomnotice.Service
|
||||
roomWorkerOptions roomnotice.RoomNoticeWorkerOptions
|
||||
roomWorkerEnabled bool
|
||||
workerCtx context.Context
|
||||
workerCancel context.CancelFunc
|
||||
workerWG sync.WaitGroup
|
||||
@ -57,6 +61,12 @@ func New(cfg config.Config) (*App, error) {
|
||||
_ = store.Close()
|
||||
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
|
||||
if cfg.TencentIM.Enabled {
|
||||
tencentClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
|
||||
@ -72,6 +82,11 @@ func New(cfg config.Config) (*App, error) {
|
||||
_ = store.Close()
|
||||
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")))
|
||||
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),
|
||||
walletWorkerOptions: walletNoticeWorkerOptions(cfg.WalletNoticeWorker),
|
||||
walletWorkerEnabled: cfg.WalletNoticeWorker.Enabled,
|
||||
roomNoticeService: roomnotice.New(roomnotice.Config{NodeID: cfg.NodeID}, roomRepo, publisher),
|
||||
roomWorkerOptions: roomNoticeWorkerOptions(cfg.RoomNoticeWorker),
|
||||
roomWorkerEnabled: cfg.RoomNoticeWorker.Enabled,
|
||||
workerCtx: workerCtx,
|
||||
workerCancel: workerCancel,
|
||||
}, nil
|
||||
@ -117,6 +135,11 @@ func (a *App) Run() error {
|
||||
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)
|
||||
if errors.Is(err, grpc.ErrServerStopped) {
|
||||
return nil
|
||||
@ -171,3 +194,15 @@ func walletNoticeWorkerOptions(cfg config.WalletNoticeWorkerConfig) walletnotice
|
||||
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"`
|
||||
// WalletDatabase 是 wallet_outbox 所在库名;notice 只把它当 append-only 事实源读取。
|
||||
WalletDatabase string `yaml:"wallet_database"`
|
||||
// RoomDatabase 是 room_outbox 所在库名;notice 只读取需要私有通知的房间事实。
|
||||
RoomDatabase string `yaml:"room_database"`
|
||||
// TencentIM 是私有 IM 投递通道配置;本地默认关闭,避免误发真实消息。
|
||||
TencentIM TencentIMConfig `yaml:"tencent_im"`
|
||||
// WalletNoticeWorker 消费 wallet_outbox 中的 WalletBalanceChanged 事件。
|
||||
WalletNoticeWorker WalletNoticeWorkerConfig `yaml:"wallet_notice_worker"`
|
||||
MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
|
||||
Log logx.Config `yaml:"log"`
|
||||
// RoomNoticeWorker 消费 room_outbox 中需要给单个用户发送的房间通知。
|
||||
RoomNoticeWorker WalletNoticeWorkerConfig `yaml:"room_notice_worker"`
|
||||
MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
|
||||
Log logx.Config `yaml:"log"`
|
||||
}
|
||||
|
||||
// TencentIMConfig 描述服务端调用腾讯云 IM REST API 的配置。
|
||||
@ -79,6 +83,7 @@ func Default() Config {
|
||||
HealthHTTPAddr: ":13109",
|
||||
MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_notice?parseTime=true&charset=utf8mb4&loc=UTC",
|
||||
WalletDatabase: "hyapp_wallet",
|
||||
RoomDatabase: "hyapp_room",
|
||||
MySQLAutoMigrate: false,
|
||||
TencentIM: TencentIMConfig{
|
||||
Enabled: false,
|
||||
@ -97,6 +102,16 @@ func Default() Config {
|
||||
InitialBackoff: 5 * time.Second,
|
||||
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{
|
||||
Level: "info",
|
||||
Format: "json",
|
||||
@ -138,7 +153,12 @@ func Load(path string) (Config, error) {
|
||||
if cfg.WalletDatabase == "" {
|
||||
cfg.WalletDatabase = "hyapp_wallet"
|
||||
}
|
||||
cfg.RoomDatabase = strings.TrimSpace(cfg.RoomDatabase)
|
||||
if cfg.RoomDatabase == "" {
|
||||
cfg.RoomDatabase = "hyapp_room"
|
||||
}
|
||||
cfg.WalletNoticeWorker = normalizeWalletNoticeWorker(cfg.WalletNoticeWorker)
|
||||
cfg.RoomNoticeWorker = normalizeWalletNoticeWorker(cfg.RoomNoticeWorker)
|
||||
if strings.TrimSpace(cfg.TencentIM.AdminIdentifier) == "" {
|
||||
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,
|
||||
mode VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
room_password_hash VARCHAR(128) NOT NULL DEFAULT '',
|
||||
visible_region_id BIGINT NOT NULL DEFAULT 0,
|
||||
created_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 '',
|
||||
mode VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
locked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
heat BIGINT NOT NULL DEFAULT 0,
|
||||
online_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)
|
||||
) 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 (
|
||||
app_code VARCHAR(32) 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.Attributes = map[string]string{"enabled": fmt.Sprintf("%t", body.GetEnabled())}
|
||||
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":
|
||||
// 管理员变更进入系统消息,客户端收到后仍应以最新 snapshot 修正本地权限 UI。
|
||||
var body roomeventsv1.RoomAdminChanged
|
||||
@ -259,6 +268,8 @@ func eventTypeForClient(eventType string) string {
|
||||
return "room_profile_updated"
|
||||
case "RoomChatEnabledChanged":
|
||||
return "room_chat_enabled_changed"
|
||||
case "RoomPasswordChanged":
|
||||
return "room_password_changed"
|
||||
case "RoomAdminChanged":
|
||||
return "room_admin_changed"
|
||||
case "RoomUserMuted":
|
||||
|
||||
@ -241,6 +241,20 @@ type SetChatEnabled struct {
|
||||
// Type 返回命令类型。
|
||||
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 定义添加或移除管理员请求。
|
||||
type SetRoomAdmin struct {
|
||||
Base
|
||||
@ -360,6 +374,8 @@ func IdempotencyPayload(payload []byte) ([]byte, error) {
|
||||
delete(values, "gift_point_added")
|
||||
delete(values, "heat_value")
|
||||
delete(values, "price_version")
|
||||
// SetRoomPassword 的哈希带随机盐;通用 payload 比较先去掉哈希,service 层再用 transient 明文和已存 bcrypt 哈希确认是否同一密码。
|
||||
delete(values, "password_hash")
|
||||
// MicUp 的发流确认 deadline 由 room-service 生成,客户端重试同一 command_id 时不能因此冲突。
|
||||
delete(values, "publish_deadline_ms")
|
||||
|
||||
@ -400,6 +416,8 @@ func Deserialize(commandType string, payload []byte) (Command, error) {
|
||||
cmd = &SetMicSeatLock{}
|
||||
case SetChatEnabled{}.Type():
|
||||
cmd = &SetChatEnabled{}
|
||||
case SetRoomPassword{}.Type():
|
||||
cmd = &SetRoomPassword{}
|
||||
case SetRoomAdmin{}.Type():
|
||||
cmd = &SetRoomAdmin{}
|
||||
case MuteUser{}.Type():
|
||||
|
||||
@ -188,14 +188,15 @@ func (s *Service) CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest)
|
||||
// 首次创建按 meta -> command log -> outbox -> snapshot 的顺序落盘,保证恢复有基础元数据和首版本命令。
|
||||
saveMetaStartedAt := time.Now()
|
||||
if err := s.repository.SaveRoomMeta(ctx, RoomMeta{
|
||||
AppCode: cmd.AppCode,
|
||||
RoomID: cmd.RoomID(),
|
||||
OwnerUserID: cmd.OwnerUserID,
|
||||
SeatCount: cmd.SeatCount,
|
||||
Mode: cmd.Mode,
|
||||
Status: state.RoomStatusActive,
|
||||
VisibleRegionID: cmd.VisibleRegionID,
|
||||
RoomShortID: cmd.RoomShortID,
|
||||
AppCode: cmd.AppCode,
|
||||
RoomID: cmd.RoomID(),
|
||||
OwnerUserID: cmd.OwnerUserID,
|
||||
SeatCount: cmd.SeatCount,
|
||||
Mode: cmd.Mode,
|
||||
Status: state.RoomStatusActive,
|
||||
RoomPasswordHash: "",
|
||||
VisibleRegionID: cmd.VisibleRegionID,
|
||||
RoomShortID: cmd.RoomShortID,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -32,9 +32,11 @@ func (s *Service) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoom
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
// 没有用户当前房间投影时直接返回空结果,不扫描房间列表或所有 Room Cell。
|
||||
return emptyCurrentRoomResponse(serverTimeMS), nil
|
||||
}
|
||||
|
||||
// presence 只是恢复入口的索引,必须用 Room Cell 快照确认房间仍 active 且用户仍在房间内。
|
||||
snapshot, err := s.currentSnapshot(ctx, presence.RoomID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -54,6 +56,7 @@ func (s *Service) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoom
|
||||
micSessionID := ""
|
||||
needRTCToken := false
|
||||
if seat != nil {
|
||||
// 上麦用户回前台需要 RTC token 恢复媒体链路;普通房间用户只需要重新加 IM 群。
|
||||
publishState = seat.GetPublishState()
|
||||
micSessionID = seat.GetMicSessionId()
|
||||
needRTCToken = publishState == state.MicPublishPending || publishState == state.MicPublishPublishing
|
||||
@ -72,15 +75,19 @@ func (s *Service) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoom
|
||||
}, nil
|
||||
}
|
||||
|
||||
// emptyCurrentRoomResponse 保留 server_time_ms,方便客户端用同一套时钟校正逻辑处理空结果。
|
||||
func emptyCurrentRoomResponse(serverTimeMS int64) *roomv1.GetCurrentRoomResponse {
|
||||
return &roomv1.GetCurrentRoomResponse{ServerTimeMs: serverTimeMS}
|
||||
}
|
||||
|
||||
// currentRoomSnapshotRestorable 判定读模型指向的房间是否仍可被客户端恢复。
|
||||
func currentRoomSnapshotRestorable(snapshot *roomv1.RoomSnapshot, userID int64) bool {
|
||||
if snapshot == nil || snapshot.GetRoomId() == "" || snapshot.GetStatus() != state.RoomStatusActive {
|
||||
// 非 active 房间不能恢复,即使用户级 presence 投影还没被清理。
|
||||
return false
|
||||
}
|
||||
if containsUserID(snapshot.GetBanUserIds(), userID) {
|
||||
// ban 优先于旧 presence,避免被踢用户通过本地恢复入口重新收消息。
|
||||
return false
|
||||
}
|
||||
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 {
|
||||
if snapshot == nil || snapshot.GetRoomId() == "" {
|
||||
return RoomPresenceSnapshot{}
|
||||
}
|
||||
|
||||
// 先按 user_id 建立麦位索引,投影用户时能补齐发流状态而不做双层循环。
|
||||
seatsByUser := make(map[int64]*roomv1.SeatState, len(snapshot.GetMicSeats()))
|
||||
for _, seat := range snapshot.GetMicSeats() {
|
||||
if seat.GetUserId() > 0 {
|
||||
@ -116,6 +126,7 @@ func roomPresenceProjectionFromSnapshot(snapshot *roomv1.RoomSnapshot, updatedAt
|
||||
|
||||
users := make([]RoomPresence, 0, len(snapshot.GetOnlineUsers()))
|
||||
if snapshot.GetStatus() == state.RoomStatusActive {
|
||||
// 只有 active 房间才投影 active 用户;关闭房间通过空 users 让 repository 收敛旧 presence。
|
||||
for _, user := range snapshot.GetOnlineUsers() {
|
||||
if user.GetUserId() <= 0 {
|
||||
continue
|
||||
@ -131,6 +142,7 @@ func roomPresenceProjectionFromSnapshot(snapshot *roomv1.RoomSnapshot, updatedAt
|
||||
UpdatedAtMS: updatedAtMS,
|
||||
}
|
||||
if seat := seatsByUser[user.GetUserId()]; seat != nil {
|
||||
// 发流状态只对当前上麦用户有意义,非上麦用户保持空字段。
|
||||
entry.PublishState = seat.GetPublishState()
|
||||
entry.MicSessionID = seat.GetMicSessionId()
|
||||
}
|
||||
@ -148,6 +160,7 @@ func roomPresenceProjectionFromSnapshot(snapshot *roomv1.RoomSnapshot, updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
// containsUserID 是当前房间恢复入口的本地集合查询,独立保留便于后续替换为二分查找。
|
||||
func containsUserID(values []int64, target int64) bool {
|
||||
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 {
|
||||
// AppCode 是运行态 Cell 所属租户,后台 worker 必须用它恢复 ctx。
|
||||
AppCode string
|
||||
RoomID string
|
||||
// RoomID 是已装载 Room Cell 的业务房间 ID。
|
||||
RoomID string
|
||||
}
|
||||
|
||||
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 {
|
||||
appCode = appcode.Normalize(appCode)
|
||||
// 使用 NUL 分隔 app_code 和 room_id,避免普通字符串拼接出现歧义。
|
||||
return appCode + "\x00" + roomID
|
||||
}
|
||||
|
||||
@ -60,10 +63,12 @@ func RuntimeRoomKey(appCode string, roomID string) string {
|
||||
}
|
||||
|
||||
func runtimeRoomKeyFromContext(ctx context.Context, roomID string) string {
|
||||
// 命令入口统一从 ctx 读取 app_code,避免 service 方法忘记把租户键传给 Redis lease。
|
||||
return runtimeRoomKey(appcode.FromContext(ctx), roomID)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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) {
|
||||
// 使用同一个 lease token 做 fencing 校验,防止 worker 批处理期间 owner 已经切走。
|
||||
return s.directory.VerifyOwner(ctx, runtimeRoomKey(roomRef.AppCode, roomRef.RoomID), s.nodeID, lease.LeaseToken, now)
|
||||
}
|
||||
|
||||
func runtimeRoomRef(key string) loadedRoomRef {
|
||||
// runtime key 是内部格式;解析失败时保留 room_id,便于旧测试数据仍能被安全跳过或排查。
|
||||
for index := 0; index < len(key); index++ {
|
||||
if key[index] == 0 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
// 进房入口使用专门错误码,gateway 可以稳定映射成客户端房间关闭态。
|
||||
return xerr.New(xerr.RoomClosed, "room closed")
|
||||
}
|
||||
|
||||
func (s *Service) isRoomClosed(ctx context.Context, roomID string) (bool, error) {
|
||||
// 关闭状态先读 rooms 元数据,避免为了拒绝进房恢复完整 Room Cell。
|
||||
roomMeta, exists, err := s.repository.GetRoomMeta(ctx, roomID)
|
||||
if err != nil || !exists {
|
||||
return false, err
|
||||
@ -33,6 +35,7 @@ func (s *Service) isRoomClosed(ctx context.Context, roomID string) (bool, error)
|
||||
|
||||
func isClosedRoomStatus(status string) bool {
|
||||
value := strings.TrimSpace(status)
|
||||
// 空状态兼容测试构造;非 active 一律按不可进入处理,closing 也不接收新用户。
|
||||
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")
|
||||
}
|
||||
if current.Status == state.RoomStatusClosed {
|
||||
// 关房命令幂等,重复请求只返回当前关闭快照,不重复写 RoomClosed 事件。
|
||||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||||
}
|
||||
if current.Status != state.RoomStatusActive && current.Status != state.RoomStatusClosing {
|
||||
// creating 等非开放状态不允许用 CloseRoom 直接跳过创建收尾语义。
|
||||
return mutationResult{}, nil, xerr.New(xerr.Conflict, "room is not active")
|
||||
}
|
||||
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))
|
||||
for _, seat := range current.MicSeats {
|
||||
if seat.UserID != 0 && seat.MicSessionID != "" {
|
||||
// 清场前先保存占麦会话,后续 ClearMicSession 会抹掉 mic_session_id。
|
||||
occupiedSeats = append(occupiedSeats, occupiedMicSeat{
|
||||
userID: seat.UserID,
|
||||
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.OnlineUsers = map[int64]*state.RoomUserState{}
|
||||
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