增加房间页面 我的, 以及我的页 聚合

This commit is contained in:
zhx 2026-05-11 17:18:52 +08:00
parent a19857a4b5
commit b95224bff1
112 changed files with 15358 additions and 8044 deletions

View File

@ -15,7 +15,7 @@ HY Voice Room 是语音房后端 monorepo。当前架构不再自研 IM 长连
- `activity-service` 当前承接每日任务、活动/系统消息基础能力和房间事件 consumer 边界;`room_outbox` 到 activity/audit 的实际消费链路按独立消费位点接入。 - `activity-service` 当前承接每日任务、活动/系统消息基础能力和房间事件 consumer 边界;`room_outbox` 到 activity/audit 的实际消费链路按独立消费位点接入。
- `cron-service` 当前负责后台调度、任务运行记录,以及通过内部 gRPC 触发 owner service 批处理;它不直接拥有房间、账务、用户或活动业务状态。 - `cron-service` 当前负责后台调度、任务运行记录,以及通过内部 gRPC 触发 owner service 批处理;它不直接拥有房间、账务、用户或活动业务状态。
- 腾讯云 IM 承担客户端长连接、群消息、公屏、单聊、离线/漫游和全球接入能力;本仓库不再部署 `im-service` - 腾讯云 IM 承担客户端长连接、群消息、公屏、单聊、离线/漫游和全球接入能力;本仓库不再部署 `im-service`
- 多 App 共用同一套后端服务,入口由 `gateway-service` 通过包名或 `app_code` 解析租户;所有业务库表按 `app_code` 隔离,设计细节见 `docs/multi-app-tenant-architecture.md`。 - 多 App 共用同一套后端服务,入口由 `gateway-service` 通过包名或 `app_code` 解析租户;所有业务库表按 `app_code` 隔离,设计细节见 `docs/多App租户架构.md`。
协议边界: 协议边界:
@ -119,7 +119,7 @@ services/room-service/configs/config.tencent.example.yaml
基础闭环实现顺序见: 基础闭环实现顺序见:
```text ```text
docs/voice-room-basic-loop-implementation.md docs/语音房基础闭环实现.md
``` ```
## Storage Model ## Storage Model

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.2 // - protoc v5.27.3
// source: proto/activity/v1/activity.proto // source: proto/activity/v1/activity.proto
package activityv1 package activityv1

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.34.2 // protoc-gen-go v1.35.1
// protoc v5.29.2 // protoc v5.27.3
// source: proto/events/room/v1/events.proto // source: proto/events/room/v1/events.proto
package roomeventsv1 package roomeventsv1
@ -38,11 +38,9 @@ type EventEnvelope struct {
func (x *EventEnvelope) Reset() { func (x *EventEnvelope) Reset() {
*x = EventEnvelope{} *x = EventEnvelope{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[0]
mi := &file_proto_events_room_v1_events_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *EventEnvelope) String() string { func (x *EventEnvelope) String() string {
@ -53,7 +51,7 @@ func (*EventEnvelope) ProtoMessage() {}
func (x *EventEnvelope) ProtoReflect() protoreflect.Message { func (x *EventEnvelope) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[0] mi := &file_proto_events_room_v1_events_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -135,11 +133,9 @@ type RoomCreated struct {
func (x *RoomCreated) Reset() { func (x *RoomCreated) Reset() {
*x = RoomCreated{} *x = RoomCreated{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[1]
mi := &file_proto_events_room_v1_events_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomCreated) String() string { func (x *RoomCreated) String() string {
@ -150,7 +146,7 @@ func (*RoomCreated) ProtoMessage() {}
func (x *RoomCreated) ProtoReflect() protoreflect.Message { func (x *RoomCreated) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[1] mi := &file_proto_events_room_v1_events_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -226,11 +222,9 @@ type RoomUserJoined struct {
func (x *RoomUserJoined) Reset() { func (x *RoomUserJoined) Reset() {
*x = RoomUserJoined{} *x = RoomUserJoined{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
mi := &file_proto_events_room_v1_events_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomUserJoined) String() string { func (x *RoomUserJoined) String() string {
@ -241,7 +235,7 @@ func (*RoomUserJoined) ProtoMessage() {}
func (x *RoomUserJoined) ProtoReflect() protoreflect.Message { func (x *RoomUserJoined) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[2] mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -281,11 +275,9 @@ type RoomUserLeft struct {
func (x *RoomUserLeft) Reset() { func (x *RoomUserLeft) Reset() {
*x = RoomUserLeft{} *x = RoomUserLeft{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
mi := &file_proto_events_room_v1_events_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomUserLeft) String() string { func (x *RoomUserLeft) String() string {
@ -296,7 +288,7 @@ func (*RoomUserLeft) ProtoMessage() {}
func (x *RoomUserLeft) ProtoReflect() protoreflect.Message { func (x *RoomUserLeft) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[3] mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -330,11 +322,9 @@ type RoomClosed struct {
func (x *RoomClosed) Reset() { func (x *RoomClosed) Reset() {
*x = RoomClosed{} *x = RoomClosed{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
mi := &file_proto_events_room_v1_events_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomClosed) String() string { func (x *RoomClosed) String() string {
@ -345,7 +335,7 @@ func (*RoomClosed) ProtoMessage() {}
func (x *RoomClosed) ProtoReflect() protoreflect.Message { func (x *RoomClosed) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[4] mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -397,11 +387,9 @@ type RoomMicChanged struct {
func (x *RoomMicChanged) Reset() { func (x *RoomMicChanged) Reset() {
*x = RoomMicChanged{} *x = RoomMicChanged{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
mi := &file_proto_events_room_v1_events_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomMicChanged) String() string { func (x *RoomMicChanged) String() string {
@ -412,7 +400,7 @@ func (*RoomMicChanged) ProtoMessage() {}
func (x *RoomMicChanged) ProtoReflect() protoreflect.Message { func (x *RoomMicChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[5] mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -510,11 +498,9 @@ type RoomMicSeatLocked struct {
func (x *RoomMicSeatLocked) Reset() { func (x *RoomMicSeatLocked) Reset() {
*x = RoomMicSeatLocked{} *x = RoomMicSeatLocked{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
mi := &file_proto_events_room_v1_events_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomMicSeatLocked) String() string { func (x *RoomMicSeatLocked) String() string {
@ -525,7 +511,7 @@ func (*RoomMicSeatLocked) ProtoMessage() {}
func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message { func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[6] mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -573,11 +559,9 @@ type RoomChatEnabledChanged struct {
func (x *RoomChatEnabledChanged) Reset() { func (x *RoomChatEnabledChanged) Reset() {
*x = RoomChatEnabledChanged{} *x = RoomChatEnabledChanged{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
mi := &file_proto_events_room_v1_events_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomChatEnabledChanged) String() string { func (x *RoomChatEnabledChanged) String() string {
@ -588,7 +572,7 @@ func (*RoomChatEnabledChanged) ProtoMessage() {}
func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message { func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[7] mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -630,11 +614,9 @@ type RoomAdminChanged struct {
func (x *RoomAdminChanged) Reset() { func (x *RoomAdminChanged) Reset() {
*x = RoomAdminChanged{} *x = RoomAdminChanged{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
mi := &file_proto_events_room_v1_events_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomAdminChanged) String() string { func (x *RoomAdminChanged) String() string {
@ -645,7 +627,7 @@ func (*RoomAdminChanged) ProtoMessage() {}
func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message { func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[8] mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -694,11 +676,9 @@ type RoomHostTransferred struct {
func (x *RoomHostTransferred) Reset() { func (x *RoomHostTransferred) Reset() {
*x = RoomHostTransferred{} *x = RoomHostTransferred{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
mi := &file_proto_events_room_v1_events_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomHostTransferred) String() string { func (x *RoomHostTransferred) String() string {
@ -709,7 +689,7 @@ func (*RoomHostTransferred) ProtoMessage() {}
func (x *RoomHostTransferred) ProtoReflect() protoreflect.Message { func (x *RoomHostTransferred) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[9] mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -758,11 +738,9 @@ type RoomUserMuted struct {
func (x *RoomUserMuted) Reset() { func (x *RoomUserMuted) Reset() {
*x = RoomUserMuted{} *x = RoomUserMuted{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
mi := &file_proto_events_room_v1_events_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomUserMuted) String() string { func (x *RoomUserMuted) String() string {
@ -773,7 +751,7 @@ func (*RoomUserMuted) ProtoMessage() {}
func (x *RoomUserMuted) ProtoReflect() protoreflect.Message { func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[10] mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -821,11 +799,9 @@ type RoomUserKicked struct {
func (x *RoomUserKicked) Reset() { func (x *RoomUserKicked) Reset() {
*x = RoomUserKicked{} *x = RoomUserKicked{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
mi := &file_proto_events_room_v1_events_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomUserKicked) String() string { func (x *RoomUserKicked) String() string {
@ -836,7 +812,7 @@ func (*RoomUserKicked) ProtoMessage() {}
func (x *RoomUserKicked) ProtoReflect() protoreflect.Message { func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[11] mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -877,11 +853,9 @@ type RoomUserUnbanned struct {
func (x *RoomUserUnbanned) Reset() { func (x *RoomUserUnbanned) Reset() {
*x = RoomUserUnbanned{} *x = RoomUserUnbanned{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
mi := &file_proto_events_room_v1_events_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomUserUnbanned) String() string { func (x *RoomUserUnbanned) String() string {
@ -892,7 +866,7 @@ func (*RoomUserUnbanned) ProtoMessage() {}
func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message { func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[12] mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -939,11 +913,9 @@ type RoomGiftSent struct {
func (x *RoomGiftSent) Reset() { func (x *RoomGiftSent) Reset() {
*x = RoomGiftSent{} *x = RoomGiftSent{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
mi := &file_proto_events_room_v1_events_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomGiftSent) String() string { func (x *RoomGiftSent) String() string {
@ -954,7 +926,7 @@ func (*RoomGiftSent) ProtoMessage() {}
func (x *RoomGiftSent) ProtoReflect() protoreflect.Message { func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[13] mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -1037,11 +1009,9 @@ type RoomHeatChanged struct {
func (x *RoomHeatChanged) Reset() { func (x *RoomHeatChanged) Reset() {
*x = RoomHeatChanged{} *x = RoomHeatChanged{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
mi := &file_proto_events_room_v1_events_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomHeatChanged) String() string { func (x *RoomHeatChanged) String() string {
@ -1052,7 +1022,7 @@ func (*RoomHeatChanged) ProtoMessage() {}
func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message { func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[14] mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -1094,11 +1064,9 @@ type RoomRankChanged struct {
func (x *RoomRankChanged) Reset() { func (x *RoomRankChanged) Reset() {
*x = RoomRankChanged{} *x = RoomRankChanged{}
if protoimpl.UnsafeEnabled { mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
mi := &file_proto_events_room_v1_events_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RoomRankChanged) String() string { func (x *RoomRankChanged) String() string {
@ -1109,7 +1077,7 @@ func (*RoomRankChanged) ProtoMessage() {}
func (x *RoomRankChanged) ProtoReflect() protoreflect.Message { func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[15] mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -1342,200 +1310,6 @@ func file_proto_events_room_v1_events_proto_init() {
if File_proto_events_room_v1_events_proto != nil { if File_proto_events_room_v1_events_proto != nil {
return return
} }
if !protoimpl.UnsafeEnabled {
file_proto_events_room_v1_events_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*EventEnvelope); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_events_room_v1_events_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*RoomCreated); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_events_room_v1_events_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*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[3].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[4].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[5].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[6].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[7].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[8].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[9].Exporter = func(v any, i int) any {
switch v := v.(*RoomHostTransferred); 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.(*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[11].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[12].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[13].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[14].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[15].Exporter = func(v any, i int) any {
switch v := v.(*RoomRankChanged); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{

File diff suppressed because it is too large Load Diff

View File

@ -383,6 +383,18 @@ message ListRoomsRequest {
string query = 7; string query = 7;
} }
// ListRoomFeedsRequest Mine visited/friend/following
//
message ListRoomFeedsRequest {
RequestMeta meta = 1;
int64 viewer_user_id = 2;
int64 visible_region_id = 3;
string tab = 4;
string cursor = 5;
int32 limit = 6;
string query = 7;
}
// RoomListItem Room Cell // RoomListItem Room Cell
message RoomListItem { message RoomListItem {
string room_id = 1; string room_id = 1;
@ -407,6 +419,21 @@ message ListRoomsResponse {
string next_cursor = 2; string next_cursor = 2;
} }
// GetMyRoomRequest
// gateway owner_user_id ID
message GetMyRoomRequest {
RequestMeta meta = 1;
int64 owner_user_id = 2;
}
// GetMyRoomResponse Mine
// rooms Room Cell
message GetMyRoomResponse {
bool has_room = 1;
RoomListItem room = 2;
int64 server_time_ms = 3;
}
// GetCurrentRoomRequest presence // GetCurrentRoomRequest presence
// gateway user_id user_id // gateway user_id user_id
message GetCurrentRoomRequest { message GetCurrentRoomRequest {
@ -473,6 +500,8 @@ service RoomGuardService {
// RoomQueryService Room Cell // RoomQueryService Room Cell
service RoomQueryService { service RoomQueryService {
rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse); rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse);
rpc ListRoomFeeds(ListRoomFeedsRequest) returns (ListRoomsResponse);
rpc GetMyRoom(GetMyRoomRequest) returns (GetMyRoomResponse);
rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse); rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse);
rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse); rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse);
} }

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.2 // - protoc v5.27.3
// source: proto/room/v1/room.proto // source: proto/room/v1/room.proto
package roomv1 package roomv1
@ -916,6 +916,8 @@ var RoomGuardService_ServiceDesc = grpc.ServiceDesc{
const ( const (
RoomQueryService_ListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRooms" RoomQueryService_ListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRooms"
RoomQueryService_ListRoomFeeds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomFeeds"
RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom"
RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom" RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom"
RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot" RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot"
) )
@ -927,6 +929,8 @@ const (
// RoomQueryService 承载不会改变 Room Cell 状态的读模型查询。 // RoomQueryService 承载不会改变 Room Cell 状态的读模型查询。
type RoomQueryServiceClient interface { type RoomQueryServiceClient interface {
ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error)
ListRoomFeeds(ctx context.Context, in *ListRoomFeedsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error)
GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error)
GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error) GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error)
GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error) GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error)
} }
@ -949,6 +953,26 @@ func (c *roomQueryServiceClient) ListRooms(ctx context.Context, in *ListRoomsReq
return out, nil return out, nil
} }
func (c *roomQueryServiceClient) ListRoomFeeds(ctx context.Context, in *ListRoomFeedsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRoomsResponse)
err := c.cc.Invoke(ctx, RoomQueryService_ListRoomFeeds_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomQueryServiceClient) GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetMyRoomResponse)
err := c.cc.Invoke(ctx, RoomQueryService_GetMyRoom_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomQueryServiceClient) GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error) { func (c *roomQueryServiceClient) GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetCurrentRoomResponse) out := new(GetCurrentRoomResponse)
@ -976,6 +1000,8 @@ func (c *roomQueryServiceClient) GetRoomSnapshot(ctx context.Context, in *GetRoo
// RoomQueryService 承载不会改变 Room Cell 状态的读模型查询。 // RoomQueryService 承载不会改变 Room Cell 状态的读模型查询。
type RoomQueryServiceServer interface { type RoomQueryServiceServer interface {
ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error)
ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error)
GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error)
GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error)
GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error)
mustEmbedUnimplementedRoomQueryServiceServer() mustEmbedUnimplementedRoomQueryServiceServer()
@ -991,6 +1017,12 @@ type UnimplementedRoomQueryServiceServer struct{}
func (UnimplementedRoomQueryServiceServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) { func (UnimplementedRoomQueryServiceServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRooms not implemented") return nil, status.Errorf(codes.Unimplemented, "method ListRooms not implemented")
} }
func (UnimplementedRoomQueryServiceServer) ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRoomFeeds not implemented")
}
func (UnimplementedRoomQueryServiceServer) GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMyRoom not implemented")
}
func (UnimplementedRoomQueryServiceServer) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) { func (UnimplementedRoomQueryServiceServer) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetCurrentRoom not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetCurrentRoom not implemented")
} }
@ -1036,6 +1068,42 @@ func _RoomQueryService_ListRooms_Handler(srv interface{}, ctx context.Context, d
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomQueryService_ListRoomFeeds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRoomFeedsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomQueryServiceServer).ListRoomFeeds(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomQueryService_ListRoomFeeds_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).ListRoomFeeds(ctx, req.(*ListRoomFeedsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_GetMyRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMyRoomRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomQueryServiceServer).GetMyRoom(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomQueryService_GetMyRoom_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).GetMyRoom(ctx, req.(*GetMyRoomRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_GetCurrentRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _RoomQueryService_GetCurrentRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetCurrentRoomRequest) in := new(GetCurrentRoomRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -1083,6 +1151,14 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListRooms", MethodName: "ListRooms",
Handler: _RoomQueryService_ListRooms_Handler, Handler: _RoomQueryService_ListRooms_Handler,
}, },
{
MethodName: "ListRoomFeeds",
Handler: _RoomQueryService_ListRoomFeeds_Handler,
},
{
MethodName: "GetMyRoom",
Handler: _RoomQueryService_GetMyRoom_Handler,
},
{ {
MethodName: "GetCurrentRoom", MethodName: "GetCurrentRoom",
Handler: _RoomQueryService_GetCurrentRoom_Handler, Handler: _RoomQueryService_GetCurrentRoom_Handler,

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.34.2 // protoc-gen-go v1.35.1
// protoc v5.29.2 // protoc v5.27.3
// source: proto/user/v1/auth.proto // source: proto/user/v1/auth.proto
package userv1 package userv1
@ -33,11 +33,9 @@ type LoginPasswordRequest struct {
func (x *LoginPasswordRequest) Reset() { func (x *LoginPasswordRequest) Reset() {
*x = LoginPasswordRequest{} *x = LoginPasswordRequest{}
if protoimpl.UnsafeEnabled { mi := &file_proto_user_v1_auth_proto_msgTypes[0]
mi := &file_proto_user_v1_auth_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *LoginPasswordRequest) String() string { func (x *LoginPasswordRequest) String() string {
@ -48,7 +46,7 @@ func (*LoginPasswordRequest) ProtoMessage() {}
func (x *LoginPasswordRequest) ProtoReflect() protoreflect.Message { func (x *LoginPasswordRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[0] mi := &file_proto_user_v1_auth_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -114,11 +112,9 @@ type LoginThirdPartyRequest struct {
func (x *LoginThirdPartyRequest) Reset() { func (x *LoginThirdPartyRequest) Reset() {
*x = LoginThirdPartyRequest{} *x = LoginThirdPartyRequest{}
if protoimpl.UnsafeEnabled { mi := &file_proto_user_v1_auth_proto_msgTypes[1]
mi := &file_proto_user_v1_auth_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *LoginThirdPartyRequest) String() string { func (x *LoginThirdPartyRequest) String() string {
@ -129,7 +125,7 @@ func (*LoginThirdPartyRequest) ProtoMessage() {}
func (x *LoginThirdPartyRequest) ProtoReflect() protoreflect.Message { func (x *LoginThirdPartyRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[1] mi := &file_proto_user_v1_auth_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -298,11 +294,9 @@ type AuthResponse struct {
func (x *AuthResponse) Reset() { func (x *AuthResponse) Reset() {
*x = AuthResponse{} *x = AuthResponse{}
if protoimpl.UnsafeEnabled { mi := &file_proto_user_v1_auth_proto_msgTypes[2]
mi := &file_proto_user_v1_auth_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *AuthResponse) String() string { func (x *AuthResponse) String() string {
@ -313,7 +307,7 @@ func (*AuthResponse) ProtoMessage() {}
func (x *AuthResponse) ProtoReflect() protoreflect.Message { func (x *AuthResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[2] mi := &file_proto_user_v1_auth_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -369,11 +363,9 @@ type SetPasswordRequest struct {
func (x *SetPasswordRequest) Reset() { func (x *SetPasswordRequest) Reset() {
*x = SetPasswordRequest{} *x = SetPasswordRequest{}
if protoimpl.UnsafeEnabled { mi := &file_proto_user_v1_auth_proto_msgTypes[3]
mi := &file_proto_user_v1_auth_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *SetPasswordRequest) String() string { func (x *SetPasswordRequest) String() string {
@ -384,7 +376,7 @@ func (*SetPasswordRequest) ProtoMessage() {}
func (x *SetPasswordRequest) ProtoReflect() protoreflect.Message { func (x *SetPasswordRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[3] mi := &file_proto_user_v1_auth_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -431,11 +423,9 @@ type SetPasswordResponse struct {
func (x *SetPasswordResponse) Reset() { func (x *SetPasswordResponse) Reset() {
*x = SetPasswordResponse{} *x = SetPasswordResponse{}
if protoimpl.UnsafeEnabled { mi := &file_proto_user_v1_auth_proto_msgTypes[4]
mi := &file_proto_user_v1_auth_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *SetPasswordResponse) String() string { func (x *SetPasswordResponse) String() string {
@ -446,7 +436,7 @@ func (*SetPasswordResponse) ProtoMessage() {}
func (x *SetPasswordResponse) ProtoReflect() protoreflect.Message { func (x *SetPasswordResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[4] mi := &file_proto_user_v1_auth_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -480,11 +470,9 @@ type RefreshTokenRequest struct {
func (x *RefreshTokenRequest) Reset() { func (x *RefreshTokenRequest) Reset() {
*x = RefreshTokenRequest{} *x = RefreshTokenRequest{}
if protoimpl.UnsafeEnabled { mi := &file_proto_user_v1_auth_proto_msgTypes[5]
mi := &file_proto_user_v1_auth_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RefreshTokenRequest) String() string { func (x *RefreshTokenRequest) String() string {
@ -495,7 +483,7 @@ func (*RefreshTokenRequest) ProtoMessage() {}
func (x *RefreshTokenRequest) ProtoReflect() protoreflect.Message { func (x *RefreshTokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[5] mi := &file_proto_user_v1_auth_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -535,11 +523,9 @@ type RefreshTokenResponse struct {
func (x *RefreshTokenResponse) Reset() { func (x *RefreshTokenResponse) Reset() {
*x = RefreshTokenResponse{} *x = RefreshTokenResponse{}
if protoimpl.UnsafeEnabled { mi := &file_proto_user_v1_auth_proto_msgTypes[6]
mi := &file_proto_user_v1_auth_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RefreshTokenResponse) String() string { func (x *RefreshTokenResponse) String() string {
@ -550,7 +536,7 @@ func (*RefreshTokenResponse) ProtoMessage() {}
func (x *RefreshTokenResponse) ProtoReflect() protoreflect.Message { func (x *RefreshTokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[6] mi := &file_proto_user_v1_auth_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -585,11 +571,9 @@ type LogoutRequest struct {
func (x *LogoutRequest) Reset() { func (x *LogoutRequest) Reset() {
*x = LogoutRequest{} *x = LogoutRequest{}
if protoimpl.UnsafeEnabled { mi := &file_proto_user_v1_auth_proto_msgTypes[7]
mi := &file_proto_user_v1_auth_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *LogoutRequest) String() string { func (x *LogoutRequest) String() string {
@ -600,7 +584,7 @@ func (*LogoutRequest) ProtoMessage() {}
func (x *LogoutRequest) ProtoReflect() protoreflect.Message { func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[7] mi := &file_proto_user_v1_auth_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -647,11 +631,9 @@ type LogoutResponse struct {
func (x *LogoutResponse) Reset() { func (x *LogoutResponse) Reset() {
*x = LogoutResponse{} *x = LogoutResponse{}
if protoimpl.UnsafeEnabled { mi := &file_proto_user_v1_auth_proto_msgTypes[8]
mi := &file_proto_user_v1_auth_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *LogoutResponse) String() string { func (x *LogoutResponse) String() string {
@ -662,7 +644,7 @@ func (*LogoutResponse) ProtoMessage() {}
func (x *LogoutResponse) ProtoReflect() protoreflect.Message { func (x *LogoutResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[8] mi := &file_proto_user_v1_auth_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -700,11 +682,9 @@ type RecordLoginBlockedRequest struct {
func (x *RecordLoginBlockedRequest) Reset() { func (x *RecordLoginBlockedRequest) Reset() {
*x = RecordLoginBlockedRequest{} *x = RecordLoginBlockedRequest{}
if protoimpl.UnsafeEnabled { mi := &file_proto_user_v1_auth_proto_msgTypes[9]
mi := &file_proto_user_v1_auth_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RecordLoginBlockedRequest) String() string { func (x *RecordLoginBlockedRequest) String() string {
@ -715,7 +695,7 @@ func (*RecordLoginBlockedRequest) ProtoMessage() {}
func (x *RecordLoginBlockedRequest) ProtoReflect() protoreflect.Message { func (x *RecordLoginBlockedRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[9] mi := &file_proto_user_v1_auth_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -783,11 +763,9 @@ type RecordLoginBlockedResponse struct {
func (x *RecordLoginBlockedResponse) Reset() { func (x *RecordLoginBlockedResponse) Reset() {
*x = RecordLoginBlockedResponse{} *x = RecordLoginBlockedResponse{}
if protoimpl.UnsafeEnabled { mi := &file_proto_user_v1_auth_proto_msgTypes[10]
mi := &file_proto_user_v1_auth_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *RecordLoginBlockedResponse) String() string { func (x *RecordLoginBlockedResponse) String() string {
@ -798,7 +776,7 @@ func (*RecordLoginBlockedResponse) ProtoMessage() {}
func (x *RecordLoginBlockedResponse) ProtoReflect() protoreflect.Message { func (x *RecordLoginBlockedResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[10] mi := &file_proto_user_v1_auth_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -1039,140 +1017,6 @@ func file_proto_user_v1_auth_proto_init() {
return return
} }
file_proto_user_v1_user_proto_init() file_proto_user_v1_user_proto_init()
if !protoimpl.UnsafeEnabled {
file_proto_user_v1_auth_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*LoginPasswordRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_user_v1_auth_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*LoginThirdPartyRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_user_v1_auth_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*AuthResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_user_v1_auth_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*SetPasswordRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_user_v1_auth_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*SetPasswordResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_user_v1_auth_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*RefreshTokenRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_user_v1_auth_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*RefreshTokenResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_user_v1_auth_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*LogoutRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_user_v1_auth_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*LogoutResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_user_v1_auth_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*RecordLoginBlockedRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_user_v1_auth_proto_msgTypes[10].Exporter = func(v any, i int) any {
switch v := v.(*RecordLoginBlockedResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.2 // - protoc v5.27.3
// source: proto/user/v1/auth.proto // source: proto/user/v1/auth.proto
package userv1 package userv1

File diff suppressed because it is too large Load Diff

View File

@ -56,6 +56,23 @@ message CoinSellerProfile {
int64 updated_at_ms = 6; int64 updated_at_ms = 6;
} }
// UserRoleSummary App 使
// hostAgencyBD user-service RPC gateway fanout
message UserRoleSummary {
int64 user_id = 1;
bool is_host = 2;
bool is_agency = 3;
bool is_bd = 4;
bool is_bd_leader = 5;
bool is_coin_seller = 6;
string host_status = 7;
int64 agency_id = 8;
int64 bd_id = 9;
string bd_status = 10;
string coin_seller_status = 11;
int64 pending_role_invitations = 12;
}
// AgencyMembership host Agency // AgencyMembership host Agency
message AgencyMembership { message AgencyMembership {
int64 membership_id = 1; int64 membership_id = 1;
@ -225,6 +242,15 @@ message GetCoinSellerProfileResponse {
CoinSellerProfile coin_seller_profile = 1; CoinSellerProfile coin_seller_profile = 1;
} }
message GetUserRoleSummaryRequest {
RequestMeta meta = 1;
int64 user_id = 2;
}
message GetUserRoleSummaryResponse {
UserRoleSummary summary = 1;
}
message GetAgencyMembersRequest { message GetAgencyMembersRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
int64 agency_id = 2; int64 agency_id = 2;
@ -364,6 +390,7 @@ service UserHostService {
rpc GetHostProfile(GetHostProfileRequest) returns (GetHostProfileResponse); rpc GetHostProfile(GetHostProfileRequest) returns (GetHostProfileResponse);
rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse); rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse);
rpc GetCoinSellerProfile(GetCoinSellerProfileRequest) returns (GetCoinSellerProfileResponse); rpc GetCoinSellerProfile(GetCoinSellerProfileRequest) returns (GetCoinSellerProfileResponse);
rpc GetUserRoleSummary(GetUserRoleSummaryRequest) returns (GetUserRoleSummaryResponse);
rpc GetAgencyMembers(GetAgencyMembersRequest) returns (GetAgencyMembersResponse); rpc GetAgencyMembers(GetAgencyMembersRequest) returns (GetAgencyMembersResponse);
rpc GetAgencyApplications(GetAgencyApplicationsRequest) returns (GetAgencyApplicationsResponse); rpc GetAgencyApplications(GetAgencyApplicationsRequest) returns (GetAgencyApplicationsResponse);
} }

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.2 // - protoc v5.27.3
// source: proto/user/v1/host.proto // source: proto/user/v1/host.proto
package userv1 package userv1
@ -29,6 +29,7 @@ const (
UserHostService_GetHostProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetHostProfile" UserHostService_GetHostProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetHostProfile"
UserHostService_GetBDProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetBDProfile" UserHostService_GetBDProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetBDProfile"
UserHostService_GetCoinSellerProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetCoinSellerProfile" UserHostService_GetCoinSellerProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetCoinSellerProfile"
UserHostService_GetUserRoleSummary_FullMethodName = "/hyapp.user.v1.UserHostService/GetUserRoleSummary"
UserHostService_GetAgencyMembers_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyMembers" UserHostService_GetAgencyMembers_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyMembers"
UserHostService_GetAgencyApplications_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyApplications" UserHostService_GetAgencyApplications_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyApplications"
) )
@ -49,6 +50,7 @@ type UserHostServiceClient interface {
GetHostProfile(ctx context.Context, in *GetHostProfileRequest, opts ...grpc.CallOption) (*GetHostProfileResponse, error) GetHostProfile(ctx context.Context, in *GetHostProfileRequest, opts ...grpc.CallOption) (*GetHostProfileResponse, error)
GetBDProfile(ctx context.Context, in *GetBDProfileRequest, opts ...grpc.CallOption) (*GetBDProfileResponse, error) GetBDProfile(ctx context.Context, in *GetBDProfileRequest, opts ...grpc.CallOption) (*GetBDProfileResponse, error)
GetCoinSellerProfile(ctx context.Context, in *GetCoinSellerProfileRequest, opts ...grpc.CallOption) (*GetCoinSellerProfileResponse, error) GetCoinSellerProfile(ctx context.Context, in *GetCoinSellerProfileRequest, opts ...grpc.CallOption) (*GetCoinSellerProfileResponse, error)
GetUserRoleSummary(ctx context.Context, in *GetUserRoleSummaryRequest, opts ...grpc.CallOption) (*GetUserRoleSummaryResponse, error)
GetAgencyMembers(ctx context.Context, in *GetAgencyMembersRequest, opts ...grpc.CallOption) (*GetAgencyMembersResponse, error) GetAgencyMembers(ctx context.Context, in *GetAgencyMembersRequest, opts ...grpc.CallOption) (*GetAgencyMembersResponse, error)
GetAgencyApplications(ctx context.Context, in *GetAgencyApplicationsRequest, opts ...grpc.CallOption) (*GetAgencyApplicationsResponse, error) GetAgencyApplications(ctx context.Context, in *GetAgencyApplicationsRequest, opts ...grpc.CallOption) (*GetAgencyApplicationsResponse, error)
} }
@ -161,6 +163,16 @@ func (c *userHostServiceClient) GetCoinSellerProfile(ctx context.Context, in *Ge
return out, nil return out, nil
} }
func (c *userHostServiceClient) GetUserRoleSummary(ctx context.Context, in *GetUserRoleSummaryRequest, opts ...grpc.CallOption) (*GetUserRoleSummaryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetUserRoleSummaryResponse)
err := c.cc.Invoke(ctx, UserHostService_GetUserRoleSummary_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userHostServiceClient) GetAgencyMembers(ctx context.Context, in *GetAgencyMembersRequest, opts ...grpc.CallOption) (*GetAgencyMembersResponse, error) { func (c *userHostServiceClient) GetAgencyMembers(ctx context.Context, in *GetAgencyMembersRequest, opts ...grpc.CallOption) (*GetAgencyMembersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetAgencyMembersResponse) out := new(GetAgencyMembersResponse)
@ -197,6 +209,7 @@ type UserHostServiceServer interface {
GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, error) GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, error)
GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error) GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error)
GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error) GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error)
GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error)
GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error) GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error)
GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error) GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error)
mustEmbedUnimplementedUserHostServiceServer() mustEmbedUnimplementedUserHostServiceServer()
@ -239,6 +252,9 @@ func (UnimplementedUserHostServiceServer) GetBDProfile(context.Context, *GetBDPr
func (UnimplementedUserHostServiceServer) GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error) { func (UnimplementedUserHostServiceServer) GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetCoinSellerProfile not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetCoinSellerProfile not implemented")
} }
func (UnimplementedUserHostServiceServer) GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserRoleSummary not implemented")
}
func (UnimplementedUserHostServiceServer) GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error) { func (UnimplementedUserHostServiceServer) GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyMembers not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetAgencyMembers not implemented")
} }
@ -446,6 +462,24 @@ func _UserHostService_GetCoinSellerProfile_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _UserHostService_GetUserRoleSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUserRoleSummaryRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserHostServiceServer).GetUserRoleSummary(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserHostService_GetUserRoleSummary_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserHostServiceServer).GetUserRoleSummary(ctx, req.(*GetUserRoleSummaryRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserHostService_GetAgencyMembers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _UserHostService_GetAgencyMembers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAgencyMembersRequest) in := new(GetAgencyMembersRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -529,6 +563,10 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetCoinSellerProfile", MethodName: "GetCoinSellerProfile",
Handler: _UserHostService_GetCoinSellerProfile_Handler, Handler: _UserHostService_GetCoinSellerProfile_Handler,
}, },
{
MethodName: "GetUserRoleSummary",
Handler: _UserHostService_GetUserRoleSummary_Handler,
},
{ {
MethodName: "GetAgencyMembers", MethodName: "GetAgencyMembers",
Handler: _UserHostService_GetAgencyMembers_Handler, Handler: _UserHostService_GetAgencyMembers_Handler,

File diff suppressed because it is too large Load Diff

View File

@ -171,6 +171,166 @@ message GetUserResponse {
User user = 1; User user = 1;
} }
// UserProfileStats read model
message UserProfileStats {
int64 user_id = 1;
int64 visitors_count = 2;
int64 following_count = 3;
int64 friends_count = 4;
int64 updated_at_ms = 5;
string app_code = 6;
}
// GetMyProfileStatsRequest
message GetMyProfileStatsRequest {
RequestMeta meta = 1;
int64 user_id = 2;
}
message GetMyProfileStatsResponse {
UserProfileStats stats = 1;
}
message RecordProfileVisitRequest {
RequestMeta meta = 1;
int64 visitor_user_id = 2;
int64 target_user_id = 3;
}
message RecordProfileVisitResponse {
bool recorded = 1;
UserProfileStats target_stats = 2;
}
message ProfileVisitRecord {
int64 visitor_user_id = 1;
int64 target_user_id = 2;
int64 visit_count = 3;
int64 last_visited_at_ms = 4;
}
message ListProfileVisitorsRequest {
RequestMeta meta = 1;
int64 user_id = 2;
int32 page = 3;
int32 page_size = 4;
}
message ListProfileVisitorsResponse {
repeated ProfileVisitRecord records = 1;
int64 total = 2;
}
message FollowUserRequest {
RequestMeta meta = 1;
int64 follower_user_id = 2;
int64 followee_user_id = 3;
}
message FollowUserResponse {
bool following = 1;
UserProfileStats follower_stats = 2;
}
message UnfollowUserRequest {
RequestMeta meta = 1;
int64 follower_user_id = 2;
int64 followee_user_id = 3;
}
message UnfollowUserResponse {
bool following = 1;
UserProfileStats follower_stats = 2;
}
message FollowRecord {
int64 follower_user_id = 1;
int64 followee_user_id = 2;
int64 followed_at_ms = 3;
}
message ListFollowingRequest {
RequestMeta meta = 1;
int64 user_id = 2;
int32 page = 3;
int32 page_size = 4;
}
message ListFollowingResponse {
repeated FollowRecord records = 1;
int64 total = 2;
}
message ApplyFriendRequest {
RequestMeta meta = 1;
int64 requester_user_id = 2;
int64 target_user_id = 3;
}
message ApplyFriendResponse {
FriendApplication application = 1;
bool already_friends = 2;
}
message AcceptFriendApplicationRequest {
RequestMeta meta = 1;
int64 accepter_user_id = 2;
int64 requester_user_id = 3;
}
message AcceptFriendApplicationResponse {
FriendRecord friend = 1;
}
message DeleteFriendRequest {
RequestMeta meta = 1;
int64 user_id = 2;
int64 friend_user_id = 3;
}
message DeleteFriendResponse {
bool deleted = 1;
}
message FriendRecord {
int64 user_id = 1;
int64 friend_user_id = 2;
int64 friended_at_ms = 3;
}
message ListFriendsRequest {
RequestMeta meta = 1;
int64 user_id = 2;
int32 page = 3;
int32 page_size = 4;
}
message ListFriendsResponse {
repeated FriendRecord records = 1;
int64 total = 2;
}
message FriendApplication {
int64 requester_user_id = 1;
int64 target_user_id = 2;
string status = 3;
int64 created_at_ms = 4;
int64 updated_at_ms = 5;
}
message ListFriendApplicationsRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string direction = 3;
int32 page = 4;
int32 page_size = 5;
}
message ListFriendApplicationsResponse {
repeated FriendApplication applications = 1;
int64 total = 2;
}
// BatchGetUsersRequest // BatchGetUsersRequest
message BatchGetUsersRequest { message BatchGetUsersRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
@ -461,6 +621,7 @@ message ExpirePrettyDisplayUserIDResponse {
// UserService // UserService
service UserService { service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse); rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc GetMyProfileStats(GetMyProfileStatsRequest) returns (GetMyProfileStatsResponse);
rpc BatchGetUsers(BatchGetUsersRequest) returns (BatchGetUsersResponse); rpc BatchGetUsers(BatchGetUsersRequest) returns (BatchGetUsersResponse);
rpc ListUserIDs(ListUserIDsRequest) returns (ListUserIDsResponse); rpc ListUserIDs(ListUserIDsRequest) returns (ListUserIDsResponse);
rpc GetUserMicLifetimeStats(GetUserMicLifetimeStatsRequest) returns (GetUserMicLifetimeStatsResponse); rpc GetUserMicLifetimeStats(GetUserMicLifetimeStatsRequest) returns (GetUserMicLifetimeStatsResponse);
@ -469,6 +630,20 @@ service UserService {
rpc CompleteOnboarding(CompleteOnboardingRequest) returns (CompleteOnboardingResponse); rpc CompleteOnboarding(CompleteOnboardingRequest) returns (CompleteOnboardingResponse);
} }
// UserSocialService 访
service UserSocialService {
rpc RecordProfileVisit(RecordProfileVisitRequest) returns (RecordProfileVisitResponse);
rpc ListProfileVisitors(ListProfileVisitorsRequest) returns (ListProfileVisitorsResponse);
rpc FollowUser(FollowUserRequest) returns (FollowUserResponse);
rpc UnfollowUser(UnfollowUserRequest) returns (UnfollowUserResponse);
rpc ListFollowing(ListFollowingRequest) returns (ListFollowingResponse);
rpc ApplyFriend(ApplyFriendRequest) returns (ApplyFriendResponse);
rpc AcceptFriendApplication(AcceptFriendApplicationRequest) returns (AcceptFriendApplicationResponse);
rpc DeleteFriend(DeleteFriendRequest) returns (DeleteFriendResponse);
rpc ListFriends(ListFriendsRequest) returns (ListFriendsResponse);
rpc ListFriendApplications(ListFriendApplicationsRequest) returns (ListFriendApplicationsResponse);
}
// UserCronService cron-service user-service owner // UserCronService cron-service user-service owner
service UserCronService { service UserCronService {
rpc ProcessLoginIPRiskBatch(CronBatchRequest) returns (CronBatchResponse); rpc ProcessLoginIPRiskBatch(CronBatchRequest) returns (CronBatchResponse);

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.2 // - protoc v5.27.3
// source: proto/user/v1/user.proto // source: proto/user/v1/user.proto
package userv1 package userv1
@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion9
const ( const (
UserService_GetUser_FullMethodName = "/hyapp.user.v1.UserService/GetUser" UserService_GetUser_FullMethodName = "/hyapp.user.v1.UserService/GetUser"
UserService_GetMyProfileStats_FullMethodName = "/hyapp.user.v1.UserService/GetMyProfileStats"
UserService_BatchGetUsers_FullMethodName = "/hyapp.user.v1.UserService/BatchGetUsers" UserService_BatchGetUsers_FullMethodName = "/hyapp.user.v1.UserService/BatchGetUsers"
UserService_ListUserIDs_FullMethodName = "/hyapp.user.v1.UserService/ListUserIDs" UserService_ListUserIDs_FullMethodName = "/hyapp.user.v1.UserService/ListUserIDs"
UserService_GetUserMicLifetimeStats_FullMethodName = "/hyapp.user.v1.UserService/GetUserMicLifetimeStats" UserService_GetUserMicLifetimeStats_FullMethodName = "/hyapp.user.v1.UserService/GetUserMicLifetimeStats"
@ -35,6 +36,7 @@ const (
// UserService 提供用户主状态、基础资料和注册资料读写接口。 // UserService 提供用户主状态、基础资料和注册资料读写接口。
type UserServiceClient interface { type UserServiceClient interface {
GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error)
GetMyProfileStats(ctx context.Context, in *GetMyProfileStatsRequest, opts ...grpc.CallOption) (*GetMyProfileStatsResponse, error)
BatchGetUsers(ctx context.Context, in *BatchGetUsersRequest, opts ...grpc.CallOption) (*BatchGetUsersResponse, error) BatchGetUsers(ctx context.Context, in *BatchGetUsersRequest, opts ...grpc.CallOption) (*BatchGetUsersResponse, error)
ListUserIDs(ctx context.Context, in *ListUserIDsRequest, opts ...grpc.CallOption) (*ListUserIDsResponse, error) ListUserIDs(ctx context.Context, in *ListUserIDsRequest, opts ...grpc.CallOption) (*ListUserIDsResponse, error)
GetUserMicLifetimeStats(ctx context.Context, in *GetUserMicLifetimeStatsRequest, opts ...grpc.CallOption) (*GetUserMicLifetimeStatsResponse, error) GetUserMicLifetimeStats(ctx context.Context, in *GetUserMicLifetimeStatsRequest, opts ...grpc.CallOption) (*GetUserMicLifetimeStatsResponse, error)
@ -61,6 +63,16 @@ func (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opt
return out, nil return out, nil
} }
func (c *userServiceClient) GetMyProfileStats(ctx context.Context, in *GetMyProfileStatsRequest, opts ...grpc.CallOption) (*GetMyProfileStatsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetMyProfileStatsResponse)
err := c.cc.Invoke(ctx, UserService_GetMyProfileStats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) BatchGetUsers(ctx context.Context, in *BatchGetUsersRequest, opts ...grpc.CallOption) (*BatchGetUsersResponse, error) { func (c *userServiceClient) BatchGetUsers(ctx context.Context, in *BatchGetUsersRequest, opts ...grpc.CallOption) (*BatchGetUsersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(BatchGetUsersResponse) out := new(BatchGetUsersResponse)
@ -128,6 +140,7 @@ func (c *userServiceClient) CompleteOnboarding(ctx context.Context, in *Complete
// UserService 提供用户主状态、基础资料和注册资料读写接口。 // UserService 提供用户主状态、基础资料和注册资料读写接口。
type UserServiceServer interface { type UserServiceServer interface {
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
GetMyProfileStats(context.Context, *GetMyProfileStatsRequest) (*GetMyProfileStatsResponse, error)
BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersResponse, error) BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersResponse, error)
ListUserIDs(context.Context, *ListUserIDsRequest) (*ListUserIDsResponse, error) ListUserIDs(context.Context, *ListUserIDsRequest) (*ListUserIDsResponse, error)
GetUserMicLifetimeStats(context.Context, *GetUserMicLifetimeStatsRequest) (*GetUserMicLifetimeStatsResponse, error) GetUserMicLifetimeStats(context.Context, *GetUserMicLifetimeStatsRequest) (*GetUserMicLifetimeStatsResponse, error)
@ -147,6 +160,9 @@ type UnimplementedUserServiceServer struct{}
func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) { func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented")
} }
func (UnimplementedUserServiceServer) GetMyProfileStats(context.Context, *GetMyProfileStatsRequest) (*GetMyProfileStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMyProfileStats not implemented")
}
func (UnimplementedUserServiceServer) BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersResponse, error) { func (UnimplementedUserServiceServer) BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BatchGetUsers not implemented") return nil, status.Errorf(codes.Unimplemented, "method BatchGetUsers not implemented")
} }
@ -204,6 +220,24 @@ func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _UserService_GetMyProfileStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMyProfileStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).GetMyProfileStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_GetMyProfileStats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).GetMyProfileStats(ctx, req.(*GetMyProfileStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_BatchGetUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _UserService_BatchGetUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BatchGetUsersRequest) in := new(BatchGetUsersRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -323,6 +357,10 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetUser", MethodName: "GetUser",
Handler: _UserService_GetUser_Handler, Handler: _UserService_GetUser_Handler,
}, },
{
MethodName: "GetMyProfileStats",
Handler: _UserService_GetMyProfileStats_Handler,
},
{ {
MethodName: "BatchGetUsers", MethodName: "BatchGetUsers",
Handler: _UserService_BatchGetUsers_Handler, Handler: _UserService_BatchGetUsers_Handler,
@ -352,6 +390,454 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
Metadata: "proto/user/v1/user.proto", Metadata: "proto/user/v1/user.proto",
} }
const (
UserSocialService_RecordProfileVisit_FullMethodName = "/hyapp.user.v1.UserSocialService/RecordProfileVisit"
UserSocialService_ListProfileVisitors_FullMethodName = "/hyapp.user.v1.UserSocialService/ListProfileVisitors"
UserSocialService_FollowUser_FullMethodName = "/hyapp.user.v1.UserSocialService/FollowUser"
UserSocialService_UnfollowUser_FullMethodName = "/hyapp.user.v1.UserSocialService/UnfollowUser"
UserSocialService_ListFollowing_FullMethodName = "/hyapp.user.v1.UserSocialService/ListFollowing"
UserSocialService_ApplyFriend_FullMethodName = "/hyapp.user.v1.UserSocialService/ApplyFriend"
UserSocialService_AcceptFriendApplication_FullMethodName = "/hyapp.user.v1.UserSocialService/AcceptFriendApplication"
UserSocialService_DeleteFriend_FullMethodName = "/hyapp.user.v1.UserSocialService/DeleteFriend"
UserSocialService_ListFriends_FullMethodName = "/hyapp.user.v1.UserSocialService/ListFriends"
UserSocialService_ListFriendApplications_FullMethodName = "/hyapp.user.v1.UserSocialService/ListFriendApplications"
)
// UserSocialServiceClient is the client API for UserSocialService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// UserSocialService 承载用户访问、关注和好友关系。
type UserSocialServiceClient interface {
RecordProfileVisit(ctx context.Context, in *RecordProfileVisitRequest, opts ...grpc.CallOption) (*RecordProfileVisitResponse, error)
ListProfileVisitors(ctx context.Context, in *ListProfileVisitorsRequest, opts ...grpc.CallOption) (*ListProfileVisitorsResponse, error)
FollowUser(ctx context.Context, in *FollowUserRequest, opts ...grpc.CallOption) (*FollowUserResponse, error)
UnfollowUser(ctx context.Context, in *UnfollowUserRequest, opts ...grpc.CallOption) (*UnfollowUserResponse, error)
ListFollowing(ctx context.Context, in *ListFollowingRequest, opts ...grpc.CallOption) (*ListFollowingResponse, error)
ApplyFriend(ctx context.Context, in *ApplyFriendRequest, opts ...grpc.CallOption) (*ApplyFriendResponse, error)
AcceptFriendApplication(ctx context.Context, in *AcceptFriendApplicationRequest, opts ...grpc.CallOption) (*AcceptFriendApplicationResponse, error)
DeleteFriend(ctx context.Context, in *DeleteFriendRequest, opts ...grpc.CallOption) (*DeleteFriendResponse, error)
ListFriends(ctx context.Context, in *ListFriendsRequest, opts ...grpc.CallOption) (*ListFriendsResponse, error)
ListFriendApplications(ctx context.Context, in *ListFriendApplicationsRequest, opts ...grpc.CallOption) (*ListFriendApplicationsResponse, error)
}
type userSocialServiceClient struct {
cc grpc.ClientConnInterface
}
func NewUserSocialServiceClient(cc grpc.ClientConnInterface) UserSocialServiceClient {
return &userSocialServiceClient{cc}
}
func (c *userSocialServiceClient) RecordProfileVisit(ctx context.Context, in *RecordProfileVisitRequest, opts ...grpc.CallOption) (*RecordProfileVisitResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RecordProfileVisitResponse)
err := c.cc.Invoke(ctx, UserSocialService_RecordProfileVisit_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userSocialServiceClient) ListProfileVisitors(ctx context.Context, in *ListProfileVisitorsRequest, opts ...grpc.CallOption) (*ListProfileVisitorsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListProfileVisitorsResponse)
err := c.cc.Invoke(ctx, UserSocialService_ListProfileVisitors_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userSocialServiceClient) FollowUser(ctx context.Context, in *FollowUserRequest, opts ...grpc.CallOption) (*FollowUserResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(FollowUserResponse)
err := c.cc.Invoke(ctx, UserSocialService_FollowUser_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userSocialServiceClient) UnfollowUser(ctx context.Context, in *UnfollowUserRequest, opts ...grpc.CallOption) (*UnfollowUserResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UnfollowUserResponse)
err := c.cc.Invoke(ctx, UserSocialService_UnfollowUser_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userSocialServiceClient) ListFollowing(ctx context.Context, in *ListFollowingRequest, opts ...grpc.CallOption) (*ListFollowingResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListFollowingResponse)
err := c.cc.Invoke(ctx, UserSocialService_ListFollowing_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userSocialServiceClient) ApplyFriend(ctx context.Context, in *ApplyFriendRequest, opts ...grpc.CallOption) (*ApplyFriendResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ApplyFriendResponse)
err := c.cc.Invoke(ctx, UserSocialService_ApplyFriend_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userSocialServiceClient) AcceptFriendApplication(ctx context.Context, in *AcceptFriendApplicationRequest, opts ...grpc.CallOption) (*AcceptFriendApplicationResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AcceptFriendApplicationResponse)
err := c.cc.Invoke(ctx, UserSocialService_AcceptFriendApplication_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userSocialServiceClient) DeleteFriend(ctx context.Context, in *DeleteFriendRequest, opts ...grpc.CallOption) (*DeleteFriendResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeleteFriendResponse)
err := c.cc.Invoke(ctx, UserSocialService_DeleteFriend_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userSocialServiceClient) ListFriends(ctx context.Context, in *ListFriendsRequest, opts ...grpc.CallOption) (*ListFriendsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListFriendsResponse)
err := c.cc.Invoke(ctx, UserSocialService_ListFriends_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userSocialServiceClient) ListFriendApplications(ctx context.Context, in *ListFriendApplicationsRequest, opts ...grpc.CallOption) (*ListFriendApplicationsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListFriendApplicationsResponse)
err := c.cc.Invoke(ctx, UserSocialService_ListFriendApplications_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// UserSocialServiceServer is the server API for UserSocialService service.
// All implementations must embed UnimplementedUserSocialServiceServer
// for forward compatibility.
//
// UserSocialService 承载用户访问、关注和好友关系。
type UserSocialServiceServer interface {
RecordProfileVisit(context.Context, *RecordProfileVisitRequest) (*RecordProfileVisitResponse, error)
ListProfileVisitors(context.Context, *ListProfileVisitorsRequest) (*ListProfileVisitorsResponse, error)
FollowUser(context.Context, *FollowUserRequest) (*FollowUserResponse, error)
UnfollowUser(context.Context, *UnfollowUserRequest) (*UnfollowUserResponse, error)
ListFollowing(context.Context, *ListFollowingRequest) (*ListFollowingResponse, error)
ApplyFriend(context.Context, *ApplyFriendRequest) (*ApplyFriendResponse, error)
AcceptFriendApplication(context.Context, *AcceptFriendApplicationRequest) (*AcceptFriendApplicationResponse, error)
DeleteFriend(context.Context, *DeleteFriendRequest) (*DeleteFriendResponse, error)
ListFriends(context.Context, *ListFriendsRequest) (*ListFriendsResponse, error)
ListFriendApplications(context.Context, *ListFriendApplicationsRequest) (*ListFriendApplicationsResponse, error)
mustEmbedUnimplementedUserSocialServiceServer()
}
// UnimplementedUserSocialServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedUserSocialServiceServer struct{}
func (UnimplementedUserSocialServiceServer) RecordProfileVisit(context.Context, *RecordProfileVisitRequest) (*RecordProfileVisitResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RecordProfileVisit not implemented")
}
func (UnimplementedUserSocialServiceServer) ListProfileVisitors(context.Context, *ListProfileVisitorsRequest) (*ListProfileVisitorsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListProfileVisitors not implemented")
}
func (UnimplementedUserSocialServiceServer) FollowUser(context.Context, *FollowUserRequest) (*FollowUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method FollowUser not implemented")
}
func (UnimplementedUserSocialServiceServer) UnfollowUser(context.Context, *UnfollowUserRequest) (*UnfollowUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UnfollowUser not implemented")
}
func (UnimplementedUserSocialServiceServer) ListFollowing(context.Context, *ListFollowingRequest) (*ListFollowingResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListFollowing not implemented")
}
func (UnimplementedUserSocialServiceServer) ApplyFriend(context.Context, *ApplyFriendRequest) (*ApplyFriendResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ApplyFriend not implemented")
}
func (UnimplementedUserSocialServiceServer) AcceptFriendApplication(context.Context, *AcceptFriendApplicationRequest) (*AcceptFriendApplicationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AcceptFriendApplication not implemented")
}
func (UnimplementedUserSocialServiceServer) DeleteFriend(context.Context, *DeleteFriendRequest) (*DeleteFriendResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteFriend not implemented")
}
func (UnimplementedUserSocialServiceServer) ListFriends(context.Context, *ListFriendsRequest) (*ListFriendsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListFriends not implemented")
}
func (UnimplementedUserSocialServiceServer) ListFriendApplications(context.Context, *ListFriendApplicationsRequest) (*ListFriendApplicationsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListFriendApplications not implemented")
}
func (UnimplementedUserSocialServiceServer) mustEmbedUnimplementedUserSocialServiceServer() {}
func (UnimplementedUserSocialServiceServer) testEmbeddedByValue() {}
// UnsafeUserSocialServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to UserSocialServiceServer will
// result in compilation errors.
type UnsafeUserSocialServiceServer interface {
mustEmbedUnimplementedUserSocialServiceServer()
}
func RegisterUserSocialServiceServer(s grpc.ServiceRegistrar, srv UserSocialServiceServer) {
// If the following call pancis, it indicates UnimplementedUserSocialServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&UserSocialService_ServiceDesc, srv)
}
func _UserSocialService_RecordProfileVisit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RecordProfileVisitRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserSocialServiceServer).RecordProfileVisit(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserSocialService_RecordProfileVisit_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserSocialServiceServer).RecordProfileVisit(ctx, req.(*RecordProfileVisitRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserSocialService_ListProfileVisitors_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListProfileVisitorsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserSocialServiceServer).ListProfileVisitors(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserSocialService_ListProfileVisitors_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserSocialServiceServer).ListProfileVisitors(ctx, req.(*ListProfileVisitorsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserSocialService_FollowUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(FollowUserRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserSocialServiceServer).FollowUser(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserSocialService_FollowUser_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserSocialServiceServer).FollowUser(ctx, req.(*FollowUserRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserSocialService_UnfollowUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UnfollowUserRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserSocialServiceServer).UnfollowUser(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserSocialService_UnfollowUser_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserSocialServiceServer).UnfollowUser(ctx, req.(*UnfollowUserRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserSocialService_ListFollowing_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListFollowingRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserSocialServiceServer).ListFollowing(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserSocialService_ListFollowing_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserSocialServiceServer).ListFollowing(ctx, req.(*ListFollowingRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserSocialService_ApplyFriend_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ApplyFriendRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserSocialServiceServer).ApplyFriend(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserSocialService_ApplyFriend_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserSocialServiceServer).ApplyFriend(ctx, req.(*ApplyFriendRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserSocialService_AcceptFriendApplication_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AcceptFriendApplicationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserSocialServiceServer).AcceptFriendApplication(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserSocialService_AcceptFriendApplication_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserSocialServiceServer).AcceptFriendApplication(ctx, req.(*AcceptFriendApplicationRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserSocialService_DeleteFriend_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteFriendRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserSocialServiceServer).DeleteFriend(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserSocialService_DeleteFriend_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserSocialServiceServer).DeleteFriend(ctx, req.(*DeleteFriendRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserSocialService_ListFriends_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListFriendsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserSocialServiceServer).ListFriends(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserSocialService_ListFriends_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserSocialServiceServer).ListFriends(ctx, req.(*ListFriendsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserSocialService_ListFriendApplications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListFriendApplicationsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserSocialServiceServer).ListFriendApplications(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserSocialService_ListFriendApplications_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserSocialServiceServer).ListFriendApplications(ctx, req.(*ListFriendApplicationsRequest))
}
return interceptor(ctx, in, info, handler)
}
// UserSocialService_ServiceDesc is the grpc.ServiceDesc for UserSocialService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var UserSocialService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "hyapp.user.v1.UserSocialService",
HandlerType: (*UserSocialServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "RecordProfileVisit",
Handler: _UserSocialService_RecordProfileVisit_Handler,
},
{
MethodName: "ListProfileVisitors",
Handler: _UserSocialService_ListProfileVisitors_Handler,
},
{
MethodName: "FollowUser",
Handler: _UserSocialService_FollowUser_Handler,
},
{
MethodName: "UnfollowUser",
Handler: _UserSocialService_UnfollowUser_Handler,
},
{
MethodName: "ListFollowing",
Handler: _UserSocialService_ListFollowing_Handler,
},
{
MethodName: "ApplyFriend",
Handler: _UserSocialService_ApplyFriend_Handler,
},
{
MethodName: "AcceptFriendApplication",
Handler: _UserSocialService_AcceptFriendApplication_Handler,
},
{
MethodName: "DeleteFriend",
Handler: _UserSocialService_DeleteFriend_Handler,
},
{
MethodName: "ListFriends",
Handler: _UserSocialService_ListFriends_Handler,
},
{
MethodName: "ListFriendApplications",
Handler: _UserSocialService_ListFriendApplications_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/user/v1/user.proto",
}
const ( const (
UserCronService_ProcessLoginIPRiskBatch_FullMethodName = "/hyapp.user.v1.UserCronService/ProcessLoginIPRiskBatch" UserCronService_ProcessLoginIPRiskBatch_FullMethodName = "/hyapp.user.v1.UserCronService/ProcessLoginIPRiskBatch"
UserCronService_ProcessRegionRebuildBatch_FullMethodName = "/hyapp.user.v1.UserCronService/ProcessRegionRebuildBatch" UserCronService_ProcessRegionRebuildBatch_FullMethodName = "/hyapp.user.v1.UserCronService/ProcessRegionRebuildBatch"

File diff suppressed because it is too large Load Diff

View File

@ -580,6 +580,207 @@ message ListRechargeBillsResponse {
int64 total = 2; int64 total = 2;
} }
// WalletFeatureFlags 使 wallet-service
message WalletFeatureFlags {
bool recharge_enabled = 1;
bool diamond_exchange_enabled = 2;
bool withdraw_enabled = 3;
}
message GetWalletOverviewRequest {
string request_id = 1;
string app_code = 2;
int64 user_id = 3;
}
message GetWalletOverviewResponse {
repeated AssetBalance balances = 1;
WalletFeatureFlags feature_flags = 2;
}
// WalletValueSummary
message WalletValueSummary {
int64 coin_amount = 1;
int64 updated_at_ms = 2;
}
message GetWalletValueSummaryRequest {
string request_id = 1;
string app_code = 2;
int64 user_id = 3;
}
message GetWalletValueSummaryResponse {
WalletValueSummary summary = 1;
}
// RechargeProduct App
message RechargeProduct {
int64 product_id = 1;
string product_code = 2;
string channel = 3;
string currency_code = 4;
int64 coin_amount = 5;
int64 amount_minor = 6;
string policy_version = 7;
bool enabled = 8;
int32 sort_order = 9;
}
message ListRechargeProductsRequest {
string request_id = 1;
string app_code = 2;
int64 user_id = 3;
int64 region_id = 4;
}
message ListRechargeProductsResponse {
repeated RechargeProduct products = 1;
repeated string channels = 2;
}
message DiamondExchangeRule {
string exchange_type = 1;
string from_asset_type = 2;
string to_asset_type = 3;
int64 from_amount = 4;
int64 to_amount = 5;
bool enabled = 6;
}
message GetDiamondExchangeConfigRequest {
string request_id = 1;
string app_code = 2;
int64 user_id = 3;
}
message GetDiamondExchangeConfigResponse {
repeated DiamondExchangeRule rules = 1;
}
// WalletTransaction 使
message WalletTransaction {
int64 entry_id = 1;
string transaction_id = 2;
string biz_type = 3;
string asset_type = 4;
int64 available_delta = 5;
int64 frozen_delta = 6;
int64 available_after = 7;
int64 frozen_after = 8;
int64 counterparty_user_id = 9;
string room_id = 10;
int64 created_at_ms = 11;
}
message ListWalletTransactionsRequest {
string request_id = 1;
string app_code = 2;
int64 user_id = 3;
string asset_type = 4;
int32 page = 5;
int32 page_size = 6;
}
message ListWalletTransactionsResponse {
repeated WalletTransaction transactions = 1;
int64 total = 2;
}
message WithdrawalRequest {
string withdrawal_id = 1;
int64 user_id = 2;
string asset_type = 3;
int64 amount = 4;
string status = 5;
string payout_account = 6;
string reason = 7;
int64 created_at_ms = 8;
int64 updated_at_ms = 9;
}
message ApplyWithdrawalRequest {
string command_id = 1;
string app_code = 2;
int64 user_id = 3;
int64 amount = 4;
string payout_account = 5;
string reason = 6;
}
message ApplyWithdrawalResponse {
WithdrawalRequest withdrawal = 1;
AssetBalance balance = 2;
}
message VipRewardItem {
int64 resource_id = 1;
string resource_code = 2;
string resource_type = 3;
string name = 4;
int64 quantity = 5;
int64 expires_at_ms = 6;
}
message VipLevel {
int32 level = 1;
string name = 2;
string status = 3;
int64 price_coin = 4;
int64 duration_ms = 5;
int64 reward_resource_group_id = 6;
repeated VipRewardItem reward_items = 7;
bool can_purchase = 8;
int32 sort_order = 9;
}
message UserVip {
int64 user_id = 1;
int32 level = 2;
string name = 3;
bool active = 4;
int64 started_at_ms = 5;
int64 expires_at_ms = 6;
int64 updated_at_ms = 7;
}
message ListVipPackagesRequest {
string request_id = 1;
string app_code = 2;
int64 user_id = 3;
}
message ListVipPackagesResponse {
UserVip current_vip = 1;
repeated VipLevel packages = 2;
}
message GetMyVipRequest {
string request_id = 1;
string app_code = 2;
int64 user_id = 3;
}
message GetMyVipResponse {
UserVip vip = 1;
}
message PurchaseVipRequest {
string command_id = 1;
string app_code = 2;
int64 user_id = 3;
int32 level = 4;
}
message PurchaseVipResponse {
string order_id = 1;
string transaction_id = 2;
UserVip vip = 3;
int64 coin_spent = 4;
int64 coin_balance_after = 5;
repeated VipRewardItem reward_items = 6;
}
// CreditTaskRewardRequest activity-service // CreditTaskRewardRequest activity-service
message CreditTaskRewardRequest { message CreditTaskRewardRequest {
string command_id = 1; string command_id = 1;
@ -627,5 +828,14 @@ service WalletService {
rpc EquipUserResource(EquipUserResourceRequest) returns (EquipUserResourceResponse); rpc EquipUserResource(EquipUserResourceRequest) returns (EquipUserResourceResponse);
rpc ListResourceGrants(ListResourceGrantsRequest) returns (ListResourceGrantsResponse); rpc ListResourceGrants(ListResourceGrantsRequest) returns (ListResourceGrantsResponse);
rpc ListRechargeBills(ListRechargeBillsRequest) returns (ListRechargeBillsResponse); rpc ListRechargeBills(ListRechargeBillsRequest) returns (ListRechargeBillsResponse);
rpc GetWalletOverview(GetWalletOverviewRequest) returns (GetWalletOverviewResponse);
rpc GetWalletValueSummary(GetWalletValueSummaryRequest) returns (GetWalletValueSummaryResponse);
rpc ListRechargeProducts(ListRechargeProductsRequest) returns (ListRechargeProductsResponse);
rpc GetDiamondExchangeConfig(GetDiamondExchangeConfigRequest) returns (GetDiamondExchangeConfigResponse);
rpc ListWalletTransactions(ListWalletTransactionsRequest) returns (ListWalletTransactionsResponse);
rpc ApplyWithdrawal(ApplyWithdrawalRequest) returns (ApplyWithdrawalResponse);
rpc ListVipPackages(ListVipPackagesRequest) returns (ListVipPackagesResponse);
rpc GetMyVip(GetMyVipRequest) returns (GetMyVipResponse);
rpc PurchaseVip(PurchaseVipRequest) returns (PurchaseVipResponse);
rpc CreditTaskReward(CreditTaskRewardRequest) returns (CreditTaskRewardResponse); rpc CreditTaskReward(CreditTaskRewardRequest) returns (CreditTaskRewardResponse);
} }

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.5.1 // - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.2 // - protoc v5.27.3
// source: proto/wallet/v1/wallet.proto // source: proto/wallet/v1/wallet.proto
package walletv1 package walletv1
@ -44,6 +44,15 @@ const (
WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource" WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource"
WalletService_ListResourceGrants_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGrants" WalletService_ListResourceGrants_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGrants"
WalletService_ListRechargeBills_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeBills" WalletService_ListRechargeBills_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeBills"
WalletService_GetWalletOverview_FullMethodName = "/hyapp.wallet.v1.WalletService/GetWalletOverview"
WalletService_GetWalletValueSummary_FullMethodName = "/hyapp.wallet.v1.WalletService/GetWalletValueSummary"
WalletService_ListRechargeProducts_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeProducts"
WalletService_GetDiamondExchangeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetDiamondExchangeConfig"
WalletService_ListWalletTransactions_FullMethodName = "/hyapp.wallet.v1.WalletService/ListWalletTransactions"
WalletService_ApplyWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/ApplyWithdrawal"
WalletService_ListVipPackages_FullMethodName = "/hyapp.wallet.v1.WalletService/ListVipPackages"
WalletService_GetMyVip_FullMethodName = "/hyapp.wallet.v1.WalletService/GetMyVip"
WalletService_PurchaseVip_FullMethodName = "/hyapp.wallet.v1.WalletService/PurchaseVip"
WalletService_CreditTaskReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditTaskReward" WalletService_CreditTaskReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditTaskReward"
) )
@ -78,6 +87,15 @@ type WalletServiceClient interface {
EquipUserResource(ctx context.Context, in *EquipUserResourceRequest, opts ...grpc.CallOption) (*EquipUserResourceResponse, error) EquipUserResource(ctx context.Context, in *EquipUserResourceRequest, opts ...grpc.CallOption) (*EquipUserResourceResponse, error)
ListResourceGrants(ctx context.Context, in *ListResourceGrantsRequest, opts ...grpc.CallOption) (*ListResourceGrantsResponse, error) ListResourceGrants(ctx context.Context, in *ListResourceGrantsRequest, opts ...grpc.CallOption) (*ListResourceGrantsResponse, error)
ListRechargeBills(ctx context.Context, in *ListRechargeBillsRequest, opts ...grpc.CallOption) (*ListRechargeBillsResponse, error) ListRechargeBills(ctx context.Context, in *ListRechargeBillsRequest, opts ...grpc.CallOption) (*ListRechargeBillsResponse, error)
GetWalletOverview(ctx context.Context, in *GetWalletOverviewRequest, opts ...grpc.CallOption) (*GetWalletOverviewResponse, error)
GetWalletValueSummary(ctx context.Context, in *GetWalletValueSummaryRequest, opts ...grpc.CallOption) (*GetWalletValueSummaryResponse, error)
ListRechargeProducts(ctx context.Context, in *ListRechargeProductsRequest, opts ...grpc.CallOption) (*ListRechargeProductsResponse, error)
GetDiamondExchangeConfig(ctx context.Context, in *GetDiamondExchangeConfigRequest, opts ...grpc.CallOption) (*GetDiamondExchangeConfigResponse, error)
ListWalletTransactions(ctx context.Context, in *ListWalletTransactionsRequest, opts ...grpc.CallOption) (*ListWalletTransactionsResponse, error)
ApplyWithdrawal(ctx context.Context, in *ApplyWithdrawalRequest, opts ...grpc.CallOption) (*ApplyWithdrawalResponse, error)
ListVipPackages(ctx context.Context, in *ListVipPackagesRequest, opts ...grpc.CallOption) (*ListVipPackagesResponse, error)
GetMyVip(ctx context.Context, in *GetMyVipRequest, opts ...grpc.CallOption) (*GetMyVipResponse, error)
PurchaseVip(ctx context.Context, in *PurchaseVipRequest, opts ...grpc.CallOption) (*PurchaseVipResponse, error)
CreditTaskReward(ctx context.Context, in *CreditTaskRewardRequest, opts ...grpc.CallOption) (*CreditTaskRewardResponse, error) CreditTaskReward(ctx context.Context, in *CreditTaskRewardRequest, opts ...grpc.CallOption) (*CreditTaskRewardResponse, error)
} }
@ -339,6 +357,96 @@ func (c *walletServiceClient) ListRechargeBills(ctx context.Context, in *ListRec
return out, nil return out, nil
} }
func (c *walletServiceClient) GetWalletOverview(ctx context.Context, in *GetWalletOverviewRequest, opts ...grpc.CallOption) (*GetWalletOverviewResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetWalletOverviewResponse)
err := c.cc.Invoke(ctx, WalletService_GetWalletOverview_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) GetWalletValueSummary(ctx context.Context, in *GetWalletValueSummaryRequest, opts ...grpc.CallOption) (*GetWalletValueSummaryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetWalletValueSummaryResponse)
err := c.cc.Invoke(ctx, WalletService_GetWalletValueSummary_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) ListRechargeProducts(ctx context.Context, in *ListRechargeProductsRequest, opts ...grpc.CallOption) (*ListRechargeProductsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRechargeProductsResponse)
err := c.cc.Invoke(ctx, WalletService_ListRechargeProducts_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) GetDiamondExchangeConfig(ctx context.Context, in *GetDiamondExchangeConfigRequest, opts ...grpc.CallOption) (*GetDiamondExchangeConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetDiamondExchangeConfigResponse)
err := c.cc.Invoke(ctx, WalletService_GetDiamondExchangeConfig_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) ListWalletTransactions(ctx context.Context, in *ListWalletTransactionsRequest, opts ...grpc.CallOption) (*ListWalletTransactionsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListWalletTransactionsResponse)
err := c.cc.Invoke(ctx, WalletService_ListWalletTransactions_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) ApplyWithdrawal(ctx context.Context, in *ApplyWithdrawalRequest, opts ...grpc.CallOption) (*ApplyWithdrawalResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ApplyWithdrawalResponse)
err := c.cc.Invoke(ctx, WalletService_ApplyWithdrawal_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) ListVipPackages(ctx context.Context, in *ListVipPackagesRequest, opts ...grpc.CallOption) (*ListVipPackagesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListVipPackagesResponse)
err := c.cc.Invoke(ctx, WalletService_ListVipPackages_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) GetMyVip(ctx context.Context, in *GetMyVipRequest, opts ...grpc.CallOption) (*GetMyVipResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetMyVipResponse)
err := c.cc.Invoke(ctx, WalletService_GetMyVip_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) PurchaseVip(ctx context.Context, in *PurchaseVipRequest, opts ...grpc.CallOption) (*PurchaseVipResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(PurchaseVipResponse)
err := c.cc.Invoke(ctx, WalletService_PurchaseVip_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) CreditTaskReward(ctx context.Context, in *CreditTaskRewardRequest, opts ...grpc.CallOption) (*CreditTaskRewardResponse, error) { func (c *walletServiceClient) CreditTaskReward(ctx context.Context, in *CreditTaskRewardRequest, opts ...grpc.CallOption) (*CreditTaskRewardResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreditTaskRewardResponse) out := new(CreditTaskRewardResponse)
@ -380,6 +488,15 @@ type WalletServiceServer interface {
EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error) EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error)
ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, error) ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, error)
ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error) ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error)
GetWalletOverview(context.Context, *GetWalletOverviewRequest) (*GetWalletOverviewResponse, error)
GetWalletValueSummary(context.Context, *GetWalletValueSummaryRequest) (*GetWalletValueSummaryResponse, error)
ListRechargeProducts(context.Context, *ListRechargeProductsRequest) (*ListRechargeProductsResponse, error)
GetDiamondExchangeConfig(context.Context, *GetDiamondExchangeConfigRequest) (*GetDiamondExchangeConfigResponse, error)
ListWalletTransactions(context.Context, *ListWalletTransactionsRequest) (*ListWalletTransactionsResponse, error)
ApplyWithdrawal(context.Context, *ApplyWithdrawalRequest) (*ApplyWithdrawalResponse, error)
ListVipPackages(context.Context, *ListVipPackagesRequest) (*ListVipPackagesResponse, error)
GetMyVip(context.Context, *GetMyVipRequest) (*GetMyVipResponse, error)
PurchaseVip(context.Context, *PurchaseVipRequest) (*PurchaseVipResponse, error)
CreditTaskReward(context.Context, *CreditTaskRewardRequest) (*CreditTaskRewardResponse, error) CreditTaskReward(context.Context, *CreditTaskRewardRequest) (*CreditTaskRewardResponse, error)
mustEmbedUnimplementedWalletServiceServer() mustEmbedUnimplementedWalletServiceServer()
} }
@ -466,6 +583,33 @@ func (UnimplementedWalletServiceServer) ListResourceGrants(context.Context, *Lis
func (UnimplementedWalletServiceServer) ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error) { func (UnimplementedWalletServiceServer) ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRechargeBills not implemented") return nil, status.Errorf(codes.Unimplemented, "method ListRechargeBills not implemented")
} }
func (UnimplementedWalletServiceServer) GetWalletOverview(context.Context, *GetWalletOverviewRequest) (*GetWalletOverviewResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetWalletOverview not implemented")
}
func (UnimplementedWalletServiceServer) GetWalletValueSummary(context.Context, *GetWalletValueSummaryRequest) (*GetWalletValueSummaryResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetWalletValueSummary not implemented")
}
func (UnimplementedWalletServiceServer) ListRechargeProducts(context.Context, *ListRechargeProductsRequest) (*ListRechargeProductsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRechargeProducts not implemented")
}
func (UnimplementedWalletServiceServer) GetDiamondExchangeConfig(context.Context, *GetDiamondExchangeConfigRequest) (*GetDiamondExchangeConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDiamondExchangeConfig not implemented")
}
func (UnimplementedWalletServiceServer) ListWalletTransactions(context.Context, *ListWalletTransactionsRequest) (*ListWalletTransactionsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListWalletTransactions not implemented")
}
func (UnimplementedWalletServiceServer) ApplyWithdrawal(context.Context, *ApplyWithdrawalRequest) (*ApplyWithdrawalResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ApplyWithdrawal not implemented")
}
func (UnimplementedWalletServiceServer) ListVipPackages(context.Context, *ListVipPackagesRequest) (*ListVipPackagesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListVipPackages not implemented")
}
func (UnimplementedWalletServiceServer) GetMyVip(context.Context, *GetMyVipRequest) (*GetMyVipResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMyVip not implemented")
}
func (UnimplementedWalletServiceServer) PurchaseVip(context.Context, *PurchaseVipRequest) (*PurchaseVipResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method PurchaseVip not implemented")
}
func (UnimplementedWalletServiceServer) CreditTaskReward(context.Context, *CreditTaskRewardRequest) (*CreditTaskRewardResponse, error) { func (UnimplementedWalletServiceServer) CreditTaskReward(context.Context, *CreditTaskRewardRequest) (*CreditTaskRewardResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreditTaskReward not implemented") return nil, status.Errorf(codes.Unimplemented, "method CreditTaskReward not implemented")
} }
@ -940,6 +1084,168 @@ func _WalletService_ListRechargeBills_Handler(srv interface{}, ctx context.Conte
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _WalletService_GetWalletOverview_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetWalletOverviewRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).GetWalletOverview(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_GetWalletOverview_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).GetWalletOverview(ctx, req.(*GetWalletOverviewRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_GetWalletValueSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetWalletValueSummaryRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).GetWalletValueSummary(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_GetWalletValueSummary_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).GetWalletValueSummary(ctx, req.(*GetWalletValueSummaryRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_ListRechargeProducts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRechargeProductsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).ListRechargeProducts(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_ListRechargeProducts_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).ListRechargeProducts(ctx, req.(*ListRechargeProductsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_GetDiamondExchangeConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetDiamondExchangeConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).GetDiamondExchangeConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_GetDiamondExchangeConfig_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).GetDiamondExchangeConfig(ctx, req.(*GetDiamondExchangeConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_ListWalletTransactions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListWalletTransactionsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).ListWalletTransactions(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_ListWalletTransactions_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).ListWalletTransactions(ctx, req.(*ListWalletTransactionsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_ApplyWithdrawal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ApplyWithdrawalRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).ApplyWithdrawal(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_ApplyWithdrawal_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).ApplyWithdrawal(ctx, req.(*ApplyWithdrawalRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_ListVipPackages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListVipPackagesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).ListVipPackages(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_ListVipPackages_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).ListVipPackages(ctx, req.(*ListVipPackagesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_GetMyVip_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMyVipRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).GetMyVip(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_GetMyVip_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).GetMyVip(ctx, req.(*GetMyVipRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_PurchaseVip_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PurchaseVipRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).PurchaseVip(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_PurchaseVip_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).PurchaseVip(ctx, req.(*PurchaseVipRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_CreditTaskReward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _WalletService_CreditTaskReward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreditTaskRewardRequest) in := new(CreditTaskRewardRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -1065,6 +1371,42 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListRechargeBills", MethodName: "ListRechargeBills",
Handler: _WalletService_ListRechargeBills_Handler, Handler: _WalletService_ListRechargeBills_Handler,
}, },
{
MethodName: "GetWalletOverview",
Handler: _WalletService_GetWalletOverview_Handler,
},
{
MethodName: "GetWalletValueSummary",
Handler: _WalletService_GetWalletValueSummary_Handler,
},
{
MethodName: "ListRechargeProducts",
Handler: _WalletService_ListRechargeProducts_Handler,
},
{
MethodName: "GetDiamondExchangeConfig",
Handler: _WalletService_GetDiamondExchangeConfig_Handler,
},
{
MethodName: "ListWalletTransactions",
Handler: _WalletService_ListWalletTransactions_Handler,
},
{
MethodName: "ApplyWithdrawal",
Handler: _WalletService_ApplyWithdrawal_Handler,
},
{
MethodName: "ListVipPackages",
Handler: _WalletService_ListVipPackages_Handler,
},
{
MethodName: "GetMyVip",
Handler: _WalletService_GetMyVip_Handler,
},
{
MethodName: "PurchaseVip",
Handler: _WalletService_PurchaseVip_Handler,
},
{ {
MethodName: "CreditTaskReward", MethodName: "CreditTaskReward",
Handler: _WalletService_CreditTaskReward_Handler, Handler: _WalletService_CreditTaskReward_Handler,

View File

@ -1,6 +1,6 @@
# App Entry State Machine # App Entry State Machine
本文定义用户从打开 App 到进入主界面的完整状态机。它只描述 App 入口流程、接口调用顺序、跳转条件和异常恢复边界;登录注册细节见 [Login And Registration Technical Design](./auth-login-register-technical-design.md),邀请码细节见 [Registration Invite Code Architecture](./registration-invite-code-architecture.md),房间重连细节见 [Room Entry Reconnect Development Requirements](./room-entry-reconnect-development-requirements.md)。 本文定义用户从打开 App 到进入主界面的完整状态机。它只描述 App 入口流程、接口调用顺序、跳转条件和异常恢复边界;登录注册细节见 [Login And Registration Technical Design](./登录注册技术设计.md),邀请码细节见 [Registration Invite Code Architecture](./注册邀请码架构.md),房间重连细节见 [Room Entry Reconnect Development Requirements](./进房与重连开发需求.md)。
## Goals ## Goals

View File

@ -5,10 +5,10 @@
相关文档: 相关文档:
```text ```text
docs/voice-room-basic-loop-implementation.md docs/语音房基础闭环实现.md
docs/voice-room-client-api-flow.md docs/语音房客户端接口流程.md
docs/app-message-tab-architecture.md docs/App消息Tab架构.md
docs/room-outbox-compensation-development.md docs/房间Outbox补偿开发.md
``` ```
## Current State ## Current State

View File

@ -1,316 +0,0 @@
# My Page Overview API
本文定义 App 底部 `我的` 页首屏聚合接口。该接口只返回首屏渲染需要的轻量摘要:用户资料、我的邀请码、核心资产余额、身份开关、消息红点和入口显隐。流水、成员列表、背包完整列表、工资明细、邀请列表和团队统计必须放到二级页接口。
## Goals
- `我的` 页首屏一次请求拿到稳定摘要,减少 App 冷启动后的接口数量。
- 聚合已有轻量能力:`/users/me``/wallet/me/balances``/users/me/host-identity``/messages/tabs`
- 避免实时 `COUNT(*)`、扫流水、扫成员、扫背包、现算工资。
- 后端控制入口显隐App 不硬编码复杂身份组合。
- 所有数据按 `app_code` 和当前 JWT `user_id` 隔离。
## Endpoint
```http
GET /api/v1/users/me/overview
Authorization: Bearer <access_token>
```
响应仍使用 gateway `/api/v1` envelope
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_01",
"data": {
"profile": {},
"wallet": {},
"invite": {},
"roles": {},
"badges": {},
"entries": [],
"server_time_ms": 1777996800000
}
}
```
## Response Shape
```json
{
"profile": {
"user_id": "10001",
"display_user_id": "163000",
"default_display_user_id": "163000",
"display_user_id_kind": "default",
"display_user_id_expires_at_ms": 0,
"username": "Alice",
"avatar": "https://cdn.example.com/avatar.png",
"gender": "female",
"birth": "2000-01-01",
"country": "US",
"country_id": 1,
"country_name": "United States",
"country_display_name": "United States",
"region_id": 10,
"region_code": "north_america",
"region_name": "North America",
"language": "en",
"profile_completed": true,
"onboarding_status": "completed"
},
"wallet": {
"balances": [
{
"asset_type": "COIN",
"available_amount": 12000,
"frozen_amount": 0,
"version": 8
},
{
"asset_type": "DIAMOND",
"available_amount": 300,
"frozen_amount": 0,
"version": 3
},
{
"asset_type": "USD_BALANCE",
"available_amount": 2500,
"frozen_amount": 500,
"version": 2
}
],
"recharge_enabled": true,
"diamond_exchange_enabled": true,
"withdraw_enabled": true
},
"invite": {
"my_invite_code": "A1B2C3",
"invite_enabled": true,
"invite_count": 12,
"valid_invite_count": 3,
"valid_invite_threshold_coin": 80000
},
"roles": {
"is_host": true,
"is_agent": false,
"is_bd": false,
"is_bd_leader": false,
"is_coin_seller": false
},
"badges": {
"unread_system_messages": 2,
"unread_activity_messages": 1,
"pending_role_invitations": 0
},
"entries": [
{
"key": "wallet",
"title": "Wallet",
"visible": true,
"badge_count": 0
},
{
"key": "host_center",
"title": "Host Center",
"visible": true,
"badge_count": 0
},
{
"key": "coin_seller_center",
"title": "Coin Seller",
"visible": false,
"badge_count": 0
},
{
"key": "backpack",
"title": "Backpack",
"visible": true,
"badge_count": 0
},
{
"key": "settings",
"title": "Settings",
"visible": true,
"badge_count": 0
}
],
"server_time_ms": 1777996800000
}
```
## Field Sources
| Field | Source | Query Pattern |
| --- | --- | --- |
| `profile` | `user-service.GetUser` | single row by `(app_code, user_id)` |
| `wallet.balances` | `wallet-service.GetBalances` | point read for `COIN``DIAMOND``USD_BALANCE` |
| `invite.my_invite_code` | `user-service` invite code module | point read active primary code by `(app_code, user_id)` |
| `invite.invite_count` | invite read model | users that successfully filled this invite code; without read model return `0` or omit |
| `invite.valid_invite_count` | invite read model | invited users whose cumulative eligible recharge reaches the configured threshold |
| `invite.valid_invite_threshold_coin` | invite validity policy | current configured threshold, default `80000` `COIN` |
| `roles` | `user-service` host identity module | point read role facts or future role summary read model |
| `badges.unread_*` | message inbox owner | counter/read model, not message list scan |
| `entries` | gateway composition rules | computed from `roles` and feature flags |
| `server_time_ms` | gateway | current server clock |
当前实现已有 `/users/me``/wallet/me/balances``/users/me/host-identity``/messages/tabs``/users/me/overview` 可以先在 `gateway-service` 聚合这些能力;后续如果角色判断的三次 not-found RPC 变慢,再在 `user-service` 增加 `GetUserRoleSummary` 内部 RPC。
## Asset Rules
首屏只返回三类资产:
| Asset | Show Rule |
| --- | --- |
| `COIN` | 所有用户展示 |
| `DIAMOND` | 所有用户展示,但文案不能暗示可直接消费 |
| `USD_BALANCE` | 有余额、冻结金额、host/agent/bd/coin_seller 身份时展示 |
不要返回 `GIFT_POINT`。积分是后台工资/活动计算输入,不是普通用户钱包可直接操作资产。
## Entry Rules
入口显隐由后端返回App 只负责渲染。
| Entry Key | Visible Rule |
| --- | --- |
| `wallet` | always true |
| `host_center` | `roles.is_host == true` |
| `apply_host` | `roles.is_host == false` |
| `agency_center` | `roles.is_agent == true` |
| `bd_center` | `roles.is_bd == true` |
| `bd_leader_center` | `roles.is_bd_leader == true` |
| `coin_seller_center` | `roles.is_coin_seller == true` |
| `backpack` | always true |
| `settings` | always true |
| `support` | always true |
同一个用户可能同时是 BD 和 host。不要用互斥枚举表达角色必须使用 bool flags。
## Expensive Fields To Exclude
不要在 `overview` 中实时查询这些字段:
| Field | Reason | Replacement |
| --- | --- | --- |
| 邀请用户列表 | 需要分页,可能扫 `user_invite_relations` | 二级页 `GET /api/v1/invites/users` |
| 实时邀请人数 `COUNT(*)` | 高活跃邀请人会让首页变慢 | read model counter 或返回 `0` |
| 邀请奖励流水 | 属于钱包/活动明细 | 二级页分页 |
这些字段不要放进首屏 overview
| Field | Reason | Replacement |
| --- | --- | --- |
| `period_mic_minutes` | 需要统计聚合,可能扫 stats | Host Center 二级页 |
| `period_gift_points` | 需要统计聚合,可能跨日汇总 | Host Center 二级页 |
| `estimated_earning_usd_minor` | 不能首屏现算工资 | 后台工资 read model 或收益页 |
| `agency_member_count` | 容易 `COUNT(*)` members | Agency Center 二级页或 summary table |
| `bd_host_count` | 团队规模统计昂贵 | BD Center 二级页或 summary table |
| `pending_withdrawals` | 扫提现/流水表 | wallet badge counter |
| `recharge_count` | 扫充值记录 | wallet 二级页 |
| `resource_count` | 扫背包权益表 | backpack 二级页 |
| `latest_transactions` | 扫交易流水 | wallet 二级页 |
| `inbox_items` | 扫消息列表 | messages 二级页 |
首屏接口禁止为了红点实时扫大表。需要红点就维护 counter/read model没有 counter 时先不展示该红点。
## Performance Budget
目标:
- P95 backend latency: `< 120ms` in normal region deployment.
- Gateway fanout RPC count: max 4.
- No list scan.
- No transaction history query.
- No member `COUNT(*)`.
- No resource entitlement list.
- No salary calculation.
推荐并行 fanout
```mermaid
sequenceDiagram
participant C as Client
participant G as gateway-service
participant U as user-service
participant W as wallet-service
participant M as message inbox owner
C->>G: GET /api/v1/users/me/overview
par profile and roles
G->>U: GetUser(user_id)
G->>U: GetUserRoleSummary(user_id)
and wallet
G->>W: GetBalances(COIN, DIAMOND, USD_BALANCE)
and badges
G->>M: ListMessageTabs(user_id)
end
G->>G: compose entries and feature flags
G-->>C: overview
```
MVP 可以复用已有 `GetHostProfile``GetBDProfile``GetCoinSellerProfile` 聚合角色;优化版应增加单个 `GetUserRoleSummary`,减少 not-found RPC 和错误映射成本。
## Degraded Responses
首屏接口可以部分降级,但必须清楚标记。
| Upstream Failure | Response Rule |
| --- | --- |
| `user-service.GetUser` fail | 整体失败,资料是首屏主数据 |
| `wallet-service.GetBalances` fail | 返回空 balances并加 `wallet.unavailable=true` |
| role summary fail | 返回 all false并加 `roles.unavailable=true`,不要误展示特权入口 |
| message tabs fail | unread 置 0并加 `badges.unavailable=true` |
如果某个上游超时gateway 不应无限等待。overview 是首屏接口,应该有比普通操作接口更短的聚合超时预算。
## HTTP Handler Shape
新增路由:
```go
mux.Handle(apiV1Prefix+"/users/me/overview", h.profileAPIHandler(jwtVerifier, h.getMyOverview))
```
handler 只做协议聚合:
1. 从 JWT context 取 `user_id``app_code`
2. 并行请求 profile、balances、roles、message tabs。
3. 把资产补齐为固定三类,缺失资产返回 0。
4. 根据 roles 和 feature flags 生成 entries。
5. 返回 envelope data。
不要在 gateway 写 SQL也不要在 gateway 做工资、成员、资源统计。
## Verification Matrix
| Scenario | Expected |
| --- | --- |
| 普通用户打开我的页 | 返回 profile、COIN/DIAMOND/USD_BALANCE、普通入口 |
| host 打开我的页 | `roles.is_host=true`,展示 `host_center` |
| BD Leader 打开我的页 | `is_bd=true``is_bd_leader=true`,展示 leader 入口 |
| 币商打开我的页 | `is_coin_seller=true`,展示 coin seller 入口 |
| 钱包没有账户行 | 三类资产都返回 0 投影 |
| 消息服务失败 | 我的页仍可展示,消息红点降级为 0 |
| 用户资料服务失败 | overview 整体失败 |
| 大量成员/流水存在 | overview 延迟不受影响,因为不查成员和流水 |
## Implementation Order
1. 新增本文档,确认字段边界和慢查询排除规则。
2. 在 `gateway-service` 增加 `GET /api/v1/users/me/overview` handler。
3. 复用现有 `GetUser``GetBalances``GetHostProfile/GetBDProfile/GetCoinSellerProfile``ListMessageTabs`
4. 增加 handler 单元测试覆盖普通用户、host、BD Leader、币商、钱包缺失、消息降级。
5. 如果角色 RPC 成本偏高,再在 `user-service` 增加 `GetUserRoleSummary`
6. 如果红点需要更多类型,先补 counter/read model再把 badge 加进 overview。
## Critical Rules
- `我的` 页 overview 是轻聚合接口,不是用户全量详情接口。
- 首屏不做 `COUNT(*)`、不扫流水、不扫成员、不查完整背包、不现算工资。
- 钱包余额以 `wallet-service` 为准。
- 用户资料以 `user-service` 为准。
- 角色入口以服务端返回的 `roles``entries` 为准。
- 消息红点以 message inbox counter/read model 为准,用户私聊未读仍以腾讯云 IM SDK 为准。

View File

@ -714,7 +714,7 @@ paths:
- users - users
summary: 我的页首屏聚合摘要 summary: 我的页首屏聚合摘要
operationId: getMyOverview operationId: getMyOverview
description: 聚合当前用户资料、固定三类钱包余额、Host/Agency/BD/币商身份、消息红点和入口显隐。该接口不查流水、不扫成员、不计算工资;钱包、角色和消息上游失败时按字段 `unavailable=true` 局部降级。 description: 聚合当前用户资料、顶部计数、金币钱包卡片、VIP、Host/Agency/BD/币商身份和入口显隐。该接口不查消息红点、邀请码、钻石、美元余额、流水、成员或工资钱包、VIP、计数和角色上游失败时按字段 `unavailable=true` 局部降级。
security: security:
- BearerAuth: [] - BearerAuth: []
parameters: parameters:
@ -911,7 +911,6 @@ paths:
enum: enum:
- hot - hot
- new - new
- me
- name: cursor - name: cursor
in: query in: query
required: false required: false
@ -943,6 +942,69 @@ paths:
$ref: "#/responses/Forbidden" $ref: "#/responses/Forbidden"
"502": "502":
$ref: "#/responses/UpstreamError" $ref: "#/responses/UpstreamError"
/api/v1/rooms/feeds:
get:
tags:
- rooms
summary: Mine 房间关系流
operationId: listRoomFeeds
description: 查询 Mine 页下方 Visited/Friend/Following 房间流;该接口读取当前用户的房间 feed 索引,再返回房间卡片和 `im_group_id`,不作为公共房间发现列表。
security:
- BearerAuth: []
parameters:
- $ref: "#/parameters/RequestIDHeader"
- name: tab
in: query
required: true
type: string
enum:
- visited
- friend
- following
- name: cursor
in: query
required: false
type: string
- name: limit
in: query
required: false
type: integer
format: int32
responses:
"200":
description: 查询成功;`visited` 由 JoinRoom 成功写入,`friend/following` 依赖关系流读模型填充。
schema:
$ref: "#/definitions/RoomListEnvelope"
"400":
$ref: "#/responses/BadRequest"
"401":
$ref: "#/responses/Unauthorized"
"403":
$ref: "#/responses/Forbidden"
"502":
$ref: "#/responses/UpstreamError"
/api/v1/rooms/me:
get:
tags:
- rooms
summary: 查询我的房间卡片
operationId: getMyRoom
description: Mine 顶部“我的房间”权威查询gateway 使用 access token user_idroom-service 直接读取 rooms 元数据并用 Room Cell 快照补齐卡片字段,不依赖发现页 `room_list_entries` 投影命中。
security:
- BearerAuth: []
parameters:
- $ref: "#/parameters/RequestIDHeader"
responses:
"200":
description: 查询成功,`data.has_room=false` 表示当前用户尚未创建房间;有房间时 `data.room.im_group_id` 显式返回腾讯 IM 房间群 ID。
schema:
$ref: "#/definitions/MyRoomEnvelope"
"401":
$ref: "#/responses/Unauthorized"
"403":
$ref: "#/responses/Forbidden"
"502":
$ref: "#/responses/UpstreamError"
/api/v1/rooms/current: /api/v1/rooms/current:
get: get:
tags: tags:
@ -2421,14 +2483,15 @@ definitions:
type: object type: object
required: required:
- is_host - is_host
- is_agent - is_agency
- is_bd - is_bd
- is_bd_leader - is_bd_leader
- is_coin_seller - is_coin_seller
- pending_role_invitations
properties: properties:
is_host: is_host:
type: boolean type: boolean
is_agent: is_agency:
type: boolean type: boolean
is_bd: is_bd:
type: boolean type: boolean
@ -2436,147 +2499,143 @@ definitions:
type: boolean type: boolean
is_coin_seller: is_coin_seller:
type: boolean type: boolean
unavailable: host_status:
type: boolean
description: 角色上游不可用时为 true此时所有特权身份按 false 返回。
MyOverviewAssetBalanceData:
type: object
required:
- asset_type
- available_amount
- frozen_amount
- version
properties:
asset_type:
type: string type: string
enum: agency_id:
- COIN
- DIAMOND
- USD_BALANCE
available_amount:
type: integer type: integer
format: int64 format: int64
frozen_amount: bd_id:
type: integer type: integer
format: int64 format: int64
version: bd_status:
type: integer type: string
format: int64 coin_seller_status:
MyOverviewWalletData:
type: object
required:
- balances
- recharge_enabled
- diamond_exchange_enabled
- withdraw_enabled
properties:
balances:
type: array
description: 固定只返回 COIN、DIAMOND、USD_BALANCE钱包账户行缺失时投影为 0不返回 GIFT_POINT。
items:
$ref: "#/definitions/MyOverviewAssetBalanceData"
recharge_enabled:
type: boolean
diamond_exchange_enabled:
type: boolean
withdraw_enabled:
type: boolean
unavailable:
type: boolean
description: 钱包上游不可用时为 true此时 balances 为空。
MyOverviewInviteData:
type: object
properties:
my_invite_code:
type: string type: string
description: 当前用户自己的 active primary 邀请码。
invite_enabled:
type: boolean
description: 当前用户的邀请码是否可用于新用户注册归因。
invite_count:
type: integer
format: int64
description: 邀请人数,表示成功填写当前用户邀请码并绑定关系的人数;没有 read model 时返回 0 或不返回,禁止首页实时 COUNT。
valid_invite_count:
type: integer
format: int64
description: 有效邀请人数,表示被邀请用户累计符合条件充值达到后台配置门槛的人数。
valid_invite_threshold_coin:
type: integer
format: int64
description: 当前有效邀请门槛,默认 80000 COIN可由后台配置。
unavailable:
type: boolean
description: 邀请码上游不可用时为 true客户端可隐藏复制入口。
MyOverviewBadgesData:
type: object
required:
- unread_system_messages
- unread_activity_messages
- pending_role_invitations
properties:
unread_system_messages:
type: integer
format: int64
unread_activity_messages:
type: integer
format: int64
pending_role_invitations: pending_role_invitations:
type: integer type: integer
format: int64 format: int64
description: 当前没有独立 counter 时返回 0不扫邀请列表。
unavailable: unavailable:
type: boolean type: boolean
description: 消息上游不可用时为 true未读数按 0 返回。 description: 角色上游不可用时为 true此时所有特权身份按 false 返回。
MyOverviewProfileStatsData:
type: object
required:
- visitors_count
- following_count
- friends_count
- updated_at_ms
properties:
visitors_count:
type: integer
format: int64
following_count:
type: integer
format: int64
friends_count:
type: integer
format: int64
updated_at_ms:
type: integer
format: int64
unavailable:
type: boolean
description: 计数 read model 上游不可用时为 true客户端可按 0 展示。
MyOverviewWalletData:
type: object
required:
- coin_amount
- updated_at_ms
properties:
coin_amount:
type: integer
format: int64
description: 我的页首屏只展示金币余额。
updated_at_ms:
type: integer
format: int64
unavailable:
type: boolean
description: 钱包上游不可用时为 true。
MyOverviewVIPData:
type: object
properties:
level:
type: integer
format: int32
name:
type: string
active:
type: boolean
started_at_ms:
type: integer
format: int64
expires_at_ms:
type: integer
format: int64
unavailable:
type: boolean
description: VIP 上游不可用时为 true。
MyOverviewEntryData: MyOverviewEntryData:
type: object type: object
required: required:
- key - key
- title
- visible - visible
- enabled
- route
- icon_key
- badge_count - badge_count
- order
properties: properties:
key: key:
type: string type: string
enum: enum:
- wallet - store
- host_center - bag
- level
- task
- apply_host - apply_host
- host_center
- agency_center - agency_center
- bd_center - bd_center
- bd_leader_center - bd_leader_center
- coin_seller_center - coin_seller_center
- backpack
- settings - settings
- support - language
title:
type: string
visible: visible:
type: boolean type: boolean
enabled:
type: boolean
route:
type: string
icon_key:
type: string
badge_count: badge_count:
type: integer type: integer
format: int64 format: int64
order:
type: integer
format: int32
MyOverviewData: MyOverviewData:
type: object type: object
required: required:
- profile - profile
- profile_stats
- wallet - wallet
- invite - vip
- roles - roles
- badges
- entries - entries
- server_time_ms - server_time_ms
properties: properties:
profile: profile:
$ref: "#/definitions/UserProfileData" $ref: "#/definitions/UserProfileData"
profile_stats:
$ref: "#/definitions/MyOverviewProfileStatsData"
wallet: wallet:
$ref: "#/definitions/MyOverviewWalletData" $ref: "#/definitions/MyOverviewWalletData"
invite: vip:
$ref: "#/definitions/MyOverviewInviteData" $ref: "#/definitions/MyOverviewVIPData"
roles: roles:
$ref: "#/definitions/MyOverviewRolesData" $ref: "#/definitions/MyOverviewRolesData"
badges:
$ref: "#/definitions/MyOverviewBadgesData"
entries: entries:
type: array type: array
items: items:
@ -3697,6 +3756,19 @@ definitions:
$ref: "#/definitions/RoomListItemData" $ref: "#/definitions/RoomListItemData"
next_cursor: next_cursor:
type: string type: string
MyRoomData:
type: object
required:
- has_room
- server_time_ms
properties:
has_room:
type: boolean
room:
$ref: "#/definitions/RoomListItemData"
server_time_ms:
type: integer
format: int64
RoomInitialData: RoomInitialData:
type: object type: object
required: required:
@ -4248,6 +4320,13 @@ definitions:
properties: properties:
data: data:
$ref: "#/definitions/RoomListData" $ref: "#/definitions/RoomListData"
MyRoomEnvelope:
allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase"
- type: object
properties:
data:
$ref: "#/definitions/MyRoomData"
CurrentRoomEnvelope: CurrentRoomEnvelope:
allOf: allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase" - $ref: "#/definitions/GatewayOKEnvelopeBase"

View File

@ -1,6 +1,6 @@
# Host, Agency And BD Admin Architecture # Host, Agency And BD Admin Architecture
本文只定义后台/Admin 侧实现政策配置、工资周期、工资单、审核、钱包入账、调整单、后台审计和运营干预。App 侧的主播申请、Agency 成员、BD 邀请和用户可见查询见 [Host Agency BD App Architecture](./host-agency-bd-architecture.md)。 本文只定义后台/Admin 侧实现政策配置、工资周期、工资单、审核、钱包入账、调整单、后台审计和运营干预。App 侧的主播申请、Agency 成员、BD 邀请和用户可见查询见 [Host Agency BD App Architecture](./主播公会BD架构.md)。
这里的工资统一指发放到主播侧 `USD_BALANCE` 的业务奖励。提现审核和人工打款仍由 `wallet-service` 拥有Admin 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。 这里的工资统一指发放到主播侧 `USD_BALANCE` 的业务奖励。提现审核和人工打款仍由 `wallet-service` 拥有Admin 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。
@ -284,7 +284,7 @@ stateDiagram-v2
### Host Salary ### Host Salary
输入来自 App 文档定义的 `host_daily_stats`。基础麦上在线事实先按所有用户沉淀,详见 [User Mic Online Time Architecture](./user-mic-online-time-architecture.md)`host_daily_stats` 是工资需要的关系快照派生表。 输入来自 App 文档定义的 `host_daily_stats`。基础麦上在线事实先按所有用户沉淀,详见 [User Mic Online Time Architecture](./用户麦上在线时长架构.md)`host_daily_stats` 是工资需要的关系快照派生表。
- Sum `mic_online_ms` within cycle。 - Sum `mic_online_ms` within cycle。
- Sum `gift_point_received` within cycle。 - Sum `gift_point_received` within cycle。
@ -433,7 +433,7 @@ service UserHostAdminService {
``` ```
`CreateBDLeaderRequest` 必须携带 `region_id`。user-service 校验该区域 active锁定目标用户行必要时更新 `users.region_id`,再创建 `bd_profiles`;同一个 `command_id` 重试只返回已创建的 BD Leader 事实,不重复迁移。 `CreateBDLeaderRequest` 必须携带 `region_id`。user-service 校验该区域 active锁定目标用户行必要时更新 `users.region_id`,再创建 `bd_profiles`;同一个 `command_id` 重试只返回已创建的 BD Leader 事实,不重复迁移。
这属于允许改变用户区域的后台关系管理路径之一;完整来源见 [User Region Change Sources](./user-region-country-development.md#user-region-change-sources)。 这属于允许改变用户区域的后台关系管理路径之一;完整来源见 [User Region Change Sources](./用户区域与国家开发.md#user-region-change-sources)。
当前项目仍在开发阶段proto 可以按当前事实调整;修改 `api/proto` 后必须运行 `make proto` 并提交生成文件。 当前项目仍在开发阶段proto 可以按当前事实调整;修改 `api/proto` 后必须运行 `make proto` 并提交生成文件。
@ -466,7 +466,7 @@ Outbox consumers can update App message inbox, BI, risk systems, and export jobs
## App Message Notifications ## App Message Notifications
后台关系和工资动作需要通知 App 用户时,统一进入 App `消息` tab 的 `system` 分区,具体 message inbox 设计见 [App Message Tab Architecture](./app-message-tab-architecture.md)。`hyapp-admin-server` 只负责后台权限和审计App 消息事实由 message inbox owner 落库。 后台关系和工资动作需要通知 App 用户时,统一进入 App `消息` tab 的 `system` 分区,具体 message inbox 设计见 [App Message Tab Architecture](./App消息Tab架构.md)。`hyapp-admin-server` 只负责后台权限和审计App 消息事实由 message inbox owner 落库。
| Admin/Salary Fact | Target User | Message Type | Action | | Admin/Salary Fact | Target User | Message Type | Action |
| --- | --- | --- | --- | | --- | --- | --- | --- |
@ -495,7 +495,7 @@ Outbox consumers can update App message inbox, BI, risk systems, and export jobs
## Implementation Order ## Implementation Order
整体开发顺序单独维护在 [Host Agency BD Implementation Sequence](./host-agency-bd-implementation-sequence.md)。Admin 文档只保留后台政策、工资、入账、调整和权限审计设计,避免和 App 实施顺序混在一起。 整体开发顺序单独维护在 [Host Agency BD Implementation Sequence](./主播公会BD实施顺序.md)。Admin 文档只保留后台政策、工资、入账、调整和权限审计设计,避免和 App 实施顺序混在一起。
## Verification Matrix ## Verification Matrix

View File

@ -1,6 +1,6 @@
# Host, Agency And BD Implementation Sequence # Host, Agency And BD Implementation Sequence
本文定义主播、Agency、BD、BD Leader 功能的开发顺序。详细 App 架构见 [Host Agency BD App Architecture](./host-agency-bd-architecture.md),后台工资和政策架构见 [Host Agency BD Admin Architecture](./host-agency-bd-admin-architecture.md)。 本文定义主播、Agency、BD、BD Leader 功能的开发顺序。详细 App 架构见 [Host Agency BD App Architecture](./主播公会BD架构.md),后台工资和政策架构见 [Host Agency BD Admin Architecture](./主播公会BD后台架构.md)。
核心原则:先做关系事实,再做 App 主链路,最后做统计和工资。工资依赖 host、Agency、BD 关系快照和有效上麦/礼物统计;如果先做工资后台,会得到一套没有可靠输入的数据系统。 核心原则:先做关系事实,再做 App 主链路,最后做统计和工资。工资依赖 host、Agency、BD 关系快照和有效上麦/礼物统计;如果先做工资后台,会得到一套没有可靠输入的数据系统。
@ -94,7 +94,7 @@
交付: 交付:
- host outbox 事件接入 message inbox owner具体接口和表见 [App Message Tab Architecture](./app-message-tab-architecture.md)。 - host outbox 事件接入 message inbox owner具体接口和表见 [App Message Tab Architecture](./App消息Tab架构.md)。
- Agency 申请提交后通知 Agency owner 或授权审核者。 - Agency 申请提交后通知 Agency owner 或授权审核者。
- 申请通过、拒绝、被踢出 Agency 时通知目标用户。 - 申请通过、拒绝、被踢出 Agency 时通知目标用户。
- BD/BD Leader 邀请 Agency 或 BD 时通知被邀请人。 - BD/BD Leader 邀请 Agency 或 BD 时通知被邀请人。
@ -111,7 +111,7 @@
## Phase 4: Stats And Attribution ## Phase 4: Stats And Attribution
关系主链路稳定后再消费房间和钱包事件。麦上在线时长先按所有用户统一沉淀,详见 [User Mic Online Time Architecture](./user-mic-online-time-architecture.md)。host stats 是派生读模型;工资和团队统计必须使用派生时的关系快照,不能从当前 membership 反推历史归属。 关系主链路稳定后再消费房间和钱包事件。麦上在线时长先按所有用户统一沉淀,详见 [User Mic Online Time Architecture](./用户麦上在线时长架构.md)。host stats 是派生读模型;工资和团队统计必须使用派生时的关系快照,不能从当前 membership 反推历史归属。
交付: 交付:

View File

@ -2,7 +2,7 @@
本文只定义 App 侧的主播组织、关系和用户可操作流程。这里的 `host` 指业务主播,不等同于 `room-service` 里的房间 `host_user_id`。房间 `host_user_id` 是单个房间的主持/管理身份;本文的主播身份是用户长期经营身份,可以有 Agency 归属和收益展示。 本文只定义 App 侧的主播组织、关系和用户可操作流程。这里的 `host` 指业务主播,不等同于 `room-service` 里的房间 `host_user_id`。房间 `host_user_id` 是单个房间的主持/管理身份;本文的主播身份是用户长期经营身份,可以有 Agency 归属和收益展示。
后台政策、工资周期、工资单审批、钱包入账和调整单实现单独放在 [Host Agency BD Admin Architecture](./host-agency-bd-admin-architecture.md)。App 侧可以读取收益和结算状态,但不能配置政策、触发结算或直接入账。 后台政策、工资周期、工资单审批、钱包入账和调整单实现单独放在 [Host Agency BD Admin Architecture](./主播公会BD后台架构.md)。App 侧可以读取收益和结算状态,但不能配置政策、触发结算或直接入账。
旧 Java 项目只作为术语和历史口径参考。新架构按当前 Go 服务边界重新设计:不新增独立 `host-service` 微服务主播、Agency、BD 层级、申请邀请和 App 查询读模型属于 user-services代码落在当前仓库的 `services/user-service` host domain。`room-service` 只产生房间事件,`wallet-service` 负责余额和提现。 旧 Java 项目只作为术语和历史口径参考。新架构按当前 Go 服务边界重新设计:不新增独立 `host-service` 微服务主播、Agency、BD 层级、申请邀请和 App 查询读模型属于 user-services代码落在当前仓库的 `services/user-service` host domain。`room-service` 只产生房间事件,`wallet-service` 负责余额和提现。
@ -282,7 +282,7 @@ agency_memberships(
用户所属区域来自 `user-service.users.region_id`,不能由客户端提交。 用户所属区域来自 `user-service.users.region_id`,不能由客户端提交。
国家/区域后台管理边界在 `hyapp-admin-server`:创建国家、启用/禁用国家、创建区域、启用/禁用区域不属于 App 侧 user-service RPC。App 侧只消费 user-service 已计算好的 `region_id` 和区域投影。 国家/区域后台管理边界在 `hyapp-admin-server`:创建国家、启用/禁用国家、创建区域、启用/禁用区域不属于 App 侧 user-service RPC。App 侧只消费 user-service 已计算好的 `region_id` 和区域投影。
后台创建 BD Leader 是关系管理命令,不是客户端区域提交:`hyapp-admin-server` 负责权限和审计,`user-service` 在同一事务里把目标用户的 `users.region_id` 更新为后台选择的区域,并用同一个区域创建 `bd_profiles` 后台创建 BD Leader 是关系管理命令,不是客户端区域提交:`hyapp-admin-server` 负责权限和审计,`user-service` 在同一事务里把目标用户的 `users.region_id` 更新为后台选择的区域,并用同一个区域创建 `bd_profiles`
用户区域允许变化的完整来源以 [User Region Change Sources](./user-region-country-development.md#user-region-change-sources) 为准Host/Agency/BD 关系只消费当前 `users.region_id` 和各关系表里的区域快照。 用户区域允许变化的完整来源以 [User Region Change Sources](./用户区域与国家开发.md#user-region-change-sources) 为准Host/Agency/BD 关系只消费当前 `users.region_id` 和各关系表里的区域快照。
| Action | Region Rule | | Action | Region Rule |
| --- | --- | | --- | --- |
@ -390,7 +390,7 @@ Leader inviting Agency uses the same Agency creation flow as BD, but `parent_bd_
App 统计和后台结算都不能直接从当前 membership 反查,因为关系会变化。每条工作指标必须在发生时固化归属快照。 App 统计和后台结算都不能直接从当前 membership 反查,因为关系会变化。每条工作指标必须在发生时固化归属快照。
麦上在线时长的基础事实不是主播专属指标,先按所有用户统一沉淀。通用用户麦上时长见 [User Mic Online Time Architecture](./user-mic-online-time-architecture.md)。本节的 `host_daily_stats` 是主播、Agency、BD 工资和团队统计的派生读模型,不是唯一时长来源。 麦上在线时长的基础事实不是主播专属指标,先按所有用户统一沉淀。通用用户麦上时长见 [User Mic Online Time Architecture](./用户麦上在线时长架构.md)。本节的 `host_daily_stats` 是主播、Agency、BD 工资和团队统计的派生读模型,不是唯一时长来源。
### Input Events ### Input Events
@ -585,7 +585,7 @@ Outbox consumers can update search indexes, notifications, BI, admin views, and
## App Message Tab Integration ## App Message Tab Integration
Host/Agency/BD 的 App 可见通知统一进入 App `消息` tab 的 `system` 分区,具体 message inbox 设计见 [App Message Tab Architecture](./app-message-tab-architecture.md)。`user-service` host domain 只产出 host outbox 事件或调用内部 `CreateInboxMessage`;不能直接写 message inbox 表,也不能复用后台 `admin_notifications` Host/Agency/BD 的 App 可见通知统一进入 App `消息` tab 的 `system` 分区,具体 message inbox 设计见 [App Message Tab Architecture](./App消息Tab架构.md)。`user-service` host domain 只产出 host outbox 事件或调用内部 `CreateInboxMessage`;不能直接写 message inbox 表,也不能复用后台 `admin_notifications`
| Source Event | Target User | Message Type | Action | | Source Event | Target User | Message Type | Action |
| --- | --- | --- | --- | | --- | --- | --- | --- |
@ -616,7 +616,7 @@ Host/Agency/BD 的 App 可见通知统一进入 App `消息` tab 的 `system`
## Implementation Order ## Implementation Order
整体开发顺序单独维护在 [Host Agency BD Implementation Sequence](./host-agency-bd-implementation-sequence.md)。App 文档只保留 App 侧目标、边界、流程和验收口径,避免和后台工资实施顺序重复。 整体开发顺序单独维护在 [Host Agency BD Implementation Sequence](./主播公会BD实施顺序.md)。App 文档只保留 App 侧目标、边界、流程和验收口径,避免和后台工资实施顺序重复。
## Verification Matrix ## Verification Matrix

View File

@ -24,7 +24,7 @@
健康检查规则见: 健康检查规则见:
```text ```text
docs/health-checks.md docs/健康检查.md
``` ```
## Shutdown State Machine ## Shutdown State Machine

View File

@ -1,6 +1,6 @@
# Health Check Development Guide # Health Check Development Guide
本文档只定义部署稳定性需要的健康检查能力。健康检查不承载业务判断,不替代监控告警,也不用于修复故障。进程下线和 drain 细节见 `docs/graceful-shutdown.md`。 本文档只定义部署稳定性需要的健康检查能力。健康检查不承载业务判断,不替代监控告警,也不用于修复故障。进程下线和 drain 细节见 `docs/优雅停机.md`。
## Goals ## Goals

View File

@ -1,6 +1,6 @@
# Lucky Gift Activity Service Development Guide # Lucky Gift Activity Service Development Guide
本文档定义幸运礼物在 `activity-service` 中的开发落地。产品规则见 `docs/lucky-gift-product.md`。 本文档定义幸运礼物在 `activity-service` 中的开发落地。产品规则见 `docs/幸运礼物产品方案.md`。
## Ownership ## Ownership

View File

@ -0,0 +1,185 @@
# My Page Overview API
本文定义 App 底部 `我的` 页首屏聚合接口。首屏只返回设计稿可直接展示的轻量摘要基础资料、顶部计数、金币卡片、VIP 卡片、身份入口和固定功能入口。
## Endpoint
```http
GET /api/v1/users/me/overview
Authorization: Bearer <access_token>
```
响应使用 gateway `/api/v1` envelope
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_01",
"data": {
"profile": {
"user_id": "10001",
"display_user_id": "163000",
"default_display_user_id": "163000",
"display_user_id_kind": "default",
"display_user_id_expires_at_ms": 0,
"username": "Alice",
"avatar": "https://cdn.example.com/avatar.png",
"language": "en",
"profile_completed": true,
"onboarding_status": "completed"
},
"profile_stats": {
"visitors_count": 12,
"following_count": 34,
"friends_count": 56,
"updated_at_ms": 1777996800000
},
"wallet": {
"coin_amount": 12000,
"updated_at_ms": 1777996800000
},
"vip": {
"level": 3,
"name": "VIP3",
"active": true,
"started_at_ms": 1777392000000,
"expires_at_ms": 1777996800000
},
"roles": {
"is_host": true,
"is_agency": false,
"is_bd": false,
"is_bd_leader": false,
"is_coin_seller": false,
"host_status": "active",
"agency_id": 0,
"bd_id": 0,
"pending_role_invitations": 0
},
"entries": [
{
"key": "store",
"visible": true,
"enabled": true,
"route": "/store",
"icon_key": "store",
"badge_count": 0,
"order": 10
},
{
"key": "apply_host",
"visible": false,
"enabled": false,
"route": "/host/apply",
"icon_key": "apply_host",
"badge_count": 0,
"order": 70
},
{
"key": "host_center",
"visible": true,
"enabled": true,
"route": "/host",
"icon_key": "host_center",
"badge_count": 0,
"order": 80
}
],
"server_time_ms": 1777996800000
}
}
```
## Field Sources
| Field | Source | Rule |
| --- | --- | --- |
| `profile` | `user-service.GetUser` | 只取头像、昵称、展示 ID、语言等基础资料 |
| `profile_stats` | `user-service.GetMyProfileStats` | 读 `user_profile_stats` 计数表,不实时扫访问、关注、好友大表 |
| `wallet` | `wallet-service.GetWalletValueSummary` | 首屏只返回金币余额,不返回钻石和美元余额 |
| `vip` | `wallet-service.GetMyVip` | 只取等级、名称、有效期和 active 状态 |
| `roles` | `user-service.GetUserRoleSummary` | 所有身份统一判断,避免 gateway 多 RPC fanout |
| `entries` | gateway composition | 根据 `roles` 生成入口显隐;标题由 App 按 key 做多语言 |
| `server_time_ms` | gateway | 当前服务端毫秒时间 |
`我的` 页首屏不调用 `ListMessageTabs`,不返回系统消息红点、活动消息红点、邀请码、钻石、美元余额,也不返回钱包操作开关。充值、提现、兑换能力在钱包二级页取。
## Entry Rules
| Entry Key | Visible Rule |
| --- | --- |
| `store` | always true |
| `bag` | always true |
| `level` | always true |
| `task` | always true |
| `apply_host` | `roles.is_host == false` |
| `host_center` | `roles.is_host == true` |
| `agency_center` | `roles.is_agency == true` |
| `bd_center` | `roles.is_bd == true` |
| `bd_leader_center` | `roles.is_bd_leader == true` |
| `coin_seller_center` | `roles.is_coin_seller == true` |
| `settings` | always true |
| `language` | always true |
同一个用户可能同时拥有多个身份。角色必须用 bool flags 表达,不要用互斥枚举。
## Excluded From Overview
这些内容不能放进首屏接口:
| Field | Reason | Replacement |
| --- | --- | --- |
| 消息 tab、系统消息、活动消息 | 我的页设计稿不展示消息红点 | 消息 tab 页面接口 |
| 邀请码入口和邀请统计 | 截图首屏没有邀请码入口 | 邀请页或活动入口 |
| `wallet.recharge_enabled` / `withdraw_enabled` / `diamond_exchange_enabled` | 首屏卡片不需要操作开关 | `GET /api/v1/wallet/me/overview` |
| `DIAMOND` / `USD_BALANCE` | 首屏钱包卡片只展示金币 | 钱包二级页余额接口 |
| 邀请用户列表、钱包流水、工资明细、团队成员、背包完整列表、任务列表 | 都需要分页或聚合 | 对应二级页接口 |
## Performance Budget
- 聚合超时预算:`200ms`
- 必须并行请求 profile、profile stats、wallet value summary、VIP、role summary。
- 允许 wallet、VIP、profile stats、role summary 降级并返回 `unavailable=true`
- `user-service.GetUser` 失败时整体失败,因为基础资料是首屏主数据。
- 首屏不得实时 `COUNT(*)`、不得扫流水、不得扫成员、不得查完整背包、不得现算工资。
```mermaid
sequenceDiagram
participant C as Client
participant G as gateway-service
participant U as user-service
participant W as wallet-service
C->>G: GET /api/v1/users/me/overview
par profile
G->>U: GetUser(user_id)
and stats
G->>U: GetMyProfileStats(user_id)
and roles
G->>U: GetUserRoleSummary(user_id)
and wallet
G->>W: GetWalletValueSummary(user_id)
G->>W: GetMyVip(user_id)
end
G->>G: compose entries
G-->>C: overview
```
## Secondary Endpoints
| Endpoint | Purpose |
| --- | --- |
| `GET /api/v1/users/me/visitors` | 访问记录列表 |
| `GET /api/v1/users/me/following` | 关注列表 |
| `GET /api/v1/users/me/friends` | 好友列表 |
| `GET /api/v1/users/me/friend-requests` | 好友申请列表 |
| `GET /api/v1/users/me/role-summary` | 单独刷新身份摘要 |
| `GET /api/v1/wallet/me/overview` | 钱包二级页余额和操作开关 |
| `GET /api/v1/wallet/recharge/products` | 当前区域充值渠道和档位 |
| `GET /api/v1/wallet/diamond-exchange/config` | 钻石兑换金币/余额配置 |
| `POST /api/v1/wallet/withdrawals/apply` | 创建待审核提现申请并冻结 `USD_BALANCE` |
| `GET /api/v1/wallet/transactions` | 钱包流水分页 |
| `GET /api/v1/vip/me` | 当前 VIP 状态 |
| `GET /api/v1/vip/packages` | 可购买 VIP 包 |
| `POST /api/v1/vip/purchase` | 购买、续期或升级 VIP |

View File

@ -31,12 +31,25 @@
| GET | `/api/v1/users/me/identity` | users | `getMyIdentity` | 查询当前用户展示短号状态 | | GET | `/api/v1/users/me/identity` | users | `getMyIdentity` | 查询当前用户展示短号状态 |
| GET | `/api/v1/users/me/host-identity` | users | `getMyHostIdentity` | 查询当前用户 Host/Agency/BD 身份 | | GET | `/api/v1/users/me/host-identity` | users | `getMyHostIdentity` | 查询当前用户 Host/Agency/BD 身份 |
| GET | `/api/v1/users/me/overview` | users | `getMyOverview` | 我的页首屏聚合摘要 | | GET | `/api/v1/users/me/overview` | users | `getMyOverview` | 我的页首屏聚合摘要 |
| GET | `/api/v1/users/me/role-summary` | users | `getMyRoleSummary` | 查询当前用户角色摘要 |
| POST | `/api/v1/users/me/onboarding/complete` | users | `completeMyOnboarding` | 完成注册页必填资料 | | POST | `/api/v1/users/me/onboarding/complete` | users | `completeMyOnboarding` | 完成注册页必填资料 |
| POST | `/api/v1/users/me/profile/update` | users | `updateMyProfile` | 修改当前用户基础资料 | | POST | `/api/v1/users/me/profile/update` | users | `updateMyProfile` | 修改当前用户基础资料 |
| POST | `/api/v1/users/me/country/change` | users | `changeMyCountry` | 修改当前用户国家 | | POST | `/api/v1/users/me/country/change` | users | `changeMyCountry` | 修改当前用户国家 |
| POST | `/api/v1/users/me/display-id/change` | users | `changeMyDisplayUserID` | 修改当前用户默认短号 | | POST | `/api/v1/users/me/display-id/change` | users | `changeMyDisplayUserID` | 修改当前用户默认短号 |
| POST | `/api/v1/users/me/display-id/pretty/apply` | users | `applyMyPrettyDisplayUserID` | 申请临时靓号 | | POST | `/api/v1/users/me/display-id/pretty/apply` | users | `applyMyPrettyDisplayUserID` | 申请临时靓号 |
| GET | `/api/v1/rooms` | rooms | `listRooms` | 房间发现列表,卡片返回 `im_group_id` | | POST | `/api/v1/users/{user_id}/visit` | users | `recordProfileVisit` | 记录访问用户主页 |
| GET | `/api/v1/users/me/visitors` | users | `listMyProfileVisitors` | 我的访问记录列表 |
| POST | `/api/v1/users/{user_id}/follow` | users | `followUser` | 关注用户 |
| DELETE | `/api/v1/users/{user_id}/follow` | users | `unfollowUser` | 取消关注用户 |
| GET | `/api/v1/users/me/following` | users | `listMyFollowing` | 我的关注列表 |
| POST | `/api/v1/users/{user_id}/friend-requests` | users | `applyFriend` | 创建好友申请,客户端随后发 IM 申请消息 |
| POST | `/api/v1/users/{user_id}/friend-requests/accept` | users | `acceptFriendApplication` | 同意该用户发来的好友申请 |
| GET | `/api/v1/users/me/friend-requests` | users | `listMyFriendApplications` | 我的好友申请列表 |
| GET | `/api/v1/users/me/friends` | users | `listMyFriends` | 我的好友列表 |
| 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/current` | rooms | `getCurrentRoom` | 查询当前可恢复房间 | | GET | `/api/v1/rooms/current` | rooms | `getCurrentRoom` | 查询当前可恢复房间 |
| GET | `/api/v1/rooms/snapshot` | rooms | `getRoomSnapshot` | 查询房间完整快照 | | GET | `/api/v1/rooms/snapshot` | rooms | `getRoomSnapshot` | 查询房间完整快照 |
| POST | `/api/v1/rooms/create` | rooms | `createRoom` | 创建房间 | | POST | `/api/v1/rooms/create` | rooms | `createRoom` | 创建房间 |
@ -65,7 +78,16 @@
| POST | `/api/v1/messages/read-all` | messages | `markInboxSectionRead` | 标记分区全部已读 | | POST | `/api/v1/messages/read-all` | messages | `markInboxSectionRead` | 标记分区全部已读 |
| POST | `/api/v1/messages/{message_id}/read` | messages | `markInboxMessageRead` | 标记单条消息已读 | | POST | `/api/v1/messages/{message_id}/read` | messages | `markInboxMessageRead` | 标记单条消息已读 |
| DELETE | `/api/v1/messages/{message_id}` | messages | `deleteInboxMessage` | 删除单条消息 | | DELETE | `/api/v1/messages/{message_id}` | messages | `deleteInboxMessage` | 删除单条消息 |
| GET | `/api/v1/wallet/me/overview` | wallet | `getWalletOverview` | 钱包二级页摘要 |
| GET | `/api/v1/wallet/me/balances` | wallet | `getMyBalances` | 查询当前用户钱包余额 |
| GET | `/api/v1/wallet/recharge/products` | wallet | `listRechargeProducts` | 查询充值商品和渠道 |
| GET | `/api/v1/wallet/diamond-exchange/config` | wallet | `getDiamondExchangeConfig` | 查询钻石兑换配置 |
| POST | `/api/v1/wallet/withdrawals/apply` | wallet | `applyWithdrawal` | 提交美元余额提现申请 |
| GET | `/api/v1/wallet/transactions` | wallet | `listWalletTransactions` | 分页查询钱包流水 |
| POST | `/api/v1/wallet/coin-seller/transfer` | wallet | `transferCoinFromSeller` | 币商给玩家转金币 | | POST | `/api/v1/wallet/coin-seller/transfer` | wallet | `transferCoinFromSeller` | 币商给玩家转金币 |
| GET | `/api/v1/vip/me` | vip | `getMyVIP` | 查询当前 VIP 状态 |
| GET | `/api/v1/vip/packages` | vip | `listVIPPackages` | 查询可购买 VIP 套餐 |
| POST | `/api/v1/vip/purchase` | vip | `purchaseVIP` | 购买、续费或升级 VIP |
## Admin HTTP ## Admin HTTP
@ -201,11 +223,22 @@
| POST | `/hyapp.user.v1.AuthService/RefreshToken` | auth | `userAuthRefreshToken` | 刷新 token | | POST | `/hyapp.user.v1.AuthService/RefreshToken` | auth | `userAuthRefreshToken` | 刷新 token |
| POST | `/hyapp.user.v1.AuthService/Logout` | auth | `userAuthLogout` | 登出并失效 refresh session | | POST | `/hyapp.user.v1.AuthService/Logout` | auth | `userAuthLogout` | 登出并失效 refresh session |
| POST | `/hyapp.user.v1.UserService/GetUser` | users | `userGetUser` | 查询单个用户主状态 | | POST | `/hyapp.user.v1.UserService/GetUser` | users | `userGetUser` | 查询单个用户主状态 |
| POST | `/hyapp.user.v1.UserService/GetMyProfileStats` | users | `userGetMyProfileStats` | 查询我的页顶部计数 read model |
| POST | `/hyapp.user.v1.UserService/BatchGetUsers` | users | `userBatchGetUsers` | 批量查询用户主状态 | | POST | `/hyapp.user.v1.UserService/BatchGetUsers` | users | `userBatchGetUsers` | 批量查询用户主状态 |
| POST | `/hyapp.user.v1.UserService/ListUserIDs` | users | `userListUserIDs` | 后台任务按 user_id 游标查询目标用户 | | POST | `/hyapp.user.v1.UserService/ListUserIDs` | users | `userListUserIDs` | 后台任务按 user_id 游标查询目标用户 |
| POST | `/hyapp.user.v1.UserService/UpdateUserProfile` | users | `userUpdateUserProfile` | 修改用户基础资料 | | POST | `/hyapp.user.v1.UserService/UpdateUserProfile` | users | `userUpdateUserProfile` | 修改用户基础资料 |
| POST | `/hyapp.user.v1.UserService/ChangeUserCountry` | users | `userChangeUserCountry` | 修改用户国家 | | POST | `/hyapp.user.v1.UserService/ChangeUserCountry` | users | `userChangeUserCountry` | 修改用户国家 |
| POST | `/hyapp.user.v1.UserService/CompleteOnboarding` | users | `userCompleteOnboarding` | 完成注册页必填资料 | | POST | `/hyapp.user.v1.UserService/CompleteOnboarding` | users | `userCompleteOnboarding` | 完成注册页必填资料 |
| POST | `/hyapp.user.v1.UserSocialService/RecordProfileVisit` | users | `userRecordProfileVisit` | 记录访问用户主页 |
| POST | `/hyapp.user.v1.UserSocialService/ListProfileVisitors` | users | `userListProfileVisitors` | 查询访问记录列表 |
| POST | `/hyapp.user.v1.UserSocialService/FollowUser` | users | `userFollowUser` | 关注用户 |
| POST | `/hyapp.user.v1.UserSocialService/UnfollowUser` | users | `userUnfollowUser` | 取消关注 |
| POST | `/hyapp.user.v1.UserSocialService/ListFollowing` | users | `userListFollowing` | 查询关注列表 |
| POST | `/hyapp.user.v1.UserSocialService/ApplyFriend` | users | `userApplyFriend` | 创建好友申请 |
| POST | `/hyapp.user.v1.UserSocialService/AcceptFriendApplication` | users | `userAcceptFriendApplication` | 同意好友申请并写双向好友关系 |
| POST | `/hyapp.user.v1.UserSocialService/DeleteFriend` | users | `userDeleteFriend` | 删除双向好友关系 |
| POST | `/hyapp.user.v1.UserSocialService/ListFriends` | users | `userListFriends` | 查询好友列表 |
| POST | `/hyapp.user.v1.UserSocialService/ListFriendApplications` | users | `userListFriendApplications` | 查询好友申请列表 |
| POST | `/hyapp.user.v1.UserDeviceService/BindPushToken` | user-devices | `userBindPushToken` | 绑定当前登录用户的系统推送 token | | POST | `/hyapp.user.v1.UserDeviceService/BindPushToken` | user-devices | `userBindPushToken` | 绑定当前登录用户的系统推送 token |
| POST | `/hyapp.user.v1.UserDeviceService/DeletePushToken` | user-devices | `userDeletePushToken` | 失效当前登录用户的系统推送 token | | POST | `/hyapp.user.v1.UserDeviceService/DeletePushToken` | user-devices | `userDeletePushToken` | 失效当前登录用户的系统推送 token |
| POST | `/hyapp.user.v1.CountryAdminService/CreateCountry` | country-admin | `userCreateCountry` | 创建国家主数据 | | POST | `/hyapp.user.v1.CountryAdminService/CreateCountry` | country-admin | `userCreateCountry` | 创建国家主数据 |

View File

@ -578,7 +578,7 @@ activity-service 额外需要:
推荐顺序: 推荐顺序:
1. 写 `docs/grpc-error-model.md` 或把本文件错误模型抽成独立文档。 1. 写 `docs/grpc错误模型.md` 或把本文件错误模型抽成独立文档。
2. 扩展 `pkg/xerr`,支持 reason 和 transport 映射。 2. 扩展 `pkg/xerr`,支持 reason 和 transport 映射。
3. 新增 `api/proto/user/v1/auth.proto``user.proto` 3. 新增 `api/proto/user/v1/auth.proto``user.proto`
4. 补 `user-service` 的 app/service/domain/storage/transport 骨架。 4. 补 `user-service` 的 app/service/domain/storage/transport 骨架。

View File

@ -1,6 +1,6 @@
# Login And Registration Technical Design # Login And Registration Technical Design
本文档定义 v1 登录、三方注册登录、密码设置、邀请码归因和 token 生命周期。它只描述当前架构事实和接口边界,不记录实现流水账。邀请码详细架构见 [Registration Invite Code Architecture](./registration-invite-code-architecture.md)。 本文档定义 v1 登录、三方注册登录、密码设置、邀请码归因和 token 生命周期。它只描述当前架构事实和接口边界,不记录实现流水账。邀请码详细架构见 [Registration Invite Code Architecture](./注册邀请码架构.md)。
## Scope ## Scope

113
docs/社交关系接口.md Normal file
View File

@ -0,0 +1,113 @@
# Social Relations API
本文定义 App 端访问记录、关注、好友申请和好友关系接口。关系事实由 `user-service` 持久化;腾讯云 IM 只负责把好友申请消息送进聊天窗口,不作为好友关系事实来源。
## Core Flow
```mermaid
sequenceDiagram
participant A as User A App
participant G as gateway-service
participant U as user-service
participant IM as Tencent IM
participant B as User B App
A->>G: POST /api/v1/users/{B}/friend-requests
G->>U: ApplyFriend(A, B)
U-->>G: pending application
G-->>A: application=pending
A->>IM: send custom friend request message to B
IM-->>B: chat window message
B->>G: POST /api/v1/users/{A}/friend-requests/accept
G->>U: AcceptFriendApplication(B, A)
U-->>G: bidirectional friendship
G-->>B: friend record
```
关键边界:
- 申请接口只创建 pending 申请,不直接发 IM。
- 同意必须调用 accept 接口,不能只靠 IM 消息状态。
- 删除好友是双向删除。
- 自己访问自己、关注自己、加自己好友全部拒绝。
- 重复访问只增加访问次数,不重复增加 `visitors_count`
- 重复关注、重复取消关注、重复同意、重复删除都按幂等处理。
## HTTP Endpoints
所有接口都要求完成资料的登录态,响应统一使用 `{code,message,request_id,data}` envelope。
| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/api/v1/users/{user_id}/visit` | 记录访问用户主页 |
| `GET` | `/api/v1/users/me/visitors?page=1&page_size=20` | 我的访问记录 |
| `POST` | `/api/v1/users/{user_id}/follow` | 关注用户 |
| `DELETE` | `/api/v1/users/{user_id}/follow` | 取消关注 |
| `GET` | `/api/v1/users/me/following?page=1&page_size=20` | 我的关注列表 |
| `POST` | `/api/v1/users/{user_id}/friend-requests` | 申请添加好友 |
| `POST` | `/api/v1/users/{user_id}/friend-requests/accept` | 同意该用户发来的好友申请 |
| `GET` | `/api/v1/users/me/friend-requests?direction=incoming&page=1&page_size=20` | 好友申请列表,`direction` 支持 `incoming`/`outgoing` |
| `GET` | `/api/v1/users/me/friends?page=1&page_size=20` | 我的好友列表 |
| `DELETE` | `/api/v1/users/{user_id}/friend` | 删除好友 |
## Response Examples
申请好友:
```json
{
"application": {
"requester_user_id": "10001",
"target_user_id": "10002",
"status": "pending",
"created_at_ms": 1777996800000,
"updated_at_ms": 1777996800000
},
"already_friends": false
}
```
同意好友:
```json
{
"friend": {
"user_id": "10002",
"friend_user_id": "10001",
"friended_at_ms": 1777996810000
}
}
```
关注:
```json
{
"following": true,
"follower_stats": {
"visitors_count": 0,
"following_count": 1,
"friends_count": 0,
"updated_at_ms": 1777996800000
}
}
```
## Storage
| Table | Owner | Purpose |
| --- | --- | --- |
| `user_profile_stats` | user-service | 我的页计数 read model访问人数、关注数、好友数 |
| `user_profile_visits` | user-service | 用户主页访问去重记录,主键 `(app_code,target_user_id,visitor_user_id)` |
| `user_follows` | user-service | 单向关注关系,取消关注只改状态 |
| `user_friend_applications` | user-service | 好友申请pending/accepted |
| `user_friendships` | user-service | 双向好友边,每个好友关系写两行 |
计数更新和关系边写入必须在同一个 MySQL 事务中完成。`user_profile_stats` 是首屏计数来源,不能在 `/users/me/overview` 里实时扫关系表。
## App Notes
- 好友申请成功后App 发送腾讯 IM 自定义消息给对方;消息里携带 `requester_user_id` 和申请状态展示参数。
- 聊天窗口点击同意时调用 `/api/v1/users/{requester_user_id}/friend-requests/accept`
- 如果 accept 返回成功App 再刷新好友列表或本地会话状态。
- 如果 IM 消息丢失pending 申请仍可在 `/api/v1/users/me/friend-requests?direction=incoming` 找回。

View File

@ -491,7 +491,7 @@ room_mic_publish_stale_event_ignored_total
## Implementation Order ## Implementation Order
1. 新增 `docs/tencent-rtc-token-uid-mapping-development.md` 并确认产品边界。 1. 新增 `docs/腾讯RTC票据UID映射开发.md` 并确认产品边界。
2. 抽出通用 Tencent UserSig 算法到 `pkg/tencentsig`,保持 IM usersig 测试通过。 2. 抽出通用 Tencent UserSig 算法到 `pkg/tencentsig`,保持 IM usersig 测试通过。
3. 新增 `pkg/tencentrtc`,定义 RTC token config/result 和 `GenerateToken` 3. 新增 `pkg/tencentrtc`,定义 RTC token config/result 和 `GenerateToken`
4. 扩展 gateway config`tencent_rtc` 4. 扩展 gateway config`tencent_rtc`

View File

@ -33,7 +33,7 @@
| 靓号租约 | `DONE` | pretty display id lease 表和接口存在 | 购买/支付入口未形成完整商业闭环 | | 靓号租约 | `DONE` | pretty display id lease 表和接口存在 | 购买/支付入口未形成完整商业闭环 |
| 用户资料更新 | `DONE` | 支持 username/avatar/birth国家独立接口 | 房间内资料变更实时刷新未做 | | 用户资料更新 | `DONE` | 支持 username/avatar/birth国家独立接口 | 房间内资料变更实时刷新未做 |
| 国家修改和审计 | `DONE` | `ChangeUserCountry``user_country_change_logs`、冷却期 | 新增“区域”能力尚未实现 | | 国家修改和审计 | `DONE` | `ChangeUserCountry``user_country_change_logs`、冷却期 | 新增“区域”能力尚未实现 |
| 区域功能 | `TODO` | 已有需求文档 `docs/user-region-country-development.md` | regions/region_countries 表、管理端、注册归属、历史回填 | | 区域功能 | `TODO` | 已有需求文档 `docs/用户区域与国家开发.md` | regions/region_countries 表、管理端、注册归属、历史回填 |
## Gateway HTTP Surface ## Gateway HTTP Surface
@ -139,7 +139,7 @@
| --- | --- | --- | --- | | --- | --- | --- | --- |
| activity-service 底座 | `PARTIAL` | gRPC Ping/GetActivityStatus、MySQL activity/outbox 表存在 | 尚未消费 room outbox | | activity-service 底座 | `PARTIAL` | gRPC Ping/GetActivityStatus、MySQL activity/outbox 表存在 | 尚未消费 room outbox |
| room outbox 消费边界 | `PARTIAL` | `consumer/room` 有接口骨架 | 真实 polling/MQ/offset/idempotency 未实现 | | room outbox 消费边界 | `PARTIAL` | `consumer/room` 有接口骨架 | 真实 polling/MQ/offset/idempotency 未实现 |
| 幸运礼物开发文档 | `DONE` | `docs/lucky-gift-activity-service-development.md` | 按文档实现 proto、pool、risk、draw | | 幸运礼物开发文档 | `DONE` | `docs/幸运礼物Activity服务开发.md` | 按文档实现 proto、pool、risk、draw |
| 活动配置 | `TODO` | 只有 activity status projection | 活动规则、时间、奖池、风控配置未实现 | | 活动配置 | `TODO` | 只有 activity status projection | 活动规则、时间、奖池、风控配置未实现 |
| LuckyGift Check | `TODO` | 文档有设计proto/code 未实现 | `CheckLuckyGift` RPC 和规则校验 | | LuckyGift Check | `TODO` | 文档有设计proto/code 未实现 | `CheckLuckyGift` RPC 和规则校验 |
| LuckyGift Draw | `TODO` | 文档有设计proto/code 未实现 | 三层奖池、一层风控、幂等 draw | | LuckyGift Draw | `TODO` | 文档有设计proto/code 未实现 | 三层奖池、一层风控、幂等 draw |

View File

@ -142,7 +142,7 @@ sequenceDiagram
- `visible_region_id` 来自创建者当前 `users.region_id` - `visible_region_id` 来自创建者当前 `users.region_id`
- 创建者没有区域时写 `0`,进入 `GLOBAL` 列表桶。 - 创建者没有区域时写 `0`,进入 `GLOBAL` 列表桶。
- 房间创建成功后,创建者改国家不自动修改房间区域。 - 房间创建成功后,创建者改国家不自动修改房间区域。
- 用户 `users.region_id` 允许变化的来源见 [User Region Change Sources](./user-region-country-development.md#user-region-change-sources)。无论是用户改国家、区域映射 rebuild还是后台创建 BD Leader 导致用户区域变化,都不自动迁移已创建房间的 `visible_region_id` - 用户 `users.region_id` 允许变化的来源见 [User Region Change Sources](./用户区域与国家开发.md#user-region-change-sources)。无论是用户改国家、区域映射 rebuild还是后台创建 BD Leader 导致用户区域变化,都不自动迁移已创建房间的 `visible_region_id`
- 管理端迁移房间区域必须走独立命令,例如 `ChangeRoomVisibleRegion`,并写审计。 - 管理端迁移房间区域必须走独立命令,例如 `ChangeRoomVisibleRegion`,并写审计。
### Why Not Dynamic By Owner Region ### Why Not Dynamic By Owner Region

View File

@ -5,7 +5,7 @@
Room Cell 的完整状态机见: Room Cell 的完整状态机见:
```text ```text
docs/room-cell-state-machines.md docs/房间Cell状态机.md
``` ```
## Scope ## Scope

View File

@ -34,8 +34,13 @@ sequenceDiagram
G-->>C: IM user_sig G-->>C: IM user_sig
C->>IM: login(user_id, user_sig) C->>IM: login(user_id, user_sig)
C->>G: GET /api/v1/rooms C->>G: GET /api/v1/rooms/me
G->>R: ListRooms(viewer region from user-service) G->>R: GetMyRoom(owner_user_id from token)
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
G->>R: ListRoomFeeds(viewer region from user-service)
R-->>G: room cards R-->>G: room cards
G-->>C: room cards + im_group_id G-->>C: room cards + im_group_id
@ -64,11 +69,12 @@ sequenceDiagram
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 1 | `GET /api/v1/im/usersig` | yes | 用户级 IM 登录票据,可在 App 启动后预取并按 TTL 复用 | | 1 | `GET /api/v1/im/usersig` | yes | 用户级 IM 登录票据,可在 App 启动后预取并按 TTL 复用 |
| 2 | Tencent IM SDK `login` | yes | 客户端先建立 IM 身份,房间群仍等 JoinRoom 成功后再进 | | 2 | Tencent IM SDK `login` | yes | 客户端先建立 IM 身份,房间群仍等 JoinRoom 成功后再进 |
| 3 | `GET /api/v1/rooms` | yes for discovery | 房间卡片返回 `im_group_id`,只用于展示和准备,不授予入群权限 | | 3 | `GET /api/v1/rooms/me` | yes for Mine top card | 直接读取 owner 房间权威元数据,不依赖发现页投影 |
| 4 | `POST /api/v1/rooms/join` | yes | 建立业务 presence并返回首屏房间数据和内嵌 RTC token | | 4 | `GET /api/v1/rooms/feeds?tab=visited\|friend\|following` | yes for Mine lists | Mine 列表 tab`visited` 由 JoinRoom 成功写入,`friend/following` 读取关系流读模型 |
| 5 | Tencent IM SDK `joinGroup(im_group_id)` | yes | 必须在 JoinRoom 成功后执行IM 回调守卫会校验 room-service presence | | 5 | `POST /api/v1/rooms/join` | yes | 建立业务 presence并返回首屏房间数据和内嵌 RTC token |
| 6 | Tencent RTC SDK `enterRoom` | yes for voice room audio | 使用 JoinRoom 响应里的 `rtc.token` 进入真实音频频道旁听 | | 6 | Tencent IM SDK `joinGroup(im_group_id)` | yes | 必须在 JoinRoom 成功后执行IM 回调守卫会校验 room-service presence |
| 7 | `POST /api/v1/rooms/heartbeat` | yes | 刷新业务 presence避免 stale timeout | | 7 | Tencent RTC SDK `enterRoom` | yes for voice room audio | 使用 JoinRoom 响应里的 `rtc.token` 进入真实音频频道旁听 |
| 8 | `POST /api/v1/rooms/heartbeat` | yes | 刷新业务 presence避免 stale timeout |
如果 JoinRoom 响应里的 `rtc.available=false`,客户端可以先展示房间页并重试 `POST /api/v1/rtc/token`RTC 失败不能回滚已经成功的业务 presence。 如果 JoinRoom 响应里的 `rtc.available=false`,客户端可以先展示房间页并重试 `POST /api/v1/rtc/token`RTC 失败不能回滚已经成功的业务 presence。

View File

@ -2,7 +2,7 @@
本文定义钱包服务的目标架构、账务模型、核心流程和落地顺序。当前 `wallet-service` 只有单币种 `coin` 扣费能力,新需求需要升级为多资产账户、外部支付订单、币商充值、主播奖励和提现审核的一体化账务边界。 本文定义钱包服务的目标架构、账务模型、核心流程和落地顺序。当前 `wallet-service` 只有单币种 `coin` 扣费能力,新需求需要升级为多资产账户、外部支付订单、币商充值、主播奖励和提现审核的一体化账务边界。
资源、资源组、用户资源赠送和礼物从资源库选择的边界见 [Resource And Resource Group Architecture](./resource-group-gift-architecture.md)。本文只保留钱包账务和礼物价格结算边界。 资源、资源组、用户资源赠送和礼物从资源库选择的边界见 [Resource And Resource Group Architecture](./资源组与礼物架构.md)。本文只保留钱包账务和礼物价格结算边界。
## Goals ## Goals

View File

@ -72,6 +72,9 @@ var catalog = map[Code]Spec{
CoinSellerStockAmountInvalid: spec(codes.InvalidArgument, httpStatusBadRequest, CoinSellerStockAmountInvalid, "invalid argument"), CoinSellerStockAmountInvalid: spec(codes.InvalidArgument, httpStatusBadRequest, CoinSellerStockAmountInvalid, "invalid argument"),
CoinSellerPaymentRefDuplicated: spec(codes.AlreadyExists, httpStatusConflict, CoinSellerPaymentRefDuplicated, "conflict"), CoinSellerPaymentRefDuplicated: spec(codes.AlreadyExists, httpStatusConflict, CoinSellerPaymentRefDuplicated, "conflict"),
IdempotencyConflict: spec(codes.AlreadyExists, httpStatusConflict, IdempotencyConflict, "conflict"), IdempotencyConflict: spec(codes.AlreadyExists, httpStatusConflict, IdempotencyConflict, "conflict"),
VIPLevelNotFound: spec(codes.NotFound, httpStatusNotFound, VIPLevelNotFound, "not found"),
VIPLevelDisabled: spec(codes.FailedPrecondition, httpStatusConflict, VIPLevelDisabled, "conflict"),
VIPDowngradeNotAllowed: spec(codes.FailedPrecondition, httpStatusConflict, VIPDowngradeNotAllowed, "conflict"),
RuleNotActive: spec(codes.FailedPrecondition, httpStatusConflict, RuleNotActive, "conflict"), RuleNotActive: spec(codes.FailedPrecondition, httpStatusConflict, RuleNotActive, "conflict"),
EventAlreadyConsumed: spec(codes.AlreadyExists, httpStatusConflict, EventAlreadyConsumed, "conflict"), EventAlreadyConsumed: spec(codes.AlreadyExists, httpStatusConflict, EventAlreadyConsumed, "conflict"),

View File

@ -88,6 +88,12 @@ const (
CoinSellerPaymentRefDuplicated Code = "COIN_SELLER_PAYMENT_REF_DUPLICATED" CoinSellerPaymentRefDuplicated Code = "COIN_SELLER_PAYMENT_REF_DUPLICATED"
// IdempotencyConflict 表示 command_id 相同但业务 payload 不一致。 // IdempotencyConflict 表示 command_id 相同但业务 payload 不一致。
IdempotencyConflict Code = "IDEMPOTENCY_CONFLICT" IdempotencyConflict Code = "IDEMPOTENCY_CONFLICT"
// VIPLevelNotFound 表示目标 VIP 等级不存在。
VIPLevelNotFound Code = "VIP_LEVEL_NOT_FOUND"
// VIPLevelDisabled 表示目标 VIP 等级当前不可购买。
VIPLevelDisabled Code = "VIP_LEVEL_DISABLED"
// VIPDowngradeNotAllowed 表示用户当前有效 VIP 等级高于本次购买目标等级。
VIPDowngradeNotAllowed Code = "VIP_DOWNGRADE_NOT_ALLOWED"
// RuleNotActive 表示活动规则当前不可用。 // RuleNotActive 表示活动规则当前不可用。
RuleNotActive Code = "RULE_NOT_ACTIVE" RuleNotActive Code = "RULE_NOT_ACTIVE"

View File

@ -76,6 +76,7 @@ func New(cfg config.Config) (*App, error) {
var userDeviceClient client.UserDeviceClient = client.NewGRPCUserDeviceClient(userConn) var userDeviceClient client.UserDeviceClient = client.NewGRPCUserDeviceClient(userConn)
var userCountryQueryClient client.UserCountryQueryClient = client.NewGRPCUserCountryQueryClient(userConn) var userCountryQueryClient client.UserCountryQueryClient = client.NewGRPCUserCountryQueryClient(userConn)
var userHostClient client.UserHostClient = client.NewGRPCUserHostClient(userConn) var userHostClient client.UserHostClient = client.NewGRPCUserHostClient(userConn)
var userSocialClient client.UserSocialClient = client.NewGRPCUserSocialClient(userConn)
var appRegistryClient client.AppRegistryClient = client.NewGRPCAppRegistryClient(userConn) var appRegistryClient client.AppRegistryClient = client.NewGRPCAppRegistryClient(userConn)
var walletClient client.WalletClient = client.NewGRPCWalletClient(walletConn) var walletClient client.WalletClient = client.NewGRPCWalletClient(walletConn)
var messageClient client.MessageInboxClient = client.NewGRPCMessageInboxClient(activityConn) var messageClient client.MessageInboxClient = client.NewGRPCMessageInboxClient(activityConn)
@ -100,6 +101,7 @@ func New(cfg config.Config) (*App, error) {
handler.SetUserDeviceClient(userDeviceClient) handler.SetUserDeviceClient(userDeviceClient)
handler.SetUserCountryQueryClient(userCountryQueryClient) handler.SetUserCountryQueryClient(userCountryQueryClient)
handler.SetUserHostClient(userHostClient) handler.SetUserHostClient(userHostClient)
handler.SetUserSocialClient(userSocialClient)
handler.SetAppRegistryClient(appRegistryClient) handler.SetAppRegistryClient(appRegistryClient)
handler.SetWalletClient(walletClient) handler.SetWalletClient(walletClient)
handler.SetMessageInboxClient(messageClient) handler.SetMessageInboxClient(messageClient)

View File

@ -38,6 +38,8 @@ type RoomGuardClient interface {
// RoomQueryClient 抽象 gateway 对 room-service 读模型查询的依赖。 // RoomQueryClient 抽象 gateway 对 room-service 读模型查询的依赖。
type RoomQueryClient interface { type RoomQueryClient interface {
ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error)
ListRoomFeeds(ctx context.Context, req *roomv1.ListRoomFeedsRequest) (*roomv1.ListRoomsResponse, error)
GetMyRoom(ctx context.Context, req *roomv1.GetMyRoomRequest) (*roomv1.GetMyRoomResponse, error)
GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error)
GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error)
} }
@ -155,6 +157,14 @@ func (c *grpcRoomQueryClient) ListRooms(ctx context.Context, req *roomv1.ListRoo
return c.client.ListRooms(ctx, req) return c.client.ListRooms(ctx, req)
} }
func (c *grpcRoomQueryClient) ListRoomFeeds(ctx context.Context, req *roomv1.ListRoomFeedsRequest) (*roomv1.ListRoomsResponse, error) {
return c.client.ListRoomFeeds(ctx, req)
}
func (c *grpcRoomQueryClient) GetMyRoom(ctx context.Context, req *roomv1.GetMyRoomRequest) (*roomv1.GetMyRoomResponse, error) {
return c.client.GetMyRoom(ctx, req)
}
func (c *grpcRoomQueryClient) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) { func (c *grpcRoomQueryClient) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) {
return c.client.GetCurrentRoom(ctx, req) return c.client.GetCurrentRoom(ctx, req)
} }

View File

@ -29,12 +29,27 @@ type UserIdentityClient interface {
// UserProfileClient 抽象 gateway 对 user-service 用户资料能力的依赖。 // UserProfileClient 抽象 gateway 对 user-service 用户资料能力的依赖。
type UserProfileClient interface { type UserProfileClient interface {
GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error)
GetMyProfileStats(ctx context.Context, req *userv1.GetMyProfileStatsRequest) (*userv1.GetMyProfileStatsResponse, error)
BatchGetUsers(ctx context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) BatchGetUsers(ctx context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error)
CompleteOnboarding(ctx context.Context, req *userv1.CompleteOnboardingRequest) (*userv1.CompleteOnboardingResponse, error) CompleteOnboarding(ctx context.Context, req *userv1.CompleteOnboardingRequest) (*userv1.CompleteOnboardingResponse, error)
UpdateUserProfile(ctx context.Context, req *userv1.UpdateUserProfileRequest) (*userv1.UpdateUserProfileResponse, error) UpdateUserProfile(ctx context.Context, req *userv1.UpdateUserProfileRequest) (*userv1.UpdateUserProfileResponse, error)
ChangeUserCountry(ctx context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) ChangeUserCountry(ctx context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error)
} }
// UserSocialClient 抽象 gateway 对 user-service 访问、关注和好友关系能力的依赖。
type UserSocialClient interface {
RecordProfileVisit(ctx context.Context, req *userv1.RecordProfileVisitRequest) (*userv1.RecordProfileVisitResponse, error)
ListProfileVisitors(ctx context.Context, req *userv1.ListProfileVisitorsRequest) (*userv1.ListProfileVisitorsResponse, error)
FollowUser(ctx context.Context, req *userv1.FollowUserRequest) (*userv1.FollowUserResponse, error)
UnfollowUser(ctx context.Context, req *userv1.UnfollowUserRequest) (*userv1.UnfollowUserResponse, error)
ListFollowing(ctx context.Context, req *userv1.ListFollowingRequest) (*userv1.ListFollowingResponse, error)
ApplyFriend(ctx context.Context, req *userv1.ApplyFriendRequest) (*userv1.ApplyFriendResponse, error)
AcceptFriendApplication(ctx context.Context, req *userv1.AcceptFriendApplicationRequest) (*userv1.AcceptFriendApplicationResponse, error)
DeleteFriend(ctx context.Context, req *userv1.DeleteFriendRequest) (*userv1.DeleteFriendResponse, error)
ListFriends(ctx context.Context, req *userv1.ListFriendsRequest) (*userv1.ListFriendsResponse, error)
ListFriendApplications(ctx context.Context, req *userv1.ListFriendApplicationsRequest) (*userv1.ListFriendApplicationsResponse, error)
}
// UserDeviceClient 抽象 gateway 对 user-service 设备 push token 的依赖。 // UserDeviceClient 抽象 gateway 对 user-service 设备 push token 的依赖。
type UserDeviceClient interface { type UserDeviceClient interface {
BindPushToken(ctx context.Context, req *userv1.BindPushTokenRequest) (*userv1.BindPushTokenResponse, error) BindPushToken(ctx context.Context, req *userv1.BindPushTokenRequest) (*userv1.BindPushTokenResponse, error)
@ -51,6 +66,7 @@ type UserHostClient interface {
GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error)
GetBDProfile(ctx context.Context, req *userv1.GetBDProfileRequest) (*userv1.GetBDProfileResponse, error) GetBDProfile(ctx context.Context, req *userv1.GetBDProfileRequest) (*userv1.GetBDProfileResponse, error)
GetCoinSellerProfile(ctx context.Context, req *userv1.GetCoinSellerProfileRequest) (*userv1.GetCoinSellerProfileResponse, error) GetCoinSellerProfile(ctx context.Context, req *userv1.GetCoinSellerProfileRequest) (*userv1.GetCoinSellerProfileResponse, error)
GetUserRoleSummary(ctx context.Context, req *userv1.GetUserRoleSummaryRequest) (*userv1.GetUserRoleSummaryResponse, error)
} }
// AppRegistryClient 抽象 gateway 对 user-service App 注册表的解析依赖。 // AppRegistryClient 抽象 gateway 对 user-service App 注册表的解析依赖。
@ -70,6 +86,10 @@ type grpcUserProfileClient struct {
client userv1.UserServiceClient client userv1.UserServiceClient
} }
type grpcUserSocialClient struct {
client userv1.UserSocialServiceClient
}
type grpcUserDeviceClient struct { type grpcUserDeviceClient struct {
client userv1.UserDeviceServiceClient client userv1.UserDeviceServiceClient
} }
@ -107,6 +127,13 @@ func NewGRPCUserProfileClient(conn *grpc.ClientConn) UserProfileClient {
} }
} }
// NewGRPCUserSocialClient 用 gRPC 连接构造 user-service social client。
func NewGRPCUserSocialClient(conn *grpc.ClientConn) UserSocialClient {
return &grpcUserSocialClient{
client: userv1.NewUserSocialServiceClient(conn),
}
}
// NewGRPCUserDeviceClient 用 gRPC 连接构造 user-service 设备推送 client。 // NewGRPCUserDeviceClient 用 gRPC 连接构造 user-service 设备推送 client。
func NewGRPCUserDeviceClient(conn *grpc.ClientConn) UserDeviceClient { func NewGRPCUserDeviceClient(conn *grpc.ClientConn) UserDeviceClient {
return &grpcUserDeviceClient{ return &grpcUserDeviceClient{
@ -179,6 +206,10 @@ func (c *grpcUserProfileClient) GetUser(ctx context.Context, req *userv1.GetUser
return c.client.GetUser(ctx, req) return c.client.GetUser(ctx, req)
} }
func (c *grpcUserProfileClient) GetMyProfileStats(ctx context.Context, req *userv1.GetMyProfileStatsRequest) (*userv1.GetMyProfileStatsResponse, error) {
return c.client.GetMyProfileStats(ctx, req)
}
func (c *grpcUserProfileClient) BatchGetUsers(ctx context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) { func (c *grpcUserProfileClient) BatchGetUsers(ctx context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) {
return c.client.BatchGetUsers(ctx, req) return c.client.BatchGetUsers(ctx, req)
} }
@ -195,6 +226,46 @@ func (c *grpcUserProfileClient) ChangeUserCountry(ctx context.Context, req *user
return c.client.ChangeUserCountry(ctx, req) return c.client.ChangeUserCountry(ctx, req)
} }
func (c *grpcUserSocialClient) RecordProfileVisit(ctx context.Context, req *userv1.RecordProfileVisitRequest) (*userv1.RecordProfileVisitResponse, error) {
return c.client.RecordProfileVisit(ctx, req)
}
func (c *grpcUserSocialClient) ListProfileVisitors(ctx context.Context, req *userv1.ListProfileVisitorsRequest) (*userv1.ListProfileVisitorsResponse, error) {
return c.client.ListProfileVisitors(ctx, req)
}
func (c *grpcUserSocialClient) FollowUser(ctx context.Context, req *userv1.FollowUserRequest) (*userv1.FollowUserResponse, error) {
return c.client.FollowUser(ctx, req)
}
func (c *grpcUserSocialClient) UnfollowUser(ctx context.Context, req *userv1.UnfollowUserRequest) (*userv1.UnfollowUserResponse, error) {
return c.client.UnfollowUser(ctx, req)
}
func (c *grpcUserSocialClient) ListFollowing(ctx context.Context, req *userv1.ListFollowingRequest) (*userv1.ListFollowingResponse, error) {
return c.client.ListFollowing(ctx, req)
}
func (c *grpcUserSocialClient) ApplyFriend(ctx context.Context, req *userv1.ApplyFriendRequest) (*userv1.ApplyFriendResponse, error) {
return c.client.ApplyFriend(ctx, req)
}
func (c *grpcUserSocialClient) AcceptFriendApplication(ctx context.Context, req *userv1.AcceptFriendApplicationRequest) (*userv1.AcceptFriendApplicationResponse, error) {
return c.client.AcceptFriendApplication(ctx, req)
}
func (c *grpcUserSocialClient) DeleteFriend(ctx context.Context, req *userv1.DeleteFriendRequest) (*userv1.DeleteFriendResponse, error) {
return c.client.DeleteFriend(ctx, req)
}
func (c *grpcUserSocialClient) ListFriends(ctx context.Context, req *userv1.ListFriendsRequest) (*userv1.ListFriendsResponse, error) {
return c.client.ListFriends(ctx, req)
}
func (c *grpcUserSocialClient) ListFriendApplications(ctx context.Context, req *userv1.ListFriendApplicationsRequest) (*userv1.ListFriendApplicationsResponse, error) {
return c.client.ListFriendApplications(ctx, req)
}
func (c *grpcUserDeviceClient) BindPushToken(ctx context.Context, req *userv1.BindPushTokenRequest) (*userv1.BindPushTokenResponse, error) { func (c *grpcUserDeviceClient) BindPushToken(ctx context.Context, req *userv1.BindPushTokenRequest) (*userv1.BindPushTokenResponse, error) {
return c.client.BindPushToken(ctx, req) return c.client.BindPushToken(ctx, req)
} }
@ -219,6 +290,10 @@ func (c *grpcUserHostClient) GetCoinSellerProfile(ctx context.Context, req *user
return c.client.GetCoinSellerProfile(ctx, req) return c.client.GetCoinSellerProfile(ctx, req)
} }
func (c *grpcUserHostClient) GetUserRoleSummary(ctx context.Context, req *userv1.GetUserRoleSummaryRequest) (*userv1.GetUserRoleSummaryResponse, error) {
return c.client.GetUserRoleSummary(ctx, req)
}
func (c *grpcAppRegistryClient) ResolveApp(ctx context.Context, req *userv1.ResolveAppRequest) (*userv1.ResolveAppResponse, error) { func (c *grpcAppRegistryClient) ResolveApp(ctx context.Context, req *userv1.ResolveAppRequest) (*userv1.ResolveAppResponse, error) {
return c.client.ResolveApp(ctx, req) return c.client.ResolveApp(ctx, req)
} }

View File

@ -11,6 +11,15 @@ import (
// WalletClient 抽象 gateway 对 wallet-service 余额查询能力的依赖。 // WalletClient 抽象 gateway 对 wallet-service 余额查询能力的依赖。
type WalletClient interface { type WalletClient interface {
GetBalances(ctx context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) GetBalances(ctx context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error)
GetWalletOverview(ctx context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error)
GetWalletValueSummary(ctx context.Context, req *walletv1.GetWalletValueSummaryRequest) (*walletv1.GetWalletValueSummaryResponse, error)
ListRechargeProducts(ctx context.Context, req *walletv1.ListRechargeProductsRequest) (*walletv1.ListRechargeProductsResponse, error)
GetDiamondExchangeConfig(ctx context.Context, req *walletv1.GetDiamondExchangeConfigRequest) (*walletv1.GetDiamondExchangeConfigResponse, error)
ListWalletTransactions(ctx context.Context, req *walletv1.ListWalletTransactionsRequest) (*walletv1.ListWalletTransactionsResponse, error)
ApplyWithdrawal(ctx context.Context, req *walletv1.ApplyWithdrawalRequest) (*walletv1.ApplyWithdrawalResponse, error)
ListVipPackages(ctx context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error)
GetMyVip(ctx context.Context, req *walletv1.GetMyVipRequest) (*walletv1.GetMyVipResponse, error)
PurchaseVip(ctx context.Context, req *walletv1.PurchaseVipRequest) (*walletv1.PurchaseVipResponse, error)
TransferCoinFromSeller(ctx context.Context, req *walletv1.TransferCoinFromSellerRequest) (*walletv1.TransferCoinFromSellerResponse, error) TransferCoinFromSeller(ctx context.Context, req *walletv1.TransferCoinFromSellerRequest) (*walletv1.TransferCoinFromSellerResponse, error)
ListResources(ctx context.Context, req *walletv1.ListResourcesRequest) (*walletv1.ListResourcesResponse, error) ListResources(ctx context.Context, req *walletv1.ListResourcesRequest) (*walletv1.ListResourcesResponse, error)
GetResourceGroup(ctx context.Context, req *walletv1.GetResourceGroupRequest) (*walletv1.GetResourceGroupResponse, error) GetResourceGroup(ctx context.Context, req *walletv1.GetResourceGroupRequest) (*walletv1.GetResourceGroupResponse, error)
@ -32,6 +41,42 @@ func (c *grpcWalletClient) GetBalances(ctx context.Context, req *walletv1.GetBal
return c.client.GetBalances(ctx, req) return c.client.GetBalances(ctx, req)
} }
func (c *grpcWalletClient) GetWalletOverview(ctx context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error) {
return c.client.GetWalletOverview(ctx, req)
}
func (c *grpcWalletClient) GetWalletValueSummary(ctx context.Context, req *walletv1.GetWalletValueSummaryRequest) (*walletv1.GetWalletValueSummaryResponse, error) {
return c.client.GetWalletValueSummary(ctx, req)
}
func (c *grpcWalletClient) ListRechargeProducts(ctx context.Context, req *walletv1.ListRechargeProductsRequest) (*walletv1.ListRechargeProductsResponse, error) {
return c.client.ListRechargeProducts(ctx, req)
}
func (c *grpcWalletClient) GetDiamondExchangeConfig(ctx context.Context, req *walletv1.GetDiamondExchangeConfigRequest) (*walletv1.GetDiamondExchangeConfigResponse, error) {
return c.client.GetDiamondExchangeConfig(ctx, req)
}
func (c *grpcWalletClient) ListWalletTransactions(ctx context.Context, req *walletv1.ListWalletTransactionsRequest) (*walletv1.ListWalletTransactionsResponse, error) {
return c.client.ListWalletTransactions(ctx, req)
}
func (c *grpcWalletClient) ApplyWithdrawal(ctx context.Context, req *walletv1.ApplyWithdrawalRequest) (*walletv1.ApplyWithdrawalResponse, error) {
return c.client.ApplyWithdrawal(ctx, req)
}
func (c *grpcWalletClient) ListVipPackages(ctx context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error) {
return c.client.ListVipPackages(ctx, req)
}
func (c *grpcWalletClient) GetMyVip(ctx context.Context, req *walletv1.GetMyVipRequest) (*walletv1.GetMyVipResponse, error) {
return c.client.GetMyVip(ctx, req)
}
func (c *grpcWalletClient) PurchaseVip(ctx context.Context, req *walletv1.PurchaseVipRequest) (*walletv1.PurchaseVipResponse, error) {
return c.client.PurchaseVip(ctx, req)
}
func (c *grpcWalletClient) TransferCoinFromSeller(ctx context.Context, req *walletv1.TransferCoinFromSellerRequest) (*walletv1.TransferCoinFromSellerResponse, error) { func (c *grpcWalletClient) TransferCoinFromSeller(ctx context.Context, req *walletv1.TransferCoinFromSellerRequest) (*walletv1.TransferCoinFromSellerResponse, error) {
return c.client.TransferCoinFromSeller(ctx, req) return c.client.TransferCoinFromSeller(ctx, req)
} }

View File

@ -0,0 +1,350 @@
package http
import (
"net/http"
"strings"
userv1 "hyapp.local/api/proto/user/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
"hyapp/services/gateway-service/internal/auth"
)
type rechargeProductData struct {
ProductID int64 `json:"product_id"`
ProductCode string `json:"product_code"`
Channel string `json:"channel"`
CurrencyCode string `json:"currency_code"`
CoinAmount int64 `json:"coin_amount"`
AmountMinor int64 `json:"amount_minor"`
PolicyVersion string `json:"policy_version"`
Enabled bool `json:"enabled"`
SortOrder int32 `json:"sort_order"`
}
type diamondExchangeRuleData struct {
ExchangeType string `json:"exchange_type"`
FromAssetType string `json:"from_asset_type"`
ToAssetType string `json:"to_asset_type"`
FromAmount int64 `json:"from_amount"`
ToAmount int64 `json:"to_amount"`
Enabled bool `json:"enabled"`
}
type walletOverviewData struct {
Balances []assetBalanceData `json:"balances"`
FeatureFlags walletFeatureFlagsData `json:"feature_flags"`
}
type walletFeatureFlagsData struct {
RechargeEnabled bool `json:"recharge_enabled"`
DiamondExchangeEnabled bool `json:"diamond_exchange_enabled"`
WithdrawEnabled bool `json:"withdraw_enabled"`
}
type walletTransactionData struct {
EntryID int64 `json:"entry_id"`
TransactionID string `json:"transaction_id"`
BizType string `json:"biz_type"`
AssetType string `json:"asset_type"`
AvailableDelta int64 `json:"available_delta"`
FrozenDelta int64 `json:"frozen_delta"`
AvailableAfter int64 `json:"available_after"`
FrozenAfter int64 `json:"frozen_after"`
CounterpartyUserID int64 `json:"counterparty_user_id"`
RoomID string `json:"room_id"`
CreatedAtMS int64 `json:"created_at_ms"`
}
type withdrawalApplyRequestBody struct {
CommandID string `json:"command_id"`
CommandIDAlt string `json:"commandId"`
Amount int64 `json:"amount"`
PayoutAccount string `json:"payout_account"`
PayoutAlt string `json:"payoutAccount"`
Reason string `json:"reason"`
}
type withdrawalData struct {
WithdrawalID string `json:"withdrawal_id"`
UserID int64 `json:"user_id"`
AssetType string `json:"asset_type"`
Amount int64 `json:"amount"`
Status string `json:"status"`
PayoutAccount string `json:"payout_account"`
Reason string `json:"reason"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type vipPurchaseRequestBody struct {
CommandID string `json:"command_id"`
CommandIDAlt string `json:"commandId"`
Level int32 `json:"level"`
}
type vipPackageData struct {
Level int32 `json:"level"`
Name string `json:"name"`
Status string `json:"status"`
PriceCoin int64 `json:"price_coin"`
DurationMS int64 `json:"duration_ms"`
RewardResourceGroupID int64 `json:"reward_resource_group_id"`
RewardItems []vipRewardItemData `json:"reward_items"`
CanPurchase bool `json:"can_purchase"`
SortOrder int32 `json:"sort_order"`
}
type vipRewardItemData struct {
ResourceID int64 `json:"resource_id"`
ResourceCode string `json:"resource_code"`
ResourceType string `json:"resource_type"`
Name string `json:"name"`
Quantity int64 `json:"quantity"`
ExpiresAtMS int64 `json:"expires_at_ms"`
}
// getWalletOverview 返回钱包二级页摘要;我的页首屏使用更轻的 WalletValueSummary。
func (h *Handler) getWalletOverview(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
resp, err := h.walletClient.GetWalletOverview(request.Context(), &walletv1.GetWalletOverviewRequest{
RequestId: requestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
UserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, walletOverviewFromProto(resp))
}
// listRechargeProducts 返回用户当前区域可用的充值档位。
func (h *Handler) listRechargeProducts(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil || h.userProfileClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
userID := auth.UserIDFromContext(request.Context())
profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: authRequestMeta(request, ""),
UserId: userID,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
resp, err := h.walletClient.ListRechargeProducts(request.Context(), &walletv1.ListRechargeProductsRequest{
RequestId: requestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
UserId: userID,
RegionId: profileResp.GetUser().GetRegionId(),
})
if err != nil {
writeRPCError(writer, request, err)
return
}
products := make([]rechargeProductData, 0, len(resp.GetProducts()))
for _, product := range resp.GetProducts() {
products = append(products, rechargeProductFromProto(product))
}
writeOK(writer, request, map[string]any{"channels": resp.GetChannels(), "products": products})
}
// getDiamondExchangeConfig 返回钻石兑换配置。
func (h *Handler) getDiamondExchangeConfig(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
resp, err := h.walletClient.GetDiamondExchangeConfig(request.Context(), &walletv1.GetDiamondExchangeConfigRequest{
RequestId: requestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
UserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
writeRPCError(writer, request, err)
return
}
rules := make([]diamondExchangeRuleData, 0, len(resp.GetRules()))
for _, rule := range resp.GetRules() {
rules = append(rules, diamondExchangeRuleFromProto(rule))
}
writeOK(writer, request, map[string]any{"rules": rules})
}
// listWalletTransactions 返回当前用户钱包流水分页。
func (h *Handler) listWalletTransactions(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
page, ok := parsePositiveInt32Query(request, "page", 1)
if !ok {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
if !ok {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
resp, err := h.walletClient.ListWalletTransactions(request.Context(), &walletv1.ListWalletTransactionsRequest{
RequestId: requestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
UserId: auth.UserIDFromContext(request.Context()),
AssetType: strings.TrimSpace(request.URL.Query().Get("asset_type")),
Page: page,
PageSize: pageSize,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
items := make([]walletTransactionData, 0, len(resp.GetTransactions()))
for _, item := range resp.GetTransactions() {
items = append(items, walletTransactionFromProto(item))
}
writeOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
}
// applyWithdrawal 创建待人工审核的提现申请。
func (h *Handler) applyWithdrawal(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
var body withdrawalApplyRequestBody
if !decode(writer, request, &body) {
return
}
commandID := strings.TrimSpace(body.CommandID)
if commandID == "" {
commandID = strings.TrimSpace(body.CommandIDAlt)
}
payoutAccount := strings.TrimSpace(body.PayoutAccount)
if payoutAccount == "" {
payoutAccount = strings.TrimSpace(body.PayoutAlt)
}
if commandID == "" || body.Amount <= 0 || payoutAccount == "" {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
resp, err := h.walletClient.ApplyWithdrawal(request.Context(), &walletv1.ApplyWithdrawalRequest{
CommandId: commandID,
AppCode: appcode.FromContext(request.Context()),
UserId: auth.UserIDFromContext(request.Context()),
Amount: body.Amount,
PayoutAccount: payoutAccount,
Reason: strings.TrimSpace(body.Reason),
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, map[string]any{
"withdrawal": withdrawalFromProto(resp.GetWithdrawal()),
"balance": balanceFromProto(resp.GetBalance()),
})
}
func rechargeProductFromProto(product *walletv1.RechargeProduct) rechargeProductData {
if product == nil {
return rechargeProductData{}
}
return rechargeProductData{
ProductID: product.GetProductId(),
ProductCode: product.GetProductCode(),
Channel: product.GetChannel(),
CurrencyCode: product.GetCurrencyCode(),
CoinAmount: product.GetCoinAmount(),
AmountMinor: product.GetAmountMinor(),
PolicyVersion: product.GetPolicyVersion(),
Enabled: product.GetEnabled(),
SortOrder: product.GetSortOrder(),
}
}
func diamondExchangeRuleFromProto(rule *walletv1.DiamondExchangeRule) diamondExchangeRuleData {
if rule == nil {
return diamondExchangeRuleData{}
}
return diamondExchangeRuleData{
ExchangeType: rule.GetExchangeType(),
FromAssetType: rule.GetFromAssetType(),
ToAssetType: rule.GetToAssetType(),
FromAmount: rule.GetFromAmount(),
ToAmount: rule.GetToAmount(),
Enabled: rule.GetEnabled(),
}
}
func walletOverviewFromProto(resp *walletv1.GetWalletOverviewResponse) walletOverviewData {
if resp == nil {
return walletOverviewData{}
}
balances := make([]assetBalanceData, 0, len(resp.GetBalances()))
for _, balance := range resp.GetBalances() {
balances = append(balances, balanceFromProto(balance))
}
flags := resp.GetFeatureFlags()
return walletOverviewData{
Balances: balances,
FeatureFlags: walletFeatureFlagsData{
RechargeEnabled: flags.GetRechargeEnabled(),
DiamondExchangeEnabled: flags.GetDiamondExchangeEnabled(),
WithdrawEnabled: flags.GetWithdrawEnabled(),
},
}
}
func walletTransactionFromProto(item *walletv1.WalletTransaction) walletTransactionData {
if item == nil {
return walletTransactionData{}
}
return walletTransactionData{
EntryID: item.GetEntryId(),
TransactionID: item.GetTransactionId(),
BizType: item.GetBizType(),
AssetType: item.GetAssetType(),
AvailableDelta: item.GetAvailableDelta(),
FrozenDelta: item.GetFrozenDelta(),
AvailableAfter: item.GetAvailableAfter(),
FrozenAfter: item.GetFrozenAfter(),
CounterpartyUserID: item.GetCounterpartyUserId(),
RoomID: item.GetRoomId(),
CreatedAtMS: item.GetCreatedAtMs(),
}
}
func withdrawalFromProto(item *walletv1.WithdrawalRequest) withdrawalData {
if item == nil {
return withdrawalData{}
}
return withdrawalData{
WithdrawalID: item.GetWithdrawalId(),
UserID: item.GetUserId(),
AssetType: item.GetAssetType(),
Amount: item.GetAmount(),
Status: item.GetStatus(),
PayoutAccount: item.GetPayoutAccount(),
Reason: item.GetReason(),
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
}
}
func balanceFromProto(balance *walletv1.AssetBalance) assetBalanceData {
if balance == nil {
return assetBalanceData{}
}
return assetBalanceData{
AssetType: balance.GetAssetType(),
AvailableAmount: balance.GetAvailableAmount(),
FrozenAmount: balance.GetFrozenAmount(),
Version: balance.GetVersion(),
}
}

View File

@ -27,6 +27,7 @@ type Handler struct {
userClient client.UserAuthClient userClient client.UserAuthClient
userIdentityClient client.UserIdentityClient userIdentityClient client.UserIdentityClient
userProfileClient client.UserProfileClient userProfileClient client.UserProfileClient
userSocialClient client.UserSocialClient
userDeviceClient client.UserDeviceClient userDeviceClient client.UserDeviceClient
userCountryClient client.UserCountryQueryClient userCountryClient client.UserCountryQueryClient
userHostClient client.UserHostClient userHostClient client.UserHostClient
@ -145,6 +146,11 @@ func (h *Handler) SetUserHostClient(userHostClient client.UserHostClient) {
h.userHostClient = userHostClient h.userHostClient = userHostClient
} }
// SetUserSocialClient 注入 user-service 访问、关注和好友关系 client。
func (h *Handler) SetUserSocialClient(userSocialClient client.UserSocialClient) {
h.userSocialClient = userSocialClient
}
// SetAppRegistryClient 注入 App 注册表解析 clientgateway 用它把包名转换成内部 app_code。 // SetAppRegistryClient 注入 App 注册表解析 clientgateway 用它把包名转换成内部 app_code。
func (h *Handler) SetAppRegistryClient(appRegistryClient client.AppRegistryClient) { func (h *Handler) SetAppRegistryClient(appRegistryClient client.AppRegistryClient) {
h.appRegistryClient = appRegistryClient h.appRegistryClient = appRegistryClient

View File

@ -77,6 +77,36 @@ func (h *Handler) getMyHostIdentity(writer http.ResponseWriter, request *http.Re
writeOK(writer, request, identity) writeOK(writer, request, identity)
} }
// getMyRoleSummary 返回我的页入口显隐所需的完整角色摘要。
func (h *Handler) getMyRoleSummary(writer http.ResponseWriter, request *http.Request) {
if h.userHostClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
resp, err := h.userHostClient.GetUserRoleSummary(request.Context(), &userv1.GetUserRoleSummaryRequest{
Meta: authRequestMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
writeRPCError(writer, request, err)
return
}
summary := resp.GetSummary()
writeOK(writer, request, overviewRolesData{
IsHost: summary.GetIsHost(),
IsAgency: summary.GetIsAgency(),
IsBD: summary.GetIsBd(),
IsBDLeader: summary.GetIsBdLeader(),
IsCoinSeller: summary.GetIsCoinSeller(),
HostStatus: summary.GetHostStatus(),
AgencyID: summary.GetAgencyId(),
BDID: summary.GetBdId(),
BDStatus: summary.GetBdStatus(),
CoinSellerStatus: summary.GetCoinSellerStatus(),
PendingRoleInvitations: summary.GetPendingRoleInvitations(),
})
}
func applyHostIdentity(identity *userHostIdentityData, profile *userv1.HostProfile) { func applyHostIdentity(identity *userHostIdentityData, profile *userv1.HostProfile) {
if profile == nil || profile.GetStatus() != hostIdentityActiveStatus { if profile == nil || profile.GetStatus() != hostIdentityActiveStatus {
return return

View File

@ -6,7 +6,6 @@ import (
"sync" "sync"
"time" "time"
activityv1 "hyapp.local/api/proto/activity/v1"
userv1 "hyapp.local/api/proto/user/v1" userv1 "hyapp.local/api/proto/user/v1"
walletv1 "hyapp.local/api/proto/wallet/v1" walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode" "hyapp/pkg/appcode"
@ -16,55 +15,62 @@ import (
const overviewAggregationTimeout = 200 * time.Millisecond const overviewAggregationTimeout = 200 * time.Millisecond
var overviewAssetTypes = []string{"COIN", "DIAMOND", "USD_BALANCE"}
type myOverviewData struct { type myOverviewData struct {
Profile userProfileData `json:"profile"` Profile userProfileData `json:"profile"`
Wallet overviewWalletData `json:"wallet"` ProfileStats overviewProfileStats `json:"profile_stats"`
Invite overviewInviteData `json:"invite"` Wallet overviewWalletData `json:"wallet"`
Roles overviewRolesData `json:"roles"` VIP overviewVIPData `json:"vip"`
Badges overviewBadgesData `json:"badges"` Roles overviewRolesData `json:"roles"`
Entries []overviewEntryData `json:"entries"` Entries []overviewEntryData `json:"entries"`
ServerTimeMS int64 `json:"server_time_ms"` ServerTimeMS int64 `json:"server_time_ms"`
} }
type overviewWalletData struct { type overviewWalletData struct {
Balances []assetBalanceData `json:"balances"` CoinAmount int64 `json:"coin_amount"`
RechargeEnabled bool `json:"recharge_enabled"` UpdatedAtMS int64 `json:"updated_at_ms"`
DiamondExchangeEnabled bool `json:"diamond_exchange_enabled"` Unavailable bool `json:"unavailable,omitempty"`
WithdrawEnabled bool `json:"withdraw_enabled"`
Unavailable bool `json:"unavailable,omitempty"`
} }
type overviewRolesData struct { type overviewRolesData struct {
IsHost bool `json:"is_host"` IsHost bool `json:"is_host"`
IsAgent bool `json:"is_agent"` IsAgency bool `json:"is_agency"`
IsBD bool `json:"is_bd"` IsBD bool `json:"is_bd"`
IsBDLeader bool `json:"is_bd_leader"` IsBDLeader bool `json:"is_bd_leader"`
IsCoinSeller bool `json:"is_coin_seller"` IsCoinSeller bool `json:"is_coin_seller"`
Unavailable bool `json:"unavailable,omitempty"` HostStatus string `json:"host_status,omitempty"`
AgencyID int64 `json:"agency_id,omitempty"`
BDID int64 `json:"bd_id,omitempty"`
BDStatus string `json:"bd_status,omitempty"`
CoinSellerStatus string `json:"coin_seller_status,omitempty"`
PendingRoleInvitations int64 `json:"pending_role_invitations"`
Unavailable bool `json:"unavailable,omitempty"`
} }
type overviewInviteData struct { type overviewProfileStats struct {
MyInviteCode string `json:"my_invite_code"` VisitorsCount int64 `json:"visitors_count"`
InviteEnabled bool `json:"invite_enabled"` FollowingCount int64 `json:"following_count"`
InviteCount int64 `json:"invite_count"` FriendsCount int64 `json:"friends_count"`
ValidInviteCount int64 `json:"valid_invite_count"` UpdatedAtMS int64 `json:"updated_at_ms"`
ValidInviteThresholdCoin int64 `json:"valid_invite_threshold_coin"` Unavailable bool `json:"unavailable,omitempty"`
}
type overviewBadgesData struct {
UnreadSystemMessages int64 `json:"unread_system_messages"`
UnreadActivityMessages int64 `json:"unread_activity_messages"`
PendingRoleInvitations int64 `json:"pending_role_invitations"`
Unavailable bool `json:"unavailable,omitempty"`
} }
type overviewEntryData struct { type overviewEntryData struct {
Key string `json:"key"` Key string `json:"key"`
Title string `json:"title"`
Visible bool `json:"visible"` Visible bool `json:"visible"`
Enabled bool `json:"enabled"`
Route string `json:"route"`
IconKey string `json:"icon_key"`
BadgeCount int64 `json:"badge_count"` BadgeCount int64 `json:"badge_count"`
Order int32 `json:"order"`
}
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"`
} }
// getMyOverview 聚合 App 我的页首屏摘要gateway 只做 RPC 编排和入口显隐规则,不查数据库。 // getMyOverview 聚合 App 我的页首屏摘要gateway 只做 RPC 编排和入口显隐规则,不查数据库。
@ -91,7 +97,7 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
}) })
}() }()
var walletResp *walletv1.GetBalancesResponse var walletResp *walletv1.GetWalletValueSummaryResponse
var walletErr error var walletErr error
wg.Add(1) wg.Add(1)
go func() { go func() {
@ -100,11 +106,26 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
walletErr = xerr.New(xerr.Unavailable, "wallet service is not configured") walletErr = xerr.New(xerr.Unavailable, "wallet service is not configured")
return return
} }
walletResp, walletErr = h.walletClient.GetBalances(ctx, &walletv1.GetBalancesRequest{ walletResp, walletErr = h.walletClient.GetWalletValueSummary(ctx, &walletv1.GetWalletValueSummaryRequest{
RequestId: requestIDFromContext(request.Context()), RequestId: requestIDFromContext(request.Context()),
UserId: userID, UserId: userID,
AssetTypes: append([]string(nil), overviewAssetTypes...), AppCode: appcode.FromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()), })
}()
var vipResp *walletv1.GetMyVipResponse
var vipErr error
wg.Add(1)
go func() {
defer wg.Done()
if h.walletClient == nil {
vipErr = xerr.New(xerr.Unavailable, "wallet service is not configured")
return
}
vipResp, vipErr = h.walletClient.GetMyVip(ctx, &walletv1.GetMyVipRequest{
RequestId: requestIDFromContext(request.Context()),
UserId: userID,
AppCode: appcode.FromContext(request.Context()),
}) })
}() }()
@ -116,17 +137,13 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
roles, rolesErr = h.getOverviewRoles(ctx, request, userID) roles, rolesErr = h.getOverviewRoles(ctx, request, userID)
}() }()
var tabsResp *activityv1.ListMessageTabsResponse var statsResp *userv1.GetMyProfileStatsResponse
var tabsErr error var statsErr error
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
if h.messageClient == nil { statsResp, statsErr = h.userProfileClient.GetMyProfileStats(ctx, &userv1.GetMyProfileStatsRequest{
tabsErr = xerr.New(xerr.Unavailable, "message service is not configured") Meta: authRequestMeta(request.WithContext(ctx), ""),
return
}
tabsResp, tabsErr = h.messageClient.ListMessageTabs(ctx, &activityv1.ListMessageTabsRequest{
Meta: activityMeta(request.WithContext(ctx)),
UserId: userID, UserId: userID,
}) })
}() }()
@ -145,173 +162,102 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
roles = overviewRolesData{Unavailable: true} roles = overviewRolesData{Unavailable: true}
} }
wallet := overviewWallet(walletResp, walletErr) wallet := overviewWallet(walletResp, walletErr)
badges := overviewBadges(tabsResp, tabsErr) vip := overviewVIP(vipResp, vipErr)
profileStats := overviewStats(statsResp, statsErr)
writeOK(writer, request, myOverviewData{ writeOK(writer, request, myOverviewData{
Profile: profileData(profileResp.GetUser(), 0), Profile: profileData(profileResp.GetUser(), 0),
ProfileStats: profileStats,
Wallet: wallet, Wallet: wallet,
Invite: overviewInvite(profileResp.GetUser().GetInvite()), VIP: vip,
Roles: roles, Roles: roles,
Badges: badges,
Entries: overviewEntries(roles), Entries: overviewEntries(roles),
ServerTimeMS: time.Now().UnixMilli(), ServerTimeMS: time.Now().UnixMilli(),
}) })
} }
func overviewInvite(invite *userv1.InviteOverview) overviewInviteData {
if invite == nil {
return overviewInviteData{}
}
return overviewInviteData{
MyInviteCode: invite.GetMyInviteCode(),
InviteEnabled: invite.GetInviteEnabled(),
InviteCount: invite.GetInviteCount(),
ValidInviteCount: invite.GetValidInviteCount(),
ValidInviteThresholdCoin: invite.GetValidInviteThresholdCoin(),
}
}
func (h *Handler) getOverviewRoles(ctx context.Context, request *http.Request, userID int64) (overviewRolesData, error) { func (h *Handler) getOverviewRoles(ctx context.Context, request *http.Request, userID int64) (overviewRolesData, error) {
if h.userHostClient == nil { if h.userHostClient == nil {
return overviewRolesData{}, xerr.New(xerr.Unavailable, "user host service is not configured") return overviewRolesData{}, xerr.New(xerr.Unavailable, "user host service is not configured")
} }
var wg sync.WaitGroup resp, err := h.userHostClient.GetUserRoleSummary(ctx, &userv1.GetUserRoleSummaryRequest{
var hostProfile *userv1.HostProfile Meta: authRequestMeta(request.WithContext(ctx), ""),
var bdProfile *userv1.BDProfile UserId: userID,
var coinSellerProfile *userv1.CoinSellerProfile })
var roleErr error if err != nil {
var roleErrOnce sync.Once return overviewRolesData{}, err
setRoleErr := func(err error) {
if err == nil || xerr.ReasonFromGRPC(err) == xerr.NotFound {
return
}
roleErrOnce.Do(func() {
roleErr = err
})
} }
summary := resp.GetSummary()
wg.Add(3)
go func() {
defer wg.Done()
resp, err := h.userHostClient.GetHostProfile(ctx, &userv1.GetHostProfileRequest{
Meta: authRequestMeta(request.WithContext(ctx), ""),
UserId: userID,
})
if err != nil {
setRoleErr(err)
return
}
hostProfile = resp.GetHostProfile()
}()
go func() {
defer wg.Done()
resp, err := h.userHostClient.GetBDProfile(ctx, &userv1.GetBDProfileRequest{
Meta: authRequestMeta(request.WithContext(ctx), ""),
UserId: userID,
})
if err != nil {
setRoleErr(err)
return
}
bdProfile = resp.GetBdProfile()
}()
go func() {
defer wg.Done()
resp, err := h.userHostClient.GetCoinSellerProfile(ctx, &userv1.GetCoinSellerProfileRequest{
Meta: authRequestMeta(request.WithContext(ctx), ""),
UserId: userID,
})
if err != nil {
setRoleErr(err)
return
}
coinSellerProfile = resp.GetCoinSellerProfile()
}()
wg.Wait()
if roleErr != nil {
return overviewRolesData{}, roleErr
}
identity := userHostIdentityData{}
applyHostIdentity(&identity, hostProfile)
applyBDIdentity(&identity, bdProfile)
applyCoinSellerIdentity(&identity, coinSellerProfile)
return overviewRolesData{ return overviewRolesData{
IsHost: identity.IsHost, IsHost: summary.GetIsHost(),
IsAgent: identity.IsAgent, IsAgency: summary.GetIsAgency(),
IsBD: identity.IsBD, IsBD: summary.GetIsBd(),
IsBDLeader: identity.IsBDLeader, IsBDLeader: summary.GetIsBdLeader(),
IsCoinSeller: identity.IsCoinSeller, IsCoinSeller: summary.GetIsCoinSeller(),
HostStatus: summary.GetHostStatus(),
AgencyID: summary.GetAgencyId(),
BDID: summary.GetBdId(),
BDStatus: summary.GetBdStatus(),
CoinSellerStatus: summary.GetCoinSellerStatus(),
PendingRoleInvitations: summary.GetPendingRoleInvitations(),
}, nil }, nil
} }
func overviewWallet(resp *walletv1.GetBalancesResponse, err error) overviewWalletData { func overviewWallet(resp *walletv1.GetWalletValueSummaryResponse, err error) overviewWalletData {
if err != nil { if err != nil {
return overviewWalletData{ return overviewWalletData{Unavailable: true}
Balances: []assetBalanceData{},
Unavailable: true,
}
} }
summary := resp.GetSummary()
byType := make(map[string]*walletv1.AssetBalance)
for _, balance := range resp.GetBalances() {
if balance == nil {
continue
}
byType[balance.GetAssetType()] = balance
}
balances := make([]assetBalanceData, 0, len(overviewAssetTypes))
for _, assetType := range overviewAssetTypes {
balance := byType[assetType]
item := assetBalanceData{AssetType: assetType}
if balance != nil {
item.AvailableAmount = balance.GetAvailableAmount()
item.FrozenAmount = balance.GetFrozenAmount()
item.Version = balance.GetVersion()
}
balances = append(balances, item)
}
return overviewWalletData{ return overviewWalletData{
Balances: balances, CoinAmount: summary.GetCoinAmount(),
RechargeEnabled: true, UpdatedAtMS: summary.GetUpdatedAtMs(),
DiamondExchangeEnabled: true,
WithdrawEnabled: true,
} }
} }
func overviewBadges(resp *activityv1.ListMessageTabsResponse, err error) overviewBadgesData { func overviewVIP(resp *walletv1.GetMyVipResponse, err error) overviewVIPData {
if err != nil { if err != nil {
return overviewBadgesData{Unavailable: true} return overviewVIPData{Unavailable: true}
} }
vip := resp.GetVip()
return overviewVIPData{
Level: vip.GetLevel(),
Name: vip.GetName(),
Active: vip.GetActive(),
StartedAtMS: vip.GetStartedAtMs(),
ExpiresAtMS: vip.GetExpiresAtMs(),
}
}
var badges overviewBadgesData func overviewStats(resp *userv1.GetMyProfileStatsResponse, err error) overviewProfileStats {
for _, section := range resp.GetSections() { if err != nil {
switch section.GetSection() { return overviewProfileStats{Unavailable: true}
case "system": }
badges.UnreadSystemMessages = section.GetUnreadCount() return overviewStatsFromProto(resp.GetStats())
case "activity": }
badges.UnreadActivityMessages = section.GetUnreadCount()
} func overviewStatsFromProto(stats *userv1.UserProfileStats) overviewProfileStats {
return overviewProfileStats{
VisitorsCount: stats.GetVisitorsCount(),
FollowingCount: stats.GetFollowingCount(),
FriendsCount: stats.GetFriendsCount(),
UpdatedAtMS: stats.GetUpdatedAtMs(),
} }
return badges
} }
func overviewEntries(roles overviewRolesData) []overviewEntryData { func overviewEntries(roles overviewRolesData) []overviewEntryData {
return []overviewEntryData{ return []overviewEntryData{
{Key: "wallet", Title: "Wallet", Visible: true}, {Key: "store", Visible: true, Enabled: true, Route: "/store", IconKey: "store", Order: 10},
{Key: "host_center", Title: "Host Center", Visible: roles.IsHost}, {Key: "bag", Visible: true, Enabled: true, Route: "/bag", IconKey: "bag", Order: 20},
{Key: "apply_host", Title: "Apply Host", Visible: !roles.IsHost}, {Key: "level", Visible: true, Enabled: true, Route: "/level", IconKey: "level", Order: 30},
{Key: "agency_center", Title: "Agency Center", Visible: roles.IsAgent}, {Key: "task", Visible: true, Enabled: true, Route: "/task", IconKey: "task", Order: 40},
{Key: "bd_center", Title: "BD Center", Visible: roles.IsBD}, {Key: "apply_host", Visible: !roles.IsHost, Enabled: true, Route: "/host/apply", IconKey: "host_apply", Order: 50},
{Key: "bd_leader_center", Title: "BD Leader Center", Visible: roles.IsBDLeader}, {Key: "host_center", Visible: roles.IsHost, Enabled: roles.IsHost, Route: "/host", IconKey: "host_center", Order: 60},
{Key: "coin_seller_center", Title: "Coin Seller", Visible: roles.IsCoinSeller}, {Key: "agency_center", Visible: roles.IsAgency, Enabled: roles.IsAgency, Route: "/agency", IconKey: "agency", Order: 70},
{Key: "backpack", Title: "Backpack", Visible: true}, {Key: "bd_center", Visible: roles.IsBD, Enabled: roles.IsBD, Route: "/bd", IconKey: "bd", Order: 80},
{Key: "settings", Title: "Settings", Visible: true}, {Key: "bd_leader_center", Visible: roles.IsBDLeader, Enabled: roles.IsBDLeader, Route: "/bd-leader", IconKey: "bd_leader", Order: 90},
{Key: "support", Title: "Support", Visible: true}, {Key: "coin_seller_center", Visible: roles.IsCoinSeller, Enabled: roles.IsCoinSeller, Route: "/coin-seller", IconKey: "coin_seller", Order: 100},
{Key: "settings", Visible: true, Enabled: true, Route: "/settings", IconKey: "settings", Order: 110},
{Key: "language", Visible: true, Enabled: true, Route: "/language", IconKey: "language", Order: 120},
} }
} }

View File

@ -7,31 +7,44 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
activityv1 "hyapp.local/api/proto/activity/v1"
userv1 "hyapp.local/api/proto/user/v1" userv1 "hyapp.local/api/proto/user/v1"
walletv1 "hyapp.local/api/proto/wallet/v1" walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/xerr" "hyapp/pkg/xerr"
"hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/auth"
) )
func TestGetMyOverviewComposesProfileWalletRolesBadgesAndEntries(t *testing.T) { func TestGetMyOverviewComposesProfileWalletVIPStatsRolesAndEntries(t *testing.T) {
profileClient := &fakeUserProfileClient{regionID: 10} profileClient := &fakeUserProfileClient{
walletClient := &fakeWalletClient{resp: &walletv1.GetBalancesResponse{Balances: []*walletv1.AssetBalance{ regionID: 10,
{AssetType: "COIN", AvailableAmount: 12000, Version: 8}, statsResp: &userv1.GetMyProfileStatsResponse{Stats: &userv1.UserProfileStats{
{AssetType: "GIFT_POINT", AvailableAmount: 999}, UserId: 42,
{AssetType: "DIAMOND", AvailableAmount: 300, Version: 3}, VisitorsCount: 1234,
{AssetType: "USD_BALANCE", AvailableAmount: 2500, FrozenAmount: 500, Version: 2}, FollowingCount: 2345,
}}} FriendsCount: 3456,
hostClient := &fakeUserHostClient{ UpdatedAtMs: 7000,
hostProfile: &userv1.HostProfile{UserId: 42, Status: "active", Source: "admin_create_agency"}, }},
bdProfile: &userv1.BDProfile{UserId: 42, Status: "active", Role: "bd_leader"},
profile: &userv1.CoinSellerProfile{UserId: 42, Status: "active", MerchantAssetType: "COIN_SELLER_COIN"},
} }
messageClient := &fakeMessageInboxClient{tabsResp: &activityv1.ListMessageTabsResponse{Sections: []*activityv1.MessageTabSection{ walletClient := &fakeWalletClient{
{Section: "user", Title: "用户", UnreadCount: 99, Source: "tencent_im_sdk"}, valueSummaryResp: &walletv1.GetWalletValueSummaryResponse{Summary: &walletv1.WalletValueSummary{CoinAmount: 12000, UpdatedAtMs: 8000}},
{Section: "system", Title: "系统", UnreadCount: 2, Source: "backend"}, myVipResp: &walletv1.GetMyVipResponse{Vip: &walletv1.UserVip{UserId: 42, Level: 3, Name: "VIP3", Active: true, StartedAtMs: 9000, ExpiresAtMs: 19000}},
{Section: "activity", Title: "活动", UnreadCount: 1, Source: "backend"}, }
}}} hostClient := &fakeUserHostClient{
roleSummary: &userv1.UserRoleSummary{
UserId: 42,
IsHost: true,
IsAgency: true,
IsBd: true,
IsBdLeader: true,
IsCoinSeller: true,
HostStatus: "active",
AgencyId: 7001,
BdId: 42,
BdStatus: "active",
CoinSellerStatus: "active",
PendingRoleInvitations: 4,
},
}
messageClient := &fakeMessageInboxClient{}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient) handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
handler.SetWalletClient(walletClient) handler.SetWalletClient(walletClient)
handler.SetUserHostClient(hostClient) handler.SetUserHostClient(hostClient)
@ -42,56 +55,61 @@ func TestGetMyOverviewComposesProfileWalletRolesBadgesAndEntries(t *testing.T) {
if profileClient.lastGet == nil || profileClient.lastGet.GetUserId() != 42 || profileClient.lastGet.GetMeta().GetAppCode() != "lalu" { if profileClient.lastGet == nil || profileClient.lastGet.GetUserId() != 42 || profileClient.lastGet.GetMeta().GetAppCode() != "lalu" {
t.Fatalf("profile request mismatch: %+v", profileClient.lastGet) t.Fatalf("profile request mismatch: %+v", profileClient.lastGet)
} }
if walletClient.last == nil || walletClient.last.GetUserId() != 42 || walletClient.last.GetAppCode() != "lalu" { if profileClient.lastStats == nil || profileClient.lastStats.GetUserId() != 42 || profileClient.lastStats.GetMeta().GetAppCode() != "lalu" {
t.Fatalf("wallet request mismatch: %+v", walletClient.last) t.Fatalf("profile stats request mismatch: %+v", profileClient.lastStats)
} }
if got := walletClient.last.GetAssetTypes(); len(got) != 3 || got[0] != "COIN" || got[1] != "DIAMOND" || got[2] != "USD_BALANCE" { if walletClient.lastValueSummary == nil || walletClient.lastValueSummary.GetUserId() != 42 || walletClient.lastValueSummary.GetAppCode() != "lalu" {
t.Fatalf("overview wallet asset types mismatch: %+v", got) t.Fatalf("wallet value summary request mismatch: %+v", walletClient.lastValueSummary)
} }
if hostClient.lastHost == nil || hostClient.lastHost.GetUserId() != 42 || hostClient.lastBD == nil || hostClient.lastBD.GetUserId() != 42 || hostClient.last == nil || hostClient.last.GetUserId() != 42 { if walletClient.lastMyVip == nil || walletClient.lastMyVip.GetUserId() != 42 || walletClient.lastMyVip.GetAppCode() != "lalu" {
t.Fatalf("role requests mismatch: host=%+v bd=%+v coin=%+v", hostClient.lastHost, hostClient.lastBD, hostClient.last) t.Fatalf("vip request mismatch: %+v", walletClient.lastMyVip)
} }
if messageClient.lastTabs == nil || messageClient.lastTabs.GetUserId() != 42 || messageClient.lastTabs.GetMeta().GetRequestId() != "req-overview" { if hostClient.lastRoleSummary == nil || hostClient.lastRoleSummary.GetUserId() != 42 || hostClient.lastRoleSummary.GetMeta().GetAppCode() != "lalu" {
t.Fatalf("message tabs request mismatch: %+v", messageClient.lastTabs) t.Fatalf("role summary request mismatch: %+v", hostClient.lastRoleSummary)
}
if messageClient.lastTabs != nil {
t.Fatalf("my overview must not request message tabs: %+v", messageClient.lastTabs)
} }
profile := data["profile"].(map[string]any) profile := data["profile"].(map[string]any)
if profile["user_id"] != "42" || profile["username"] != "hy" || profile["region_id"] != float64(10) { if profile["user_id"] != "42" || profile["username"] != "hy" || profile["region_id"] != float64(10) {
t.Fatalf("profile data mismatch: %+v", profile) t.Fatalf("profile data mismatch: %+v", profile)
} }
stats := data["profile_stats"].(map[string]any)
if stats["visitors_count"] != float64(1234) || stats["following_count"] != float64(2345) || stats["friends_count"] != float64(3456) || stats["updated_at_ms"] != float64(7000) {
t.Fatalf("profile stats mismatch: %+v", stats)
}
roles := data["roles"].(map[string]any) roles := data["roles"].(map[string]any)
if roles["is_host"] != true || roles["is_agent"] != true || roles["is_bd"] != true || roles["is_bd_leader"] != true || roles["is_coin_seller"] != true { if roles["is_host"] != true || roles["is_agency"] != true || roles["is_bd"] != true || roles["is_bd_leader"] != true || roles["is_coin_seller"] != true || roles["agency_id"] != float64(7001) {
t.Fatalf("roles data mismatch: %+v", roles) t.Fatalf("roles data mismatch: %+v", roles)
} }
badges := data["badges"].(map[string]any)
if badges["unread_system_messages"] != float64(2) || badges["unread_activity_messages"] != float64(1) || badges["pending_role_invitations"] != float64(0) {
t.Fatalf("badges data mismatch: %+v", badges)
}
wallet := data["wallet"].(map[string]any) wallet := data["wallet"].(map[string]any)
if wallet["recharge_enabled"] != true || wallet["diamond_exchange_enabled"] != true || wallet["withdraw_enabled"] != true { if wallet["coin_amount"] != float64(12000) || wallet["updated_at_ms"] != float64(8000) {
t.Fatalf("wallet feature flags mismatch: %+v", wallet) t.Fatalf("wallet value summary mismatch: %+v", wallet)
} }
invite := data["invite"].(map[string]any) vip := data["vip"].(map[string]any)
if invite["my_invite_code"] != "A1B2C3" || invite["invite_enabled"] != true || invite["invite_count"] != float64(12) || invite["valid_invite_count"] != float64(3) || invite["valid_invite_threshold_coin"] != float64(80000) { if vip["level"] != float64(3) || vip["name"] != "VIP3" || vip["active"] != true {
t.Fatalf("invite overview mismatch: %+v", invite) t.Fatalf("vip summary mismatch: %+v", vip)
} }
balances := wallet["balances"].([]any) if _, ok := data["invite"]; ok {
if len(balances) != 3 { t.Fatalf("my overview must not include invite: %+v", data["invite"])
t.Fatalf("overview must return three fixed balances, got %+v", balances)
} }
if balanceAssetType(balances[0]) != "COIN" || balanceAvailable(balances[0]) != 12000 || if _, ok := data["badges"]; ok {
balanceAssetType(balances[1]) != "DIAMOND" || balanceAvailable(balances[1]) != 300 || t.Fatalf("my overview must not include message badges: %+v", data["badges"])
balanceAssetType(balances[2]) != "USD_BALANCE" || balanceAvailable(balances[2]) != 2500 {
t.Fatalf("balance projection mismatch: %+v", balances)
} }
entries := data["entries"].([]any) entries := data["entries"].([]any)
if !overviewEntryVisible(entries, "wallet") || if !overviewEntryVisible(entries, "store") ||
!overviewEntryVisible(entries, "bag") ||
!overviewEntryVisible(entries, "level") ||
!overviewEntryVisible(entries, "task") ||
!overviewEntryVisible(entries, "host_center") || !overviewEntryVisible(entries, "host_center") ||
!overviewEntryVisible(entries, "agency_center") || !overviewEntryVisible(entries, "agency_center") ||
!overviewEntryVisible(entries, "bd_center") || !overviewEntryVisible(entries, "bd_center") ||
!overviewEntryVisible(entries, "bd_leader_center") || !overviewEntryVisible(entries, "bd_leader_center") ||
!overviewEntryVisible(entries, "coin_seller_center") || !overviewEntryVisible(entries, "coin_seller_center") ||
!overviewEntryVisible(entries, "settings") ||
!overviewEntryVisible(entries, "language") ||
overviewEntryVisible(entries, "apply_host") { overviewEntryVisible(entries, "apply_host") {
t.Fatalf("entry visibility mismatch: %+v", entries) t.Fatalf("entry visibility mismatch: %+v", entries)
} }
@ -100,24 +118,21 @@ func TestGetMyOverviewComposesProfileWalletRolesBadgesAndEntries(t *testing.T) {
} }
} }
func TestGetMyOverviewFillsMissingWalletBalancesForOrdinaryUser(t *testing.T) { func TestGetMyOverviewProjectsOrdinaryUserEntriesAndZeroWallet(t *testing.T) {
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}) handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
handler.SetWalletClient(&fakeWalletClient{resp: &walletv1.GetBalancesResponse{}}) handler.SetWalletClient(&fakeWalletClient{})
handler.SetUserHostClient(&fakeUserHostClient{}) handler.SetUserHostClient(&fakeUserHostClient{})
handler.SetMessageInboxClient(&fakeMessageInboxClient{}) handler.SetMessageInboxClient(&fakeMessageInboxClient{})
data := performOverviewRequest(t, handler, "req-overview-empty-wallet") data := performOverviewRequest(t, handler, "req-overview-empty-wallet")
wallet := data["wallet"].(map[string]any) wallet := data["wallet"].(map[string]any)
balances := wallet["balances"].([]any) if wallet["coin_amount"] != float64(0) || wallet["updated_at_ms"] != float64(0) {
if len(balances) != 3 { t.Fatalf("missing wallet summary should be zero projection: %+v", wallet)
t.Fatalf("missing wallet rows must still project three balances: %+v", balances)
} }
for _, balance := range balances { stats := data["profile_stats"].(map[string]any)
item := balance.(map[string]any) if stats["visitors_count"] != float64(0) || stats["following_count"] != float64(0) || stats["friends_count"] != float64(0) {
if item["available_amount"] != float64(0) || item["frozen_amount"] != float64(0) || item["version"] != float64(0) { t.Fatalf("missing stats should be zero projection: %+v", stats)
t.Fatalf("missing balance should be zero projection: %+v", item)
}
} }
roles := data["roles"].(map[string]any) roles := data["roles"].(map[string]any)
if roles["is_host"] != false || roles["is_bd"] != false || roles["is_coin_seller"] != false { if roles["is_host"] != false || roles["is_bd"] != false || roles["is_coin_seller"] != false {
@ -130,25 +145,25 @@ func TestGetMyOverviewFillsMissingWalletBalancesForOrdinaryUser(t *testing.T) {
} }
func TestGetMyOverviewDegradesOptionalUpstreamFailures(t *testing.T) { func TestGetMyOverviewDegradesOptionalUpstreamFailures(t *testing.T) {
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}) handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{statsErr: errors.New("stats down")})
handler.SetWalletClient(&fakeWalletClient{err: errors.New("wallet down")}) handler.SetWalletClient(&fakeWalletClient{err: errors.New("wallet down")})
handler.SetUserHostClient(&fakeUserHostClient{hostErr: xerr.ToGRPCError(xerr.New(xerr.Internal, "role service down"))}) handler.SetUserHostClient(&fakeUserHostClient{roleSummaryErr: xerr.ToGRPCError(xerr.New(xerr.Internal, "role service down"))})
handler.SetMessageInboxClient(&fakeMessageInboxClient{err: errors.New("message down")}) handler.SetMessageInboxClient(&fakeMessageInboxClient{})
data := performOverviewRequest(t, handler, "req-overview-degraded") data := performOverviewRequest(t, handler, "req-overview-degraded")
wallet := data["wallet"].(map[string]any) wallet := data["wallet"].(map[string]any)
if wallet["unavailable"] != true || len(wallet["balances"].([]any)) != 0 { if wallet["unavailable"] != true || wallet["coin_amount"] != float64(0) {
t.Fatalf("wallet degradation mismatch: %+v", wallet) t.Fatalf("wallet degradation mismatch: %+v", wallet)
} }
stats := data["profile_stats"].(map[string]any)
if stats["unavailable"] != true || stats["visitors_count"] != float64(0) || stats["following_count"] != float64(0) || stats["friends_count"] != float64(0) {
t.Fatalf("profile stats degradation mismatch: %+v", stats)
}
roles := data["roles"].(map[string]any) roles := data["roles"].(map[string]any)
if roles["unavailable"] != true || roles["is_host"] != false || roles["is_bd"] != false || roles["is_coin_seller"] != false { if roles["unavailable"] != true || roles["is_host"] != false || roles["is_bd"] != false || roles["is_coin_seller"] != false {
t.Fatalf("roles degradation mismatch: %+v", roles) t.Fatalf("roles degradation mismatch: %+v", roles)
} }
badges := data["badges"].(map[string]any)
if badges["unavailable"] != true || badges["unread_system_messages"] != float64(0) || badges["unread_activity_messages"] != float64(0) {
t.Fatalf("badges degradation mismatch: %+v", badges)
}
entries := data["entries"].([]any) entries := data["entries"].([]any)
if overviewEntryVisible(entries, "host_center") || overviewEntryVisible(entries, "bd_center") || overviewEntryVisible(entries, "coin_seller_center") { if overviewEntryVisible(entries, "host_center") || overviewEntryVisible(entries, "bd_center") || overviewEntryVisible(entries, "coin_seller_center") {
t.Fatalf("degraded roles must not expose privileged entries: %+v", entries) t.Fatalf("degraded roles must not expose privileged entries: %+v", entries)
@ -161,7 +176,6 @@ func TestGetMyOverviewFailsWhenProfileFails(t *testing.T) {
}) })
handler.SetWalletClient(&fakeWalletClient{}) handler.SetWalletClient(&fakeWalletClient{})
handler.SetUserHostClient(&fakeUserHostClient{}) handler.SetUserHostClient(&fakeUserHostClient{})
handler.SetMessageInboxClient(&fakeMessageInboxClient{})
router := handler.Routes(auth.NewVerifier("secret")) router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/users/me/overview", nil) request := httptest.NewRequest(http.MethodGet, "/api/v1/users/me/overview", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
@ -208,15 +222,3 @@ func overviewEntryVisible(entries []any, key string) bool {
} }
return false return false
} }
func balanceAssetType(balance any) string {
item, _ := balance.(map[string]any)
value, _ := item["asset_type"].(string)
return value
}
func balanceAvailable(balance any) int64 {
item, _ := balance.(map[string]any)
value, _ := item["available_amount"].(float64)
return int64(value)
}

View File

@ -190,12 +190,15 @@ type fakeUserAuthClient struct {
type fakeUserProfileClient struct { type fakeUserProfileClient struct {
lastGet *userv1.GetUserRequest lastGet *userv1.GetUserRequest
lastStats *userv1.GetMyProfileStatsRequest
lastBatch *userv1.BatchGetUsersRequest lastBatch *userv1.BatchGetUsersRequest
getRequests []*userv1.GetUserRequest getRequests []*userv1.GetUserRequest
lastComplete *userv1.CompleteOnboardingRequest lastComplete *userv1.CompleteOnboardingRequest
lastUpdate *userv1.UpdateUserProfileRequest lastUpdate *userv1.UpdateUserProfileRequest
lastCountry *userv1.ChangeUserCountryRequest lastCountry *userv1.ChangeUserCountryRequest
getErr error getErr error
statsResp *userv1.GetMyProfileStatsResponse
statsErr error
completeErr error completeErr error
countryErr error countryErr error
regionID int64 regionID int64
@ -210,12 +213,18 @@ type fakeUserIdentityClient struct {
type fakeRoomQueryClient struct { type fakeRoomQueryClient struct {
lastList *roomv1.ListRoomsRequest lastList *roomv1.ListRoomsRequest
lastFeeds *roomv1.ListRoomFeedsRequest
lastMyRoom *roomv1.GetMyRoomRequest
lastCurrent *roomv1.GetCurrentRoomRequest lastCurrent *roomv1.GetCurrentRoomRequest
lastSnapshot *roomv1.GetRoomSnapshotRequest lastSnapshot *roomv1.GetRoomSnapshotRequest
resp *roomv1.ListRoomsResponse resp *roomv1.ListRoomsResponse
feedsResp *roomv1.ListRoomsResponse
myRoomResp *roomv1.GetMyRoomResponse
currentResp *roomv1.GetCurrentRoomResponse currentResp *roomv1.GetCurrentRoomResponse
snapshotResp *roomv1.GetRoomSnapshotResponse snapshotResp *roomv1.GetRoomSnapshotResponse
err error err error
feedsErr error
myRoomErr error
currentErr error currentErr error
snapshotErr error snapshotErr error
} }
@ -234,24 +243,48 @@ type fakeUserCountryQueryClient struct {
} }
type fakeUserHostClient struct { type fakeUserHostClient struct {
last *userv1.GetCoinSellerProfileRequest last *userv1.GetCoinSellerProfileRequest
profile *userv1.CoinSellerProfile profile *userv1.CoinSellerProfile
err error err error
lastHost *userv1.GetHostProfileRequest lastHost *userv1.GetHostProfileRequest
hostProfile *userv1.HostProfile hostProfile *userv1.HostProfile
hostErr error hostErr error
lastBD *userv1.GetBDProfileRequest lastBD *userv1.GetBDProfileRequest
bdProfile *userv1.BDProfile bdProfile *userv1.BDProfile
bdErr error bdErr error
lastRoleSummary *userv1.GetUserRoleSummaryRequest
roleSummary *userv1.UserRoleSummary
roleSummaryErr error
} }
type fakeWalletClient struct { type fakeWalletClient struct {
last *walletv1.GetBalancesRequest last *walletv1.GetBalancesRequest
resp *walletv1.GetBalancesResponse resp *walletv1.GetBalancesResponse
err error err error
lastTransfer *walletv1.TransferCoinFromSellerRequest lastOverview *walletv1.GetWalletOverviewRequest
transferResp *walletv1.TransferCoinFromSellerResponse overviewResp *walletv1.GetWalletOverviewResponse
transferErr error overviewErr error
lastValueSummary *walletv1.GetWalletValueSummaryRequest
valueSummaryResp *walletv1.GetWalletValueSummaryResponse
valueSummaryErr error
lastRechargeProducts *walletv1.ListRechargeProductsRequest
rechargeProductsResp *walletv1.ListRechargeProductsResponse
lastDiamondExchange *walletv1.GetDiamondExchangeConfigRequest
diamondExchangeResp *walletv1.GetDiamondExchangeConfigResponse
lastTransactions *walletv1.ListWalletTransactionsRequest
transactionsResp *walletv1.ListWalletTransactionsResponse
lastWithdrawal *walletv1.ApplyWithdrawalRequest
withdrawalResp *walletv1.ApplyWithdrawalResponse
lastVipPackages *walletv1.ListVipPackagesRequest
vipPackagesResp *walletv1.ListVipPackagesResponse
lastMyVip *walletv1.GetMyVipRequest
myVipResp *walletv1.GetMyVipResponse
vipErr error
lastPurchaseVip *walletv1.PurchaseVipRequest
purchaseVipResp *walletv1.PurchaseVipResponse
lastTransfer *walletv1.TransferCoinFromSellerRequest
transferResp *walletv1.TransferCoinFromSellerResponse
transferErr error
} }
type fakeMessageInboxClient struct { type fakeMessageInboxClient struct {
@ -372,6 +405,17 @@ func (f *fakeUserProfileClient) GetUser(_ context.Context, req *userv1.GetUserRe
}}, nil }}, nil
} }
func (f *fakeUserProfileClient) GetMyProfileStats(_ context.Context, req *userv1.GetMyProfileStatsRequest) (*userv1.GetMyProfileStatsResponse, error) {
f.lastStats = req
if f.statsErr != nil {
return nil, f.statsErr
}
if f.statsResp != nil {
return f.statsResp, nil
}
return &userv1.GetMyProfileStatsResponse{Stats: &userv1.UserProfileStats{UserId: req.GetUserId()}}, nil
}
func (f *fakeUserProfileClient) BatchGetUsers(_ context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) { func (f *fakeUserProfileClient) BatchGetUsers(_ context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) {
f.lastBatch = req f.lastBatch = req
users := make(map[int64]*userv1.User, len(req.GetUserIds())) users := make(map[int64]*userv1.User, len(req.GetUserIds()))
@ -504,6 +548,31 @@ func (f *fakeRoomQueryClient) ListRooms(_ context.Context, req *roomv1.ListRooms
return &roomv1.ListRoomsResponse{}, nil return &roomv1.ListRoomsResponse{}, nil
} }
func (f *fakeRoomQueryClient) ListRoomFeeds(_ context.Context, req *roomv1.ListRoomFeedsRequest) (*roomv1.ListRoomsResponse, error) {
f.lastFeeds = req
if f.feedsErr != nil {
return nil, f.feedsErr
}
if f.feedsResp != nil {
return f.feedsResp, nil
}
if f.resp != nil {
return f.resp, nil
}
return &roomv1.ListRoomsResponse{}, nil
}
func (f *fakeRoomQueryClient) GetMyRoom(_ context.Context, req *roomv1.GetMyRoomRequest) (*roomv1.GetMyRoomResponse, error) {
f.lastMyRoom = req
if f.myRoomErr != nil {
return nil, f.myRoomErr
}
if f.myRoomResp != nil {
return f.myRoomResp, nil
}
return &roomv1.GetMyRoomResponse{}, nil
}
func (f *fakeRoomQueryClient) GetCurrentRoom(_ context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) { func (f *fakeRoomQueryClient) GetCurrentRoom(_ context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) {
f.lastCurrent = req f.lastCurrent = req
if f.currentErr != nil { if f.currentErr != nil {
@ -675,6 +744,17 @@ func (f *fakeUserHostClient) GetCoinSellerProfile(_ context.Context, req *userv1
return &userv1.GetCoinSellerProfileResponse{}, nil return &userv1.GetCoinSellerProfileResponse{}, nil
} }
func (f *fakeUserHostClient) GetUserRoleSummary(_ context.Context, req *userv1.GetUserRoleSummaryRequest) (*userv1.GetUserRoleSummaryResponse, error) {
f.lastRoleSummary = req
if f.roleSummaryErr != nil {
return nil, f.roleSummaryErr
}
if f.roleSummary != nil {
return &userv1.GetUserRoleSummaryResponse{Summary: f.roleSummary}, nil
}
return &userv1.GetUserRoleSummaryResponse{Summary: &userv1.UserRoleSummary{UserId: req.GetUserId()}}, nil
}
func (f *fakeWalletClient) GetBalances(_ context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) { func (f *fakeWalletClient) GetBalances(_ context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
f.last = req f.last = req
if f.err != nil { if f.err != nil {
@ -687,6 +767,131 @@ func (f *fakeWalletClient) GetBalances(_ context.Context, req *walletv1.GetBalan
return &walletv1.GetBalancesResponse{}, nil return &walletv1.GetBalancesResponse{}, nil
} }
func (f *fakeWalletClient) GetWalletOverview(_ context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error) {
f.lastOverview = req
if f.overviewErr != nil {
return nil, f.overviewErr
}
if f.overviewResp != nil {
return f.overviewResp, nil
}
if f.err != nil {
return nil, f.err
}
balances := []*walletv1.AssetBalance(nil)
if f.resp != nil {
balances = f.resp.GetBalances()
}
return &walletv1.GetWalletOverviewResponse{
Balances: balances,
FeatureFlags: &walletv1.WalletFeatureFlags{
RechargeEnabled: true,
DiamondExchangeEnabled: true,
WithdrawEnabled: true,
},
}, nil
}
func (f *fakeWalletClient) GetWalletValueSummary(_ context.Context, req *walletv1.GetWalletValueSummaryRequest) (*walletv1.GetWalletValueSummaryResponse, error) {
f.lastValueSummary = req
if f.valueSummaryErr != nil {
return nil, f.valueSummaryErr
}
if f.valueSummaryResp != nil {
return f.valueSummaryResp, nil
}
if f.err != nil {
return nil, f.err
}
var coinAmount int64
if f.resp != nil {
for _, balance := range f.resp.GetBalances() {
if balance.GetAssetType() == "COIN" {
coinAmount = balance.GetAvailableAmount()
break
}
}
}
return &walletv1.GetWalletValueSummaryResponse{Summary: &walletv1.WalletValueSummary{CoinAmount: coinAmount}}, nil
}
func (f *fakeWalletClient) ListRechargeProducts(_ context.Context, req *walletv1.ListRechargeProductsRequest) (*walletv1.ListRechargeProductsResponse, error) {
f.lastRechargeProducts = req
if f.err != nil {
return nil, f.err
}
if f.rechargeProductsResp != nil {
return f.rechargeProductsResp, nil
}
return &walletv1.ListRechargeProductsResponse{}, nil
}
func (f *fakeWalletClient) GetDiamondExchangeConfig(_ context.Context, req *walletv1.GetDiamondExchangeConfigRequest) (*walletv1.GetDiamondExchangeConfigResponse, error) {
f.lastDiamondExchange = req
if f.err != nil {
return nil, f.err
}
if f.diamondExchangeResp != nil {
return f.diamondExchangeResp, nil
}
return &walletv1.GetDiamondExchangeConfigResponse{}, nil
}
func (f *fakeWalletClient) ListWalletTransactions(_ context.Context, req *walletv1.ListWalletTransactionsRequest) (*walletv1.ListWalletTransactionsResponse, error) {
f.lastTransactions = req
if f.err != nil {
return nil, f.err
}
if f.transactionsResp != nil {
return f.transactionsResp, nil
}
return &walletv1.ListWalletTransactionsResponse{}, nil
}
func (f *fakeWalletClient) ApplyWithdrawal(_ context.Context, req *walletv1.ApplyWithdrawalRequest) (*walletv1.ApplyWithdrawalResponse, error) {
f.lastWithdrawal = req
if f.err != nil {
return nil, f.err
}
if f.withdrawalResp != nil {
return f.withdrawalResp, nil
}
return &walletv1.ApplyWithdrawalResponse{}, nil
}
func (f *fakeWalletClient) ListVipPackages(_ context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error) {
f.lastVipPackages = req
if f.vipErr != nil {
return nil, f.vipErr
}
if f.vipPackagesResp != nil {
return f.vipPackagesResp, nil
}
return &walletv1.ListVipPackagesResponse{}, nil
}
func (f *fakeWalletClient) GetMyVip(_ context.Context, req *walletv1.GetMyVipRequest) (*walletv1.GetMyVipResponse, error) {
f.lastMyVip = req
if f.vipErr != nil {
return nil, f.vipErr
}
if f.myVipResp != nil {
return f.myVipResp, nil
}
return &walletv1.GetMyVipResponse{Vip: &walletv1.UserVip{UserId: req.GetUserId()}}, nil
}
func (f *fakeWalletClient) PurchaseVip(_ context.Context, req *walletv1.PurchaseVipRequest) (*walletv1.PurchaseVipResponse, error) {
f.lastPurchaseVip = req
if f.vipErr != nil {
return nil, f.vipErr
}
if f.purchaseVipResp != nil {
return f.purchaseVipResp, nil
}
return &walletv1.PurchaseVipResponse{}, nil
}
func (f *fakeWalletClient) TransferCoinFromSeller(_ context.Context, req *walletv1.TransferCoinFromSellerRequest) (*walletv1.TransferCoinFromSellerResponse, error) { func (f *fakeWalletClient) TransferCoinFromSeller(_ context.Context, req *walletv1.TransferCoinFromSellerRequest) (*walletv1.TransferCoinFromSellerResponse, error) {
f.lastTransfer = req f.lastTransfer = req
if f.transferErr != nil { if f.transferErr != nil {
@ -1097,7 +1302,7 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient) handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
handler.SetRoomQueryClient(queryClient) handler.SetRoomQueryClient(queryClient)
router := handler.Routes(auth.NewVerifier("secret")) router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms?tab=me&limit=2&cursor=cursor-1&q=room&visible_region_id=9999", nil) request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms?tab=hot&limit=2&cursor=cursor-1&q=room&visible_region_id=9999", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
request.Header.Set("X-Request-ID", "req-room-list") request.Header.Set("X-Request-ID", "req-room-list")
recorder := httptest.NewRecorder() recorder := httptest.NewRecorder()
@ -1116,7 +1321,7 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
if queryClient.lastList.GetViewerUserId() != 42 || queryClient.lastList.GetVisibleRegionId() != 1001 { if queryClient.lastList.GetViewerUserId() != 42 || queryClient.lastList.GetVisibleRegionId() != 1001 {
t.Fatalf("ListRooms must use authenticated user and server-side region: %+v", queryClient.lastList) t.Fatalf("ListRooms must use authenticated user and server-side region: %+v", queryClient.lastList)
} }
if queryClient.lastList.GetTab() != "me" || queryClient.lastList.GetLimit() != 2 || queryClient.lastList.GetCursor() != "cursor-1" || queryClient.lastList.GetQuery() != "room" { if queryClient.lastList.GetTab() != "hot" || queryClient.lastList.GetLimit() != 2 || queryClient.lastList.GetCursor() != "cursor-1" || queryClient.lastList.GetQuery() != "room" {
t.Fatalf("ListRooms query params were not propagated: %+v", queryClient.lastList) t.Fatalf("ListRooms query params were not propagated: %+v", queryClient.lastList)
} }
var response responseEnvelope var response responseEnvelope
@ -1131,6 +1336,98 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
} }
} }
func TestListRoomsRejectsMineFeedTabs(t *testing.T) {
for _, tab := range []string{"visited", "friend", "following", "me"} {
t.Run(tab, func(t *testing.T) {
queryClient := &fakeRoomQueryClient{}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})
handler.SetRoomQueryClient(queryClient)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms?tab="+tab+"&limit=3", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
}
if queryClient.lastList != nil || queryClient.lastFeeds != nil {
t.Fatalf("invalid public list tab must not reach room-service: list=%+v feeds=%+v", queryClient.lastList, queryClient.lastFeeds)
}
})
}
}
func TestListRoomFeedsSupportsMineFeedTabs(t *testing.T) {
for _, tab := range []string{"visited", "friend", "following"} {
t.Run(tab, func(t *testing.T) {
profileClient := &fakeUserProfileClient{regionID: 1001}
queryClient := &fakeRoomQueryClient{feedsResp: &roomv1.ListRoomsResponse{
Rooms: []*roomv1.RoomListItem{{RoomId: "room-feed", VisibleRegionId: 1001, Heat: 7}},
}}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
handler.SetRoomQueryClient(queryClient)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms/feeds?tab="+tab+"&limit=3", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
request.Header.Set("X-Request-ID", "req-room-feed-"+tab)
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.lastFeeds == nil || queryClient.lastFeeds.GetTab() != tab || queryClient.lastFeeds.GetViewerUserId() != 42 || queryClient.lastFeeds.GetVisibleRegionId() != 1001 {
t.Fatalf("feed tab was not forwarded with authenticated user and region: %+v", queryClient.lastFeeds)
}
})
}
}
func TestGetMyRoomUsesAuthenticatedOwnerAndReturnsIMGroup(t *testing.T) {
queryClient := &fakeRoomQueryClient{myRoomResp: &roomv1.GetMyRoomResponse{
HasRoom: true,
Room: &roomv1.RoomListItem{
RoomId: "room-owner",
OwnerUserId: 42,
Title: "Owner Room",
Status: "active",
SeatCount: 8,
},
ServerTimeMs: 1_700_000_000_123,
}}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
handler.SetRoomQueryClient(queryClient)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms/me", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
request.Header.Set("X-Request-ID", "req-my-room")
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.lastMyRoom == nil || queryClient.lastMyRoom.GetOwnerUserId() != 42 {
t.Fatalf("my room must use authenticated owner: %+v", queryClient.lastMyRoom)
}
var response responseEnvelope
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode my room response failed: %v", err)
}
data := response.Data.(map[string]any)
if data["has_room"] != true {
t.Fatalf("my room must preserve has_room=true: %+v", data)
}
room := data["room"].(map[string]any)
if room["room_id"] != "room-owner" || room["im_group_id"] != "room-owner" {
t.Fatalf("my room must expose room id and im group id: %+v", room)
}
}
func TestListRoomsRejectsInvalidLimitBeforeGRPC(t *testing.T) { func TestListRoomsRejectsInvalidLimitBeforeGRPC(t *testing.T) {
queryClient := &fakeRoomQueryClient{} queryClient := &fakeRoomQueryClient{}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001}) handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})

View File

@ -25,6 +25,11 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument") writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return return
} }
tab, ok := parsePublicRoomListTab(request.URL.Query().Get("tab"))
if !ok {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
viewerUserID := auth.UserIDFromContext(request.Context()) viewerUserID := auth.UserIDFromContext(request.Context())
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{ userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
@ -41,7 +46,7 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
Meta: meta(request, "", ""), Meta: meta(request, "", ""),
ViewerUserId: viewerUserID, ViewerUserId: viewerUserID,
VisibleRegionId: userResp.GetUser().GetRegionId(), VisibleRegionId: userResp.GetUser().GetRegionId(),
Tab: request.URL.Query().Get("tab"), Tab: tab,
Cursor: request.URL.Query().Get("cursor"), Cursor: request.URL.Query().Get("cursor"),
Limit: limit, Limit: limit,
Query: roomListQuery(request), Query: roomListQuery(request),
@ -53,6 +58,75 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
writeOK(writer, request, roomListDataFromProto(resp)) writeOK(writer, request, roomListDataFromProto(resp))
} }
// listRoomFeeds 查询 Mine 页 visited/friend/following 用户房间流。
func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Request) {
if h.userProfileClient == nil || h.roomQueryClient == nil {
// feed 同样需要 user-service 区域归属,避免跨区域露出用户关系流房间。
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
limit, ok := parseRoomListLimit(request.URL.Query().Get("limit"))
if !ok {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
tab, ok := parseRoomFeedTab(request.URL.Query().Get("tab"))
if !ok {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
viewerUserID := auth.UserIDFromContext(request.Context())
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: authRequestMeta(request, ""),
UserId: viewerUserID,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
resp, err := h.roomQueryClient.ListRoomFeeds(request.Context(), &roomv1.ListRoomFeedsRequest{
Meta: meta(request, "", ""),
ViewerUserId: viewerUserID,
VisibleRegionId: userResp.GetUser().GetRegionId(),
Tab: tab,
Cursor: request.URL.Query().Get("cursor"),
Limit: limit,
Query: roomListQuery(request),
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, roomListDataFromProto(resp))
}
func parsePublicRoomListTab(raw string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "", "hot":
return "hot", true
case "new":
return "new", true
default:
return "", false
}
}
func parseRoomFeedTab(raw string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "visited":
return "visited", true
case "friend":
return "friend", true
case "following":
return "following", true
default:
return "", false
}
}
func roomListQuery(request *http.Request) string { func roomListQuery(request *http.Request) string {
if query := strings.TrimSpace(request.URL.Query().Get("q")); query != "" { if query := strings.TrimSpace(request.URL.Query().Get("q")); query != "" {
return query return query
@ -77,6 +151,25 @@ func parseRoomListLimit(raw string) (int32, bool) {
return int32(value), true return int32(value), true
} }
// getMyRoom 返回 Mine 顶部“我的房间”卡片。
// 这个入口直接查 room-service 的 owner 权威数据,不依赖发现页 room_list_entries 投影是否命中。
func (h *Handler) getMyRoom(writer http.ResponseWriter, request *http.Request) {
if h.roomQueryClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
resp, err := h.roomQueryClient.GetMyRoom(request.Context(), &roomv1.GetMyRoomRequest{
Meta: meta(request, "", ""),
OwnerUserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, myRoomDataFromProto(resp))
}
// getCurrentRoom 查询当前登录用户是否仍有可恢复房间。 // getCurrentRoom 查询当前登录用户是否仍有可恢复房间。
// gateway 只传鉴权 user_idpresence 和麦位状态由 room-service 用读模型和 Room Cell 快照判断。 // gateway 只传鉴权 user_idpresence 和麦位状态由 room-service 用读模型和 Room Cell 快照判断。
func (h *Handler) getCurrentRoom(writer http.ResponseWriter, request *http.Request) { func (h *Handler) getCurrentRoom(writer http.ResponseWriter, request *http.Request) {

View File

@ -15,6 +15,12 @@ type roomListData struct {
NextCursor string `json:"next_cursor,omitempty"` NextCursor string `json:"next_cursor,omitempty"`
} }
type myRoomData struct {
HasRoom bool `json:"has_room"`
Room *roomListItemData `json:"room,omitempty"`
ServerTimeMS int64 `json:"server_time_ms"`
}
type roomListItemData struct { type roomListItemData struct {
RoomID string `json:"room_id"` RoomID string `json:"room_id"`
IMGroupID string `json:"im_group_id"` IMGroupID string `json:"im_group_id"`
@ -118,28 +124,47 @@ func roomListDataFromProto(resp *roomv1.ListRoomsResponse) roomListData {
} }
items := make([]roomListItemData, 0, len(resp.GetRooms())) items := make([]roomListItemData, 0, len(resp.GetRooms()))
for _, room := range resp.GetRooms() { for _, room := range resp.GetRooms() {
roomID := room.GetRoomId() items = append(items, roomListItemDataFromProto(room))
items = append(items, roomListItemData{
RoomID: roomID,
IMGroupID: roomIMGroupID(roomID),
OwnerUserID: formatOptionalUserID(room.GetOwnerUserId()),
HostUserID: formatOptionalUserID(room.GetHostUserId()),
Title: room.GetTitle(),
CoverURL: room.GetCoverUrl(),
Mode: room.GetMode(),
Status: room.GetStatus(),
Heat: room.GetHeat(),
OnlineCount: room.GetOnlineCount(),
SeatCount: room.GetSeatCount(),
OccupiedSeatCount: room.GetOccupiedSeatCount(),
VisibleRegionID: room.GetVisibleRegionId(),
AppCode: room.GetAppCode(),
RoomShortID: room.GetRoomShortId(),
})
} }
return roomListData{Rooms: items, NextCursor: resp.GetNextCursor()} return roomListData{Rooms: items, NextCursor: resp.GetNextCursor()}
} }
func myRoomDataFromProto(resp *roomv1.GetMyRoomResponse) myRoomData {
if resp == nil {
return myRoomData{}
}
data := myRoomData{HasRoom: resp.GetHasRoom(), ServerTimeMS: resp.GetServerTimeMs()}
if resp.GetHasRoom() && resp.GetRoom() != nil {
room := roomListItemDataFromProto(resp.GetRoom())
data.Room = &room
}
return data
}
func roomListItemDataFromProto(room *roomv1.RoomListItem) roomListItemData {
if room == nil {
return roomListItemData{}
}
roomID := room.GetRoomId()
return roomListItemData{
RoomID: roomID,
IMGroupID: roomIMGroupID(roomID),
OwnerUserID: formatOptionalUserID(room.GetOwnerUserId()),
HostUserID: formatOptionalUserID(room.GetHostUserId()),
Title: room.GetTitle(),
CoverURL: room.GetCoverUrl(),
Mode: room.GetMode(),
Status: room.GetStatus(),
Heat: room.GetHeat(),
OnlineCount: room.GetOnlineCount(),
SeatCount: room.GetSeatCount(),
OccupiedSeatCount: room.GetOccupiedSeatCount(),
VisibleRegionID: room.GetVisibleRegionId(),
AppCode: room.GetAppCode(),
RoomShortID: room.GetRoomShortId(),
}
}
func commandResultDataFromProto(result *roomv1.CommandResult) roomCommandResultData { func commandResultDataFromProto(result *roomv1.CommandResult) roomCommandResultData {
if result == nil { if result == nil {
return roomCommandResultData{} return roomCommandResultData{}

View File

@ -36,17 +36,25 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
mux.Handle(apiV1Prefix+"/users/by-display-user-id/", h.publicAPIHandler(h.resolveDisplayUserID)) mux.Handle(apiV1Prefix+"/users/by-display-user-id/", h.publicAPIHandler(h.resolveDisplayUserID))
mux.Handle(apiV1Prefix+"/users/profiles:batch", h.profileAPIHandler(jwtVerifier, h.batchUserProfiles)) mux.Handle(apiV1Prefix+"/users/profiles:batch", h.profileAPIHandler(jwtVerifier, h.batchUserProfiles))
mux.Handle(apiV1Prefix+"/users/me/overview", h.profileAPIHandler(jwtVerifier, h.getMyOverview)) mux.Handle(apiV1Prefix+"/users/me/overview", h.profileAPIHandler(jwtVerifier, h.getMyOverview))
mux.Handle(apiV1Prefix+"/users/me", h.profileAPIHandler(jwtVerifier, h.getMyProfile))
mux.Handle(apiV1Prefix+"/users/me/identity", h.apiHandler(jwtVerifier, h.getMyIdentity)) mux.Handle(apiV1Prefix+"/users/me/identity", h.apiHandler(jwtVerifier, h.getMyIdentity))
mux.Handle(apiV1Prefix+"/users/me/host-identity", h.profileAPIHandler(jwtVerifier, h.getMyHostIdentity)) mux.Handle(apiV1Prefix+"/users/me/host-identity", h.profileAPIHandler(jwtVerifier, h.getMyHostIdentity))
mux.Handle(apiV1Prefix+"/users/me/role-summary", h.profileAPIHandler(jwtVerifier, h.getMyRoleSummary))
mux.Handle(apiV1Prefix+"/users/me/onboarding/complete", h.apiHandler(jwtVerifier, h.completeMyOnboarding)) mux.Handle(apiV1Prefix+"/users/me/onboarding/complete", h.apiHandler(jwtVerifier, h.completeMyOnboarding))
mux.Handle(apiV1Prefix+"/users/me/profile/update", h.profileAPIHandler(jwtVerifier, h.updateMyProfile)) mux.Handle(apiV1Prefix+"/users/me/profile/update", h.profileAPIHandler(jwtVerifier, h.updateMyProfile))
mux.Handle(apiV1Prefix+"/users/me/country/change", h.profileAPIHandler(jwtVerifier, h.changeMyCountry)) mux.Handle(apiV1Prefix+"/users/me/country/change", h.profileAPIHandler(jwtVerifier, h.changeMyCountry))
mux.Handle(apiV1Prefix+"/users/me/display-id/change", h.apiHandler(jwtVerifier, h.changeMyDisplayUserID)) mux.Handle(apiV1Prefix+"/users/me/display-id/change", h.apiHandler(jwtVerifier, h.changeMyDisplayUserID))
mux.Handle(apiV1Prefix+"/users/me/display-id/pretty/apply", h.apiHandler(jwtVerifier, h.applyMyPrettyDisplayUserID)) mux.Handle(apiV1Prefix+"/users/me/display-id/pretty/apply", h.apiHandler(jwtVerifier, h.applyMyPrettyDisplayUserID))
mux.Handle(apiV1Prefix+"/users/me/visitors", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listMyProfileVisitors)))
mux.Handle(apiV1Prefix+"/users/me/following", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listMyFollowing)))
mux.Handle(apiV1Prefix+"/users/me/friends", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listMyFriends)))
mux.Handle(apiV1Prefix+"/users/me/friend-requests", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listMyFriendApplications)))
mux.Handle(apiV1Prefix+"/users/me", h.profileAPIHandler(jwtVerifier, h.getMyProfile))
mux.Handle(apiV1Prefix+"/users/me/resources", h.profileAPIHandler(jwtVerifier, h.listMyResources)) mux.Handle(apiV1Prefix+"/users/me/resources", h.profileAPIHandler(jwtVerifier, h.listMyResources))
mux.Handle(apiV1Prefix+"/users/me/resources/", h.profileAPIHandler(jwtVerifier, h.equipMyResource)) mux.Handle(apiV1Prefix+"/users/me/resources/", h.profileAPIHandler(jwtVerifier, h.equipMyResource))
mux.Handle(apiV1Prefix+"/users/", h.profileAPIHandler(jwtVerifier, h.userSocialAction))
mux.Handle(apiV1Prefix+"/rooms/me", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getMyRoom)))
mux.Handle(apiV1Prefix+"/rooms/feeds", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listRoomFeeds)))
mux.Handle(apiV1Prefix+"/rooms", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listRooms))) mux.Handle(apiV1Prefix+"/rooms", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listRooms)))
mux.Handle(apiV1Prefix+"/rooms/current", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getCurrentRoom))) mux.Handle(apiV1Prefix+"/rooms/current", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getCurrentRoom)))
mux.Handle(apiV1Prefix+"/rooms/snapshot", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getRoomSnapshot))) mux.Handle(apiV1Prefix+"/rooms/snapshot", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getRoomSnapshot)))
@ -73,8 +81,16 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
mux.Handle(apiV1Prefix+"/messages", h.profileAPIHandler(jwtVerifier, h.listInboxMessages)) mux.Handle(apiV1Prefix+"/messages", h.profileAPIHandler(jwtVerifier, h.listInboxMessages))
mux.Handle(apiV1Prefix+"/tasks/tabs", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listTaskTabs))) mux.Handle(apiV1Prefix+"/tasks/tabs", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listTaskTabs)))
mux.Handle(apiV1Prefix+"/tasks/claim", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.claimTaskReward))) mux.Handle(apiV1Prefix+"/tasks/claim", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.claimTaskReward)))
mux.Handle(apiV1Prefix+"/wallet/me/overview", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getWalletOverview)))
mux.Handle(apiV1Prefix+"/wallet/me/balances", h.profileAPIHandler(jwtVerifier, h.getMyBalances)) mux.Handle(apiV1Prefix+"/wallet/me/balances", h.profileAPIHandler(jwtVerifier, h.getMyBalances))
mux.Handle(apiV1Prefix+"/wallet/recharge/products", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listRechargeProducts)))
mux.Handle(apiV1Prefix+"/wallet/diamond-exchange/config", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getDiamondExchangeConfig)))
mux.Handle(apiV1Prefix+"/wallet/withdrawals/apply", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.applyWithdrawal)))
mux.Handle(apiV1Prefix+"/wallet/transactions", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listWalletTransactions)))
mux.Handle(apiV1Prefix+"/wallet/coin-seller/transfer", h.profileAPIHandler(jwtVerifier, h.transferCoinFromSeller)) mux.Handle(apiV1Prefix+"/wallet/coin-seller/transfer", h.profileAPIHandler(jwtVerifier, h.transferCoinFromSeller))
mux.Handle(apiV1Prefix+"/vip/me", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getMyVIP)))
mux.Handle(apiV1Prefix+"/vip/packages", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listVIPPackages)))
mux.Handle(apiV1Prefix+"/vip/purchase", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.purchaseVIP)))
return mux return mux
} }

View File

@ -0,0 +1,352 @@
package http
import (
"net/http"
"strconv"
"strings"
userv1 "hyapp.local/api/proto/user/v1"
"hyapp/services/gateway-service/internal/auth"
)
type profileVisitRecordData struct {
VisitorUserID string `json:"visitor_user_id"`
TargetUserID string `json:"target_user_id"`
VisitCount int64 `json:"visit_count"`
LastVisitedAtMS int64 `json:"last_visited_at_ms"`
}
type followRecordData struct {
FollowerUserID string `json:"follower_user_id"`
FolloweeUserID string `json:"followee_user_id"`
FollowedAtMS int64 `json:"followed_at_ms"`
}
type friendRecordData struct {
UserID string `json:"user_id"`
FriendUserID string `json:"friend_user_id"`
FriendedAtMS int64 `json:"friended_at_ms"`
}
type friendApplicationData struct {
RequesterUserID string `json:"requester_user_id"`
TargetUserID string `json:"target_user_id"`
Status string `json:"status"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
// listMyProfileVisitors 返回访问我的主页的人;这是关系 read model不实时扫用户行为日志。
func (h *Handler) listMyProfileVisitors(writer http.ResponseWriter, request *http.Request) {
if h.userSocialClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
page, pageSize, ok := socialPage(request)
if !ok {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
resp, err := h.userSocialClient.ListProfileVisitors(request.Context(), &userv1.ListProfileVisitorsRequest{
Meta: authRequestMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
Page: page,
PageSize: pageSize,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
records := make([]profileVisitRecordData, 0, len(resp.GetRecords()))
for _, record := range resp.GetRecords() {
records = append(records, profileVisitData(record))
}
writeOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()})
}
// listMyFollowing 返回我关注的用户列表;关注我的列表后续单独建接口,避免语义混淆。
func (h *Handler) listMyFollowing(writer http.ResponseWriter, request *http.Request) {
if h.userSocialClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
page, pageSize, ok := socialPage(request)
if !ok {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
resp, err := h.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{
Meta: authRequestMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
Page: page,
PageSize: pageSize,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
records := make([]followRecordData, 0, len(resp.GetRecords()))
for _, record := range resp.GetRecords() {
records = append(records, followData(record))
}
writeOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()})
}
// listMyFriends 返回双向好友列表。
func (h *Handler) listMyFriends(writer http.ResponseWriter, request *http.Request) {
if h.userSocialClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
page, pageSize, ok := socialPage(request)
if !ok {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
resp, err := h.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{
Meta: authRequestMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
Page: page,
PageSize: pageSize,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
records := make([]friendRecordData, 0, len(resp.GetRecords()))
for _, record := range resp.GetRecords() {
records = append(records, friendData(record))
}
writeOK(writer, request, map[string]any{"records": records, "total": resp.GetTotal()})
}
// listMyFriendApplications 返回待处理申请direction=incoming/outgoing。
func (h *Handler) listMyFriendApplications(writer http.ResponseWriter, request *http.Request) {
if h.userSocialClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
page, pageSize, ok := socialPage(request)
if !ok {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
resp, err := h.userSocialClient.ListFriendApplications(request.Context(), &userv1.ListFriendApplicationsRequest{
Meta: authRequestMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
Direction: strings.TrimSpace(request.URL.Query().Get("direction")),
Page: page,
PageSize: pageSize,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
applications := make([]friendApplicationData, 0, len(resp.GetApplications()))
for _, application := range resp.GetApplications() {
applications = append(applications, friendApplicationDataFromProto(application))
}
writeOK(writer, request, map[string]any{"applications": applications, "total": resp.GetTotal()})
}
// userSocialAction 处理 /users/{user_id}/visit|follow|friend-requests|friend-requests/accept|friend。
func (h *Handler) userSocialAction(writer http.ResponseWriter, request *http.Request) {
if h.userSocialClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
targetUserID, action, ok := parseUserSocialActionPath(request.URL.Path)
if !ok {
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
return
}
switch action {
case "visit":
h.recordProfileVisit(writer, request, targetUserID)
case "follow":
h.handleFollowAction(writer, request, targetUserID)
case "friend-requests":
h.applyFriend(writer, request, targetUserID)
case "friend-requests/accept":
h.acceptFriendApplication(writer, request, targetUserID)
case "friend":
h.deleteFriend(writer, request, targetUserID)
default:
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
}
}
func (h *Handler) recordProfileVisit(writer http.ResponseWriter, request *http.Request, targetUserID int64) {
if request.Method != http.MethodPost {
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
return
}
resp, err := h.userSocialClient.RecordProfileVisit(request.Context(), &userv1.RecordProfileVisitRequest{
Meta: authRequestMeta(request, ""),
VisitorUserId: auth.UserIDFromContext(request.Context()),
TargetUserId: targetUserID,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, map[string]any{"recorded": resp.GetRecorded(), "target_stats": overviewStatsFromProto(resp.GetTargetStats())})
}
func (h *Handler) handleFollowAction(writer http.ResponseWriter, request *http.Request, followeeUserID int64) {
userID := auth.UserIDFromContext(request.Context())
switch request.Method {
case http.MethodPost:
resp, err := h.userSocialClient.FollowUser(request.Context(), &userv1.FollowUserRequest{
Meta: authRequestMeta(request, ""),
FollowerUserId: userID,
FolloweeUserId: followeeUserID,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
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, ""),
FollowerUserId: userID,
FolloweeUserId: followeeUserID,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, map[string]any{"following": resp.GetFollowing(), "follower_stats": overviewStatsFromProto(resp.GetFollowerStats())})
default:
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
}
}
func (h *Handler) applyFriend(writer http.ResponseWriter, request *http.Request, targetUserID int64) {
if request.Method != http.MethodPost {
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
return
}
resp, err := h.userSocialClient.ApplyFriend(request.Context(), &userv1.ApplyFriendRequest{
Meta: authRequestMeta(request, ""),
RequesterUserId: auth.UserIDFromContext(request.Context()),
TargetUserId: targetUserID,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, map[string]any{"application": friendApplicationDataFromProto(resp.GetApplication()), "already_friends": resp.GetAlreadyFriends()})
}
func (h *Handler) acceptFriendApplication(writer http.ResponseWriter, request *http.Request, requesterUserID int64) {
if request.Method != http.MethodPost {
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
return
}
resp, err := h.userSocialClient.AcceptFriendApplication(request.Context(), &userv1.AcceptFriendApplicationRequest{
Meta: authRequestMeta(request, ""),
AccepterUserId: auth.UserIDFromContext(request.Context()),
RequesterUserId: requesterUserID,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, map[string]any{"friend": friendData(resp.GetFriend())})
}
func (h *Handler) deleteFriend(writer http.ResponseWriter, request *http.Request, friendUserID int64) {
if request.Method != http.MethodDelete {
writeError(writer, request, http.StatusNotFound, codeNotFound, "not found")
return
}
resp, err := h.userSocialClient.DeleteFriend(request.Context(), &userv1.DeleteFriendRequest{
Meta: authRequestMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
FriendUserId: friendUserID,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, map[string]any{"deleted": resp.GetDeleted()})
}
func socialPage(request *http.Request) (int32, int32, bool) {
page, ok := parsePositiveInt32Query(request, "page", 1)
if !ok {
return 0, 0, false
}
pageSize, ok := parsePositiveInt32Query(request, "page_size", 20)
if !ok {
return 0, 0, false
}
return page, pageSize, true
}
func parseUserSocialActionPath(path string) (int64, string, bool) {
if !strings.HasPrefix(path, apiV1Prefix+"/users/") {
return 0, "", false
}
raw := strings.Trim(strings.TrimPrefix(path, apiV1Prefix+"/users/"), "/")
parts := strings.Split(raw, "/")
if len(parts) < 2 || len(parts) > 3 || parts[0] == "me" {
return 0, "", false
}
userID, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil || userID <= 0 {
return 0, "", false
}
action := strings.Join(parts[1:], "/")
return userID, action, true
}
func profileVisitData(record *userv1.ProfileVisitRecord) profileVisitRecordData {
if record == nil {
return profileVisitRecordData{}
}
return profileVisitRecordData{
VisitorUserID: userIDString(record.GetVisitorUserId()),
TargetUserID: userIDString(record.GetTargetUserId()),
VisitCount: record.GetVisitCount(),
LastVisitedAtMS: record.GetLastVisitedAtMs(),
}
}
func followData(record *userv1.FollowRecord) followRecordData {
if record == nil {
return followRecordData{}
}
return followRecordData{
FollowerUserID: userIDString(record.GetFollowerUserId()),
FolloweeUserID: userIDString(record.GetFolloweeUserId()),
FollowedAtMS: record.GetFollowedAtMs(),
}
}
func friendData(record *userv1.FriendRecord) friendRecordData {
if record == nil {
return friendRecordData{}
}
return friendRecordData{
UserID: userIDString(record.GetUserId()),
FriendUserID: userIDString(record.GetFriendUserId()),
FriendedAtMS: record.GetFriendedAtMs(),
}
}
func friendApplicationDataFromProto(application *userv1.FriendApplication) friendApplicationData {
if application == nil {
return friendApplicationData{}
}
return friendApplicationData{
RequesterUserID: userIDString(application.GetRequesterUserId()),
TargetUserID: userIDString(application.GetTargetUserId()),
Status: application.GetStatus(),
CreatedAtMS: application.GetCreatedAtMs(),
UpdatedAtMS: application.GetUpdatedAtMs(),
}
}

View File

@ -0,0 +1,126 @@
package http
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
userv1 "hyapp.local/api/proto/user/v1"
"hyapp/services/gateway-service/internal/auth"
)
type fakeUserSocialClient struct {
lastVisit *userv1.RecordProfileVisitRequest
lastVisitors *userv1.ListProfileVisitorsRequest
lastFollow *userv1.FollowUserRequest
lastUnfollow *userv1.UnfollowUserRequest
lastFollowing *userv1.ListFollowingRequest
lastApply *userv1.ApplyFriendRequest
lastAccept *userv1.AcceptFriendApplicationRequest
lastDelete *userv1.DeleteFriendRequest
lastFriends *userv1.ListFriendsRequest
lastFriendApps *userv1.ListFriendApplicationsRequest
}
func (f *fakeUserSocialClient) RecordProfileVisit(_ context.Context, req *userv1.RecordProfileVisitRequest) (*userv1.RecordProfileVisitResponse, error) {
f.lastVisit = req
return &userv1.RecordProfileVisitResponse{Recorded: true, TargetStats: &userv1.UserProfileStats{UserId: req.GetTargetUserId(), VisitorsCount: 1}}, nil
}
func (f *fakeUserSocialClient) ListProfileVisitors(_ context.Context, req *userv1.ListProfileVisitorsRequest) (*userv1.ListProfileVisitorsResponse, error) {
f.lastVisitors = req
return &userv1.ListProfileVisitorsResponse{Records: []*userv1.ProfileVisitRecord{{VisitorUserId: 10002, TargetUserId: req.GetUserId(), VisitCount: 2, LastVisitedAtMs: 7000}}, Total: 1}, nil
}
func (f *fakeUserSocialClient) FollowUser(_ context.Context, req *userv1.FollowUserRequest) (*userv1.FollowUserResponse, error) {
f.lastFollow = req
return &userv1.FollowUserResponse{Following: true, FollowerStats: &userv1.UserProfileStats{UserId: req.GetFollowerUserId(), FollowingCount: 1}}, nil
}
func (f *fakeUserSocialClient) UnfollowUser(_ context.Context, req *userv1.UnfollowUserRequest) (*userv1.UnfollowUserResponse, error) {
f.lastUnfollow = req
return &userv1.UnfollowUserResponse{Following: false, FollowerStats: &userv1.UserProfileStats{UserId: req.GetFollowerUserId()}}, nil
}
func (f *fakeUserSocialClient) ListFollowing(_ context.Context, req *userv1.ListFollowingRequest) (*userv1.ListFollowingResponse, error) {
f.lastFollowing = req
return &userv1.ListFollowingResponse{Records: []*userv1.FollowRecord{{FollowerUserId: req.GetUserId(), FolloweeUserId: 10002, FollowedAtMs: 8000}}, Total: 1}, nil
}
func (f *fakeUserSocialClient) ApplyFriend(_ context.Context, req *userv1.ApplyFriendRequest) (*userv1.ApplyFriendResponse, error) {
f.lastApply = req
return &userv1.ApplyFriendResponse{Application: &userv1.FriendApplication{RequesterUserId: req.GetRequesterUserId(), TargetUserId: req.GetTargetUserId(), Status: "pending", CreatedAtMs: 9000, UpdatedAtMs: 9000}}, nil
}
func (f *fakeUserSocialClient) AcceptFriendApplication(_ context.Context, req *userv1.AcceptFriendApplicationRequest) (*userv1.AcceptFriendApplicationResponse, error) {
f.lastAccept = req
return &userv1.AcceptFriendApplicationResponse{Friend: &userv1.FriendRecord{UserId: req.GetAccepterUserId(), FriendUserId: req.GetRequesterUserId(), FriendedAtMs: 10000}}, nil
}
func (f *fakeUserSocialClient) DeleteFriend(_ context.Context, req *userv1.DeleteFriendRequest) (*userv1.DeleteFriendResponse, error) {
f.lastDelete = req
return &userv1.DeleteFriendResponse{Deleted: true}, nil
}
func (f *fakeUserSocialClient) ListFriends(_ context.Context, req *userv1.ListFriendsRequest) (*userv1.ListFriendsResponse, error) {
f.lastFriends = req
return &userv1.ListFriendsResponse{Records: []*userv1.FriendRecord{{UserId: req.GetUserId(), FriendUserId: 10002, FriendedAtMs: 10000}}, Total: 1}, nil
}
func (f *fakeUserSocialClient) ListFriendApplications(_ context.Context, req *userv1.ListFriendApplicationsRequest) (*userv1.ListFriendApplicationsResponse, error) {
f.lastFriendApps = req
return &userv1.ListFriendApplicationsResponse{Applications: []*userv1.FriendApplication{{RequesterUserId: 10002, TargetUserId: req.GetUserId(), Status: "pending", CreatedAtMs: 9000, UpdatedAtMs: 9000}}, Total: 1}, nil
}
func TestFriendApplicationFlowRoutesUseBackendThenIMOwnedReminder(t *testing.T) {
socialClient := &fakeUserSocialClient{}
handler := NewHandler(&fakeRoomClient{})
handler.SetUserSocialClient(socialClient)
router := handler.Routes(auth.NewVerifier("secret"))
applyReq := httptest.NewRequest(http.MethodPost, "/api/v1/users/10002/friend-requests", nil)
applyReq.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 10001))
applyResp := httptest.NewRecorder()
router.ServeHTTP(applyResp, applyReq)
if applyResp.Code != http.StatusOK {
t.Fatalf("apply friend status = %d body=%s", applyResp.Code, applyResp.Body.String())
}
if socialClient.lastApply == nil || socialClient.lastApply.GetRequesterUserId() != 10001 || socialClient.lastApply.GetTargetUserId() != 10002 {
t.Fatalf("apply friend request mismatch: %+v", socialClient.lastApply)
}
acceptReq := httptest.NewRequest(http.MethodPost, "/api/v1/users/10001/friend-requests/accept", nil)
acceptReq.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 10002))
acceptResp := httptest.NewRecorder()
router.ServeHTTP(acceptResp, acceptReq)
if acceptResp.Code != http.StatusOK {
t.Fatalf("accept friend status = %d body=%s", acceptResp.Code, acceptResp.Body.String())
}
if socialClient.lastAccept == nil || socialClient.lastAccept.GetAccepterUserId() != 10002 || socialClient.lastAccept.GetRequesterUserId() != 10001 {
t.Fatalf("accept friend request mismatch: %+v", socialClient.lastAccept)
}
}
func TestSocialListRoutesPassPagingAndDirection(t *testing.T) {
socialClient := &fakeUserSocialClient{}
handler := NewHandler(&fakeRoomClient{})
handler.SetUserSocialClient(socialClient)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/users/me/friend-requests?direction=outgoing&page=2&page_size=30", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 10001))
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("list friend applications status = %d body=%s", response.Code, response.Body.String())
}
if socialClient.lastFriendApps == nil || socialClient.lastFriendApps.GetUserId() != 10001 || socialClient.lastFriendApps.GetDirection() != "outgoing" || socialClient.lastFriendApps.GetPage() != 2 || socialClient.lastFriendApps.GetPageSize() != 30 {
t.Fatalf("friend application list request mismatch: %+v", socialClient.lastFriendApps)
}
var envelope responseEnvelope
if err := json.Unmarshal(response.Body.Bytes(), &envelope); err != nil || envelope.Code != codeOK {
t.Fatalf("invalid envelope: %+v err=%v", envelope, err)
}
}

View File

@ -0,0 +1,136 @@
package http
import (
"net/http"
"strings"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
"hyapp/services/gateway-service/internal/auth"
)
// getMyVIP 返回当前用户 VIP 状态。
func (h *Handler) getMyVIP(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
resp, err := h.walletClient.GetMyVip(request.Context(), &walletv1.GetMyVipRequest{
RequestId: requestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
UserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, vipFromProto(resp.GetVip()))
}
// listVIPPackages 返回可购买 VIP 包和当前会员状态。
func (h *Handler) listVIPPackages(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
resp, err := h.walletClient.ListVipPackages(request.Context(), &walletv1.ListVipPackagesRequest{
RequestId: requestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
UserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
writeRPCError(writer, request, err)
return
}
packages := make([]vipPackageData, 0, len(resp.GetPackages()))
for _, item := range resp.GetPackages() {
packages = append(packages, vipPackageFromProto(item))
}
writeOK(writer, request, map[string]any{"current_vip": vipFromProto(resp.GetCurrentVip()), "packages": packages})
}
// purchaseVIP 购买、续期或升级 VIP。
func (h *Handler) purchaseVIP(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
var body vipPurchaseRequestBody
if !decode(writer, request, &body) {
return
}
commandID := strings.TrimSpace(body.CommandID)
if commandID == "" {
commandID = strings.TrimSpace(body.CommandIDAlt)
}
if commandID == "" || body.Level <= 0 {
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
resp, err := h.walletClient.PurchaseVip(request.Context(), &walletv1.PurchaseVipRequest{
CommandId: commandID,
AppCode: appcode.FromContext(request.Context()),
UserId: auth.UserIDFromContext(request.Context()),
Level: body.Level,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, map[string]any{
"order_id": resp.GetOrderId(),
"transaction_id": resp.GetTransactionId(),
"vip": vipFromProto(resp.GetVip()),
"coin_spent": resp.GetCoinSpent(),
"coin_balance_after": resp.GetCoinBalanceAfter(),
"reward_items": vipRewardItemsFromProto(resp.GetRewardItems()),
})
}
func vipFromProto(vip *walletv1.UserVip) overviewVIPData {
if vip == nil {
return overviewVIPData{}
}
return overviewVIPData{
Level: vip.GetLevel(),
Name: vip.GetName(),
Active: vip.GetActive(),
StartedAtMS: vip.GetStartedAtMs(),
ExpiresAtMS: vip.GetExpiresAtMs(),
}
}
func vipPackageFromProto(item *walletv1.VipLevel) vipPackageData {
if item == nil {
return vipPackageData{}
}
return vipPackageData{
Level: item.GetLevel(),
Name: item.GetName(),
Status: item.GetStatus(),
PriceCoin: item.GetPriceCoin(),
DurationMS: item.GetDurationMs(),
RewardResourceGroupID: item.GetRewardResourceGroupId(),
RewardItems: vipRewardItemsFromProto(item.GetRewardItems()),
CanPurchase: item.GetCanPurchase(),
SortOrder: item.GetSortOrder(),
}
}
func vipRewardItemsFromProto(items []*walletv1.VipRewardItem) []vipRewardItemData {
resp := make([]vipRewardItemData, 0, len(items))
for _, item := range items {
if item == nil {
continue
}
resp = append(resp, vipRewardItemData{
ResourceID: item.GetResourceId(),
ResourceCode: item.GetResourceCode(),
ResourceType: item.GetResourceType(),
Name: item.GetName(),
Quantity: item.GetQuantity(),
ExpiresAtMS: item.GetExpiresAtMs(),
})
}
return resp
}

View File

@ -109,3 +109,14 @@ CREATE TABLE IF NOT EXISTS room_outbox (
KEY idx_room_outbox_pending (app_code, status, created_at_ms), KEY idx_room_outbox_pending (app_code, status, created_at_ms),
KEY idx_room_outbox_claim (app_code, status, lock_until_ms, created_at_ms) KEY idx_room_outbox_claim (app_code, status, lock_until_ms, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS room_user_feed_entries (
app_code VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
feed_type VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, user_id, feed_type, room_id),
KEY idx_room_user_feed_list (app_code, user_id, feed_type, updated_at_ms DESC, room_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -16,7 +16,9 @@ import (
const ( const (
roomListTabHot = "hot" roomListTabHot = "hot"
roomListTabNew = "new" roomListTabNew = "new"
roomListTabMe = "me" roomListTabVisited = "visited"
roomListTabFriend = "friend"
roomListTabFollowing = "following"
defaultRoomListLimit = 20 defaultRoomListLimit = 20
maxRoomListLimit = 50 maxRoomListLimit = 50
maxRoomListQueryRunes = 64 maxRoomListQueryRunes = 64
@ -29,10 +31,11 @@ type roomListCursor struct {
Query string `json:"query,omitempty"` Query string `json:"query,omitempty"`
SortScore int64 `json:"sort_score,omitempty"` SortScore int64 `json:"sort_score,omitempty"`
CreatedAtMS int64 `json:"created_at_ms,omitempty"` CreatedAtMS int64 `json:"created_at_ms,omitempty"`
UpdatedAtMS int64 `json:"updated_at_ms,omitempty"`
RoomID string `json:"room_id"` RoomID string `json:"room_id"`
} }
// ListRooms 查询当前用户区域内的房间列表读模型。 // ListRooms 查询当前用户区域内的公共房间发现列表读模型。
func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) { func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
viewerUserID := req.GetViewerUserId() viewerUserID := req.GetViewerUserId()
@ -55,16 +58,11 @@ func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
ownerUserID := int64(0)
if tab == roomListTabMe {
ownerUserID = viewerUserID
}
entries, err := s.repository.ListRoomListEntries(ctx, RoomListQuery{ entries, err := s.repository.ListRoomListEntries(ctx, RoomListQuery{
AppCode: appcode.FromContext(ctx), AppCode: appcode.FromContext(ctx),
VisibleRegionID: normalizeVisibleRegionID(req.GetVisibleRegionId()), VisibleRegionID: normalizeVisibleRegionID(req.GetVisibleRegionId()),
Tab: tab, Tab: tab,
OwnerUserID: ownerUserID,
Query: query, Query: query,
Limit: limit + 1, Limit: limit + 1,
CursorSortScore: cursor.SortScore, CursorSortScore: cursor.SortScore,
@ -75,6 +73,81 @@ func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (
return nil, err return nil, err
} }
return roomListResponseFromEntries(tab, query, entries, limit), nil
}
// ListRoomFeeds 查询 Mine 页 visited/friend/following 用户房间流。
func (s *Service) ListRoomFeeds(ctx context.Context, req *roomv1.ListRoomFeedsRequest) (*roomv1.ListRoomsResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
viewerUserID := req.GetViewerUserId()
if viewerUserID <= 0 {
// feed 一定绑定当前登录用户,不能允许匿名或客户端自选用户。
return nil, xerr.New(xerr.InvalidArgument, "viewer_user_id is required")
}
tab := normalizeRoomFeedTab(req.GetTab())
if tab == "" {
return nil, xerr.New(xerr.InvalidArgument, "tab is invalid")
}
limit := normalizeRoomListLimit(req.GetLimit())
query, err := normalizeRoomListQuery(req.GetQuery())
if err != nil {
return nil, err
}
cursor, err := decodeRoomListCursor(tab, query, req.GetCursor())
if err != nil {
return nil, err
}
entries, err := s.repository.ListRoomUserFeedEntries(ctx, RoomUserFeedQuery{
AppCode: appcode.FromContext(ctx),
UserID: viewerUserID,
FeedType: tab,
VisibleRegionID: normalizeVisibleRegionID(req.GetVisibleRegionId()),
Query: query,
Limit: limit + 1,
CursorUpdatedAtMS: cursor.UpdatedAtMS,
CursorRoomID: cursor.RoomID,
})
if err != nil {
return nil, err
}
return roomListResponseFromEntries(tab, query, entries, limit), nil
}
// GetMyRoom 查询当前用户自己创建的房间,不依赖 room_list_entries 投影命中。
func (s *Service) GetMyRoom(ctx context.Context, req *roomv1.GetMyRoomRequest) (*roomv1.GetMyRoomResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
ownerUserID := req.GetOwnerUserId()
if ownerUserID <= 0 {
// owner_user_id 必须由 gateway 鉴权后填充Mine 卡片不能接受客户端任意查人。
return nil, xerr.New(xerr.InvalidArgument, "owner_user_id is required")
}
serverTimeMS := s.clock.Now().UnixMilli()
meta, exists, err := s.repository.GetRoomMetaByOwner(ctx, ownerUserID)
if err != nil {
return nil, err
}
if !exists {
return &roomv1.GetMyRoomResponse{ServerTimeMs: serverTimeMS}, nil
}
snapshot, err := s.currentSnapshot(ctx, meta.RoomID)
if err != nil {
return nil, err
}
return &roomv1.GetMyRoomResponse{
HasRoom: true,
Room: roomListItemToProto(roomListEntryFromMetaAndSnapshot(meta, snapshot, serverTimeMS)),
ServerTimeMs: serverTimeMS,
}, nil
}
func roomListResponseFromEntries(tab string, query string, entries []RoomListEntry, limit int) *roomv1.ListRoomsResponse {
nextCursor := "" nextCursor := ""
if len(entries) > limit { if len(entries) > limit {
// 多查一条用于判断是否存在下一页;返回数据只包含请求的 limit 条。 // 多查一条用于判断是否存在下一页;返回数据只包含请求的 limit 条。
@ -91,7 +164,7 @@ func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (
return &roomv1.ListRoomsResponse{ return &roomv1.ListRoomsResponse{
Rooms: items, Rooms: items,
NextCursor: nextCursor, NextCursor: nextCursor,
}, nil }
} }
// projectRoomListBestEffort 根据最新快照刷新房间列表投影。 // projectRoomListBestEffort 根据最新快照刷新房间列表投影。
@ -164,15 +237,26 @@ func roomListSortScore(heat int64, onlineCount int32, occupiedSeatCount int32) i
return heat*1000 + int64(onlineCount)*100 + int64(occupiedSeatCount)*10 return heat*1000 + int64(onlineCount)*100 + int64(occupiedSeatCount)*10
} }
// normalizeRoomListTab 固定首版只支持 hot/new/me未知 tab fail-closed // normalizeRoomListTab 只支持公共发现列表 tabMine 关系流走 ListRoomFeeds
func normalizeRoomListTab(tab string) string { func normalizeRoomListTab(tab string) string {
switch strings.ToLower(strings.TrimSpace(tab)) { switch strings.ToLower(strings.TrimSpace(tab)) {
case "", roomListTabHot: case "", roomListTabHot:
return roomListTabHot return roomListTabHot
case roomListTabNew: case roomListTabNew:
return roomListTabNew return roomListTabNew
case roomListTabMe: default:
return roomListTabMe return ""
}
}
func normalizeRoomFeedTab(tab string) string {
switch strings.ToLower(strings.TrimSpace(tab)) {
case roomListTabVisited:
return roomListTabVisited
case roomListTabFriend:
return roomListTabFriend
case roomListTabFollowing:
return roomListTabFollowing
default: default:
return "" return ""
} }
@ -240,6 +324,7 @@ func encodeRoomListCursor(tab string, query string, entry RoomListEntry) string
Query: query, Query: query,
SortScore: entry.SortScore, SortScore: entry.SortScore,
CreatedAtMS: entry.CreatedAtMS, CreatedAtMS: entry.CreatedAtMS,
UpdatedAtMS: entry.UpdatedAtMS,
RoomID: entry.RoomID, RoomID: entry.RoomID,
} }
@ -271,3 +356,39 @@ func roomListItemToProto(entry RoomListEntry) *roomv1.RoomListItem {
VisibleRegionId: entry.VisibleRegionID, VisibleRegionId: entry.VisibleRegionID,
} }
} }
func roomListEntryFromMetaAndSnapshot(meta RoomMeta, snapshot *roomv1.RoomSnapshot, nowMS int64) RoomListEntry {
if snapshot == nil || snapshot.GetRoomId() == "" {
return RoomListEntry{
AppCode: meta.AppCode,
RoomID: meta.RoomID,
RoomShortID: meta.RoomShortID,
VisibleRegionID: normalizeVisibleRegionID(meta.VisibleRegionID),
OwnerUserID: meta.OwnerUserID,
HostUserID: meta.HostUserID,
Mode: meta.Mode,
Status: meta.Status,
SeatCount: meta.SeatCount,
CreatedAtMS: nowMS,
UpdatedAtMS: nowMS,
}
}
entry := roomListEntryFromSnapshot(snapshot, meta.VisibleRegionID, nowMS)
if entry.RoomShortID == "" {
entry.RoomShortID = meta.RoomShortID
}
if entry.Mode == "" {
entry.Mode = meta.Mode
}
if entry.Status == "" {
entry.Status = meta.Status
}
if entry.OwnerUserID <= 0 {
entry.OwnerUserID = meta.OwnerUserID
}
if entry.HostUserID <= 0 {
entry.HostUserID = meta.HostUserID
}
return entry
}

View File

@ -2,12 +2,14 @@ package service
import ( import (
"context" "context"
"log/slog"
"time" "time"
roomeventsv1 "hyapp.local/api/proto/events/room/v1" roomeventsv1 "hyapp.local/api/proto/events/room/v1"
roomv1 "hyapp.local/api/proto/room/v1" roomv1 "hyapp.local/api/proto/room/v1"
"hyapp/pkg/appcode" "hyapp/pkg/appcode"
"hyapp/pkg/idgen" "hyapp/pkg/idgen"
"hyapp/pkg/logx"
"hyapp/pkg/tencentim" "hyapp/pkg/tencentim"
"hyapp/pkg/xerr" "hyapp/pkg/xerr"
"hyapp/services/room-service/internal/room/command" "hyapp/services/room-service/internal/room/command"
@ -86,6 +88,7 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.projectRoomVisitBestEffort(ctx, cmd.ActorUserID(), cmd.RoomID())
return &roomv1.JoinRoomResponse{ return &roomv1.JoinRoomResponse{
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
@ -94,6 +97,29 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
}, nil }, nil
} }
// projectRoomVisitBestEffort 记录 Mine/Visited tab 的房间访问索引。
// 这是列表读模型,不参与 Room Cell 提交;失败只记录日志,不能影响已经成功的 JoinRoom。
func (s *Service) projectRoomVisitBestEffort(ctx context.Context, userID int64, roomID string) {
if userID <= 0 || roomID == "" {
return
}
nowMS := s.clock.Now().UnixMilli()
if err := s.repository.UpsertRoomUserFeedEntry(ctx, RoomUserFeedEntry{
AppCode: appcode.FromContext(ctx),
UserID: userID,
FeedType: roomListTabVisited,
RoomID: roomID,
CreatedAtMS: nowMS,
UpdatedAtMS: nowMS,
}); err != nil {
logx.Error(ctx, "room_visit_feed_upsert_failed", err,
slog.String("component", "room_user_feed_projector"),
slog.String("room_id", roomID),
slog.Int64("user_id", userID),
)
}
}
// RoomHeartbeat 显式刷新已在房间内用户的业务 presence。 // RoomHeartbeat 显式刷新已在房间内用户的业务 presence。
func (s *Service) RoomHeartbeat(ctx context.Context, req *roomv1.RoomHeartbeatRequest) (*roomv1.RoomHeartbeatResponse, error) { func (s *Service) RoomHeartbeat(ctx context.Context, req *roomv1.RoomHeartbeatRequest) (*roomv1.RoomHeartbeatResponse, error) {
ctx = contextFromMeta(ctx, req.GetMeta()) ctx = contextFromMeta(ctx, req.GetMeta())

View File

@ -146,9 +146,9 @@ type RoomListQuery struct {
AppCode string AppCode string
// VisibleRegionID 是服务端解析出的用户区域,客户端不能伪造。 // VisibleRegionID 是服务端解析出的用户区域,客户端不能伪造。
VisibleRegionID int64 VisibleRegionID int64
// Tab 只允许 hot/new/me决定过滤和排序字段。 // Tab 只允许 hot/new,决定公共发现列表排序字段。
Tab string Tab string
// OwnerUserID 非 0 时只返回该用户创建的房间 // OwnerUserID 保留给内部定向读模型;公开 Mine 顶部卡片走 GetMyRoom
OwnerUserID int64 OwnerUserID int64
// Query 非空时按 room_id 或 title 做包含匹配。 // Query 非空时按 room_id 或 title 做包含匹配。
Query string Query string
@ -158,10 +158,49 @@ type RoomListQuery struct {
CursorSortScore int64 CursorSortScore int64
// CursorCreatedAtMS 是 new tab 的游标创建时间。 // CursorCreatedAtMS 是 new tab 的游标创建时间。
CursorCreatedAtMS int64 CursorCreatedAtMS int64
// CursorUpdatedAtMS 是用户关系 feed tab 的游标更新时间。
CursorUpdatedAtMS int64
// CursorRoomID 是同分或同时间下的稳定翻页边界。 // CursorRoomID 是同分或同时间下的稳定翻页边界。
CursorRoomID string CursorRoomID string
} }
// RoomUserFeedEntry 是“访问过/好友/关注”房间流的读模型索引。
// 它只保存用户到房间的列表关系,不复制房间卡片字段,卡片内容仍来自 room_list_entries。
type RoomUserFeedEntry struct {
// AppCode 是 feed 所属 App防止跨 App 看到关系流。
AppCode string
// UserID 是拥有该 feed 的用户。
UserID int64
// FeedType 是 visited/friend/following 之一;关系事实 owner 后续可通过事件填充 friend/following。
FeedType string
// RoomID 是 feed 指向的房间。
RoomID string
// CreatedAtMS 是首次建立该 feed 关系的 UTC 毫秒。
CreatedAtMS int64
// UpdatedAtMS 是 feed 排序时间visited 会在 JoinRoom 成功后刷新。
UpdatedAtMS int64
}
// RoomUserFeedQuery 是用户房间关系流的分页查询条件。
type RoomUserFeedQuery struct {
// AppCode 是 feed 查询所属 App。
AppCode string
// UserID 是当前登录用户。
UserID int64
// FeedType 是 visited/friend/following 之一。
FeedType string
// VisibleRegionID 沿用房间发现页区域桶,避免关系流跨区域露出。
VisibleRegionID int64
// Query 非空时按 room_id、room_short_id 或 title 做包含匹配。
Query string
// Limit 是实际查询数量service 层会限制最大值。
Limit int
// CursorUpdatedAtMS 是上一页最后一条 feed 更新时间。
CursorUpdatedAtMS int64
// CursorRoomID 是同时间下的稳定翻页边界。
CursorRoomID string
}
// Repository 聚合 room-service 在首版需要的全部持久化读写。 // Repository 聚合 room-service 在首版需要的全部持久化读写。
type Repository interface { type Repository interface {
// SaveRoomMeta 保存房间基础元数据,创建房间时必须先建立它。 // SaveRoomMeta 保存房间基础元数据,创建房间时必须先建立它。
@ -196,6 +235,10 @@ type Repository interface {
UpsertRoomListEntry(ctx context.Context, entry RoomListEntry) error UpsertRoomListEntry(ctx context.Context, entry RoomListEntry) error
// ListRoomListEntries 按区域和 tab 查询房间列表,不访问 Room Cell 内存。 // ListRoomListEntries 按区域和 tab 查询房间列表,不访问 Room Cell 内存。
ListRoomListEntries(ctx context.Context, query RoomListQuery) ([]RoomListEntry, error) ListRoomListEntries(ctx context.Context, query RoomListQuery) ([]RoomListEntry, error)
// UpsertRoomUserFeedEntry 写入用户房间关系流读模型;失败不应影响房间主链路。
UpsertRoomUserFeedEntry(ctx context.Context, entry RoomUserFeedEntry) error
// ListRoomUserFeedEntries 按用户关系流读取房间卡片,不访问 Room Cell 内存集合。
ListRoomUserFeedEntries(ctx context.Context, query RoomUserFeedQuery) ([]RoomListEntry, error)
// ProjectRoomPresence 用最新快照刷新用户当前房间读模型;失败不应破坏 Room Cell 已提交状态。 // ProjectRoomPresence 用最新快照刷新用户当前房间读模型;失败不应破坏 Room Cell 已提交状态。
ProjectRoomPresence(ctx context.Context, snapshot RoomPresenceSnapshot) error ProjectRoomPresence(ctx context.Context, snapshot RoomPresenceSnapshot) error
// GetCurrentRoomPresence 查询用户当前 active 房间 presence不扫描房间列表或快照。 // GetCurrentRoomPresence 查询用户当前 active 房间 presence不扫描房间列表或快照。

View File

@ -333,6 +333,106 @@ func TestRoomListUsesRegionReadModelAndProjectionUpdates(t *testing.T) {
} }
} }
func TestGetMyRoomDoesNotDependOnRoomListProjection(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
svc := newRoomService("node-a", 1, router.NewMemoryDirectory(), repository, &fakeSyncPublisher{}, &fakeOutboxPublisher{})
roomID := "room-my-room-owner"
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
Meta: roomservice.NewRequestMeta(roomID, 42),
SeatCount: 6,
Mode: "voice",
RoomName: "Owner Top Card",
RoomAvatar: "https://cdn.example.com/owner-room.png",
VisibleRegionId: 1001,
}); err != nil {
t.Fatalf("CreateRoom failed: %v", err)
}
repository.DeleteRoomListEntry(roomID)
resp, err := svc.GetMyRoom(ctx, &roomv1.GetMyRoomRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
OwnerUserId: 42,
})
if err != nil {
t.Fatalf("GetMyRoom failed: %v", err)
}
if !resp.GetHasRoom() || resp.GetRoom().GetRoomId() != roomID {
t.Fatalf("my room should return owner room from rooms metadata: %+v", resp)
}
if resp.GetRoom().GetTitle() != "Owner Top Card" || resp.GetRoom().GetSeatCount() != 6 || resp.GetRoom().GetVisibleRegionId() != 1001 {
t.Fatalf("my room card should use snapshot/meta fields: %+v", resp.GetRoom())
}
}
func TestListRoomFeedsVisitedUsesJoinRoomFeed(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
svc := newRoomService("node-a", 1, router.NewMemoryDirectory(), repository, &fakeSyncPublisher{}, &fakeOutboxPublisher{})
roomID := "room-visited-feed"
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
Meta: roomservice.NewRequestMeta(roomID, 1),
SeatCount: 4,
Mode: "voice",
RoomName: "Visited Room",
VisibleRegionId: 1001,
}); err != nil {
t.Fatalf("CreateRoom failed: %v", err)
}
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{Meta: roomservice.NewRequestMeta(roomID, 2)}); err != nil {
t.Fatalf("JoinRoom failed: %v", err)
}
resp, err := svc.ListRoomFeeds(ctx, &roomv1.ListRoomFeedsRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
ViewerUserId: 2,
VisibleRegionId: 1001,
Tab: "visited",
Limit: 20,
})
if err != nil {
t.Fatalf("ListRoomFeeds visited failed: %v", err)
}
if len(resp.GetRooms()) != 1 || resp.GetRooms()[0].GetRoomId() != roomID {
t.Fatalf("visited feed should expose joined room: %+v", resp.GetRooms())
}
otherUserResp, err := svc.ListRoomFeeds(ctx, &roomv1.ListRoomFeedsRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
ViewerUserId: 3,
VisibleRegionId: 1001,
Tab: "visited",
Limit: 20,
})
if err != nil {
t.Fatalf("ListRoomFeeds visited for other user failed: %v", err)
}
if len(otherUserResp.GetRooms()) != 0 {
t.Fatalf("visited feed must be scoped to viewer: %+v", otherUserResp.GetRooms())
}
}
func TestListRoomsRejectsUserFeedTabs(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
svc := newRoomService("node-a", 1, router.NewMemoryDirectory(), repository, &fakeSyncPublisher{}, &fakeOutboxPublisher{})
for _, tab := range []string{"visited", "friend", "following", "me"} {
_, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
ViewerUserId: 2,
VisibleRegionId: 1001,
Tab: tab,
Limit: 20,
})
if !xerr.IsCode(err, xerr.InvalidArgument) {
t.Fatalf("ListRooms must reject %q tab: %v", tab, err)
}
}
}
func TestGetCurrentRoomReturnsRecoverablePresence(t *testing.T) { func TestGetCurrentRoomReturnsRecoverablePresence(t *testing.T) {
ctx := context.Background() ctx := context.Background()
repository := mysqltest.NewRepository(t) repository := mysqltest.NewRepository(t)

View File

@ -167,12 +167,23 @@ func (r *Repository) Migrate(ctx context.Context) error {
KEY idx_room_outbox_pending (app_code, status, created_at_ms), KEY idx_room_outbox_pending (app_code, status, created_at_ms),
KEY idx_room_outbox_claim (app_code, status, lock_until_ms, created_at_ms) KEY idx_room_outbox_claim (app_code, status, lock_until_ms, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_user_feed_entries (
app_code VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
feed_type VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, user_id, feed_type, room_id),
KEY idx_room_user_feed_list (app_code, user_id, feed_type, updated_at_ms DESC, room_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`ALTER TABLE rooms MODIFY COLUMN app_code VARCHAR(32) NOT NULL`, `ALTER TABLE rooms MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_list_entries MODIFY COLUMN app_code VARCHAR(32) NOT NULL`, `ALTER TABLE room_list_entries MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_user_presence MODIFY COLUMN app_code VARCHAR(32) NOT NULL`, `ALTER TABLE room_user_presence MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_snapshots MODIFY COLUMN app_code VARCHAR(32) NOT NULL`, `ALTER TABLE room_snapshots MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_command_log MODIFY COLUMN app_code VARCHAR(32) NOT NULL`, `ALTER TABLE room_command_log MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_outbox MODIFY COLUMN app_code VARCHAR(32) NOT NULL`, `ALTER TABLE room_outbox MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_user_feed_entries MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
} }
for _, statement := range statements { for _, statement := range statements {
@ -812,11 +823,84 @@ func (r *Repository) ListRoomListEntries(ctx context.Context, query roomservice.
return entries, nil return entries, nil
} }
// UpsertRoomUserFeedEntry 写入用户房间关系流索引。
func (r *Repository) UpsertRoomUserFeedEntry(ctx context.Context, entry roomservice.RoomUserFeedEntry) error {
// feed 只保存用户到房间的索引;房间卡片字段仍以 room_list_entries 为准,避免复制不一致。
appCode := normalizedRecordAppCode(ctx, entry.AppCode)
_, err := r.db.ExecContext(ctx,
`INSERT INTO room_user_feed_entries (app_code, user_id, feed_type, room_id, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE updated_at_ms = VALUES(updated_at_ms)`,
appCode,
entry.UserID,
entry.FeedType,
entry.RoomID,
entry.CreatedAtMS,
entry.UpdatedAtMS,
)
return err
}
// ListRoomUserFeedEntries 按用户关系流读取房间卡片。
func (r *Repository) ListRoomUserFeedEntries(ctx context.Context, query roomservice.RoomUserFeedQuery) ([]roomservice.RoomListEntry, error) {
if query.Limit <= 0 {
query.Limit = 20
}
query.AppCode = normalizedRecordAppCode(ctx, query.AppCode)
sqlText, args := buildRoomUserFeedQuerySQL(query)
rows, err := r.db.QueryContext(ctx, sqlText, args...)
if err != nil {
return nil, err
}
defer rows.Close()
entries := make([]roomservice.RoomListEntry, 0, query.Limit)
for rows.Next() {
var entry roomservice.RoomListEntry
if err := rows.Scan(
&entry.AppCode,
&entry.RoomID,
&entry.RoomShortID,
&entry.VisibleRegionID,
&entry.OwnerUserID,
&entry.HostUserID,
&entry.Title,
&entry.CoverURL,
&entry.Mode,
&entry.Status,
&entry.Heat,
&entry.OnlineCount,
&entry.SeatCount,
&entry.OccupiedSeatCount,
&entry.SortScore,
&entry.CreatedAtMS,
&entry.UpdatedAtMS,
); err != nil {
return nil, err
}
entries = append(entries, entry)
}
if err := rows.Err(); err != nil {
return nil, err
}
return entries, nil
}
const roomListSelectColumns = ` const roomListSelectColumns = `
SELECT app_code, room_id, room_short_id, visible_region_id, owner_user_id, host_user_id, title, cover_url, mode, status, SELECT app_code, room_id, room_short_id, visible_region_id, owner_user_id, host_user_id, title, cover_url, mode, status,
heat, online_count, seat_count, occupied_seat_count, sort_score, created_at_ms, updated_at_ms heat, online_count, seat_count, occupied_seat_count, sort_score, created_at_ms, updated_at_ms
FROM room_list_entries` FROM room_list_entries`
const roomUserFeedSelectColumns = `
SELECT r.app_code, r.room_id, r.room_short_id, r.visible_region_id, r.owner_user_id, r.host_user_id, r.title, r.cover_url, r.mode, r.status,
r.heat, r.online_count, r.seat_count, r.occupied_seat_count, r.sort_score, r.created_at_ms, f.updated_at_ms
FROM room_user_feed_entries f
JOIN room_list_entries r ON r.app_code = f.app_code AND r.room_id = f.room_id`
func buildRoomListQuerySQL(query roomservice.RoomListQuery) (string, []any) { func buildRoomListQuerySQL(query roomservice.RoomListQuery) (string, []any) {
where := []string{"app_code = ?", "visible_region_id = ?", "status = ?"} where := []string{"app_code = ?", "visible_region_id = ?", "status = ?"}
args := []any{appcode.Normalize(query.AppCode), query.VisibleRegionID, "active"} args := []any{appcode.Normalize(query.AppCode), query.VisibleRegionID, "active"}
@ -829,7 +913,7 @@ func buildRoomListQuerySQL(query roomservice.RoomListQuery) (string, []any) {
like := "%" + escapeRoomListLike(query.Query) + "%" like := "%" + escapeRoomListLike(query.Query) + "%"
args = append(args, like, like, like) args = append(args, like, like, like)
} }
if query.Tab == "new" || query.Tab == "me" { if query.Tab == "new" {
if query.CursorRoomID != "" { if query.CursorRoomID != "" {
where = append(where, "(created_at_ms < ? OR (created_at_ms = ? AND room_id > ?))") where = append(where, "(created_at_ms < ? OR (created_at_ms = ? AND room_id > ?))")
args = append(args, query.CursorCreatedAtMS, query.CursorCreatedAtMS, query.CursorRoomID) args = append(args, query.CursorCreatedAtMS, query.CursorCreatedAtMS, query.CursorRoomID)
@ -846,6 +930,22 @@ func buildRoomListQuerySQL(query roomservice.RoomListQuery) (string, []any) {
return roomListSelectColumns + "\n\tWHERE " + strings.Join(where, " AND ") + "\n\tORDER BY sort_score DESC, room_id ASC\n\tLIMIT ?", args return roomListSelectColumns + "\n\tWHERE " + strings.Join(where, " AND ") + "\n\tORDER BY sort_score DESC, room_id ASC\n\tLIMIT ?", args
} }
func buildRoomUserFeedQuerySQL(query roomservice.RoomUserFeedQuery) (string, []any) {
where := []string{"f.app_code = ?", "f.user_id = ?", "f.feed_type = ?", "r.visible_region_id = ?", "r.status = ?"}
args := []any{appcode.Normalize(query.AppCode), query.UserID, query.FeedType, query.VisibleRegionID, "active"}
if strings.TrimSpace(query.Query) != "" {
where = append(where, "(r.room_id LIKE ? ESCAPE '\\\\' OR r.room_short_id LIKE ? ESCAPE '\\\\' OR r.title LIKE ? ESCAPE '\\\\')")
like := "%" + escapeRoomListLike(query.Query) + "%"
args = append(args, like, like, like)
}
if query.CursorRoomID != "" {
where = append(where, "(f.updated_at_ms < ? OR (f.updated_at_ms = ? AND f.room_id > ?))")
args = append(args, query.CursorUpdatedAtMS, query.CursorUpdatedAtMS, query.CursorRoomID)
}
args = append(args, query.Limit)
return roomUserFeedSelectColumns + "\n\tWHERE " + strings.Join(where, " AND ") + "\n\tORDER BY f.updated_at_ms DESC, f.room_id ASC\n\tLIMIT ?", args
}
// ProjectRoomPresence 用最新 RoomSnapshot 投影用户当前房间读模型。 // ProjectRoomPresence 用最新 RoomSnapshot 投影用户当前房间读模型。
func (r *Repository) ProjectRoomPresence(ctx context.Context, snapshot roomservice.RoomPresenceSnapshot) error { func (r *Repository) ProjectRoomPresence(ctx context.Context, snapshot roomservice.RoomPresenceSnapshot) error {
appCode := normalizedRecordAppCode(ctx, snapshot.AppCode) appCode := normalizedRecordAppCode(ctx, snapshot.AppCode)

View File

@ -63,6 +63,15 @@ func (r *Repository) SetRoomStatus(roomID string, status string) {
} }
} }
// DeleteRoomListEntry removes the discovery projection while keeping rooms metadata.
func (r *Repository) DeleteRoomListEntry(roomID string) {
r.t.Helper()
if _, err := r.schema.DB.ExecContext(context.Background(), `DELETE FROM room_list_entries WHERE app_code = ? AND room_id = ?`, appcode.Default, roomID); err != nil {
r.t.Fatalf("delete room list entry failed: %v", err)
}
}
// OutboxRecord reads one outbox row, including delivered rows that normal scans skip. // OutboxRecord reads one outbox row, including delivered rows that normal scans skip.
func (r *Repository) OutboxRecord(eventID string) (outbox.Record, bool) { func (r *Repository) OutboxRecord(eventID string) (outbox.Record, bool) {
r.t.Helper() r.t.Helper()

View File

@ -187,6 +187,20 @@ func (s *Server) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*
return mapServiceResult(s.svc.ListRooms(ctx, req)) return mapServiceResult(s.svc.ListRooms(ctx, req))
} }
// ListRoomFeeds 代理到 Mine 页用户房间流读模型。
func (s *Server) ListRoomFeeds(ctx context.Context, req *roomv1.ListRoomFeedsRequest) (*roomv1.ListRoomsResponse, error) {
// feed 查询只读用户到房间索引,并 join 房间卡片投影,不扫描 Room Cell 内存集合。
ctx = contextWithMetaApp(ctx, req.GetMeta())
return mapServiceResult(s.svc.ListRoomFeeds(ctx, req))
}
// GetMyRoom 代理到 Mine 顶部“我的房间”权威查询。
func (s *Server) GetMyRoom(ctx context.Context, req *roomv1.GetMyRoomRequest) (*roomv1.GetMyRoomResponse, error) {
// 我的房间查询直接读取 rooms 元数据,并用快照补齐实时卡片字段,不依赖发现页投影。
ctx = contextWithMetaApp(ctx, req.GetMeta())
return mapServiceResult(s.svc.GetMyRoom(ctx, req))
}
// GetCurrentRoom 代理到用户当前房间恢复探测读模型。 // GetCurrentRoom 代理到用户当前房间恢复探测读模型。
func (s *Server) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) { func (s *Server) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) {
// 恢复探测不修改 Room Cell只在读模型命中后用快照做二次校验。 // 恢复探测不修改 Room Cell只在读模型命中后用快照做二次校验。

View File

@ -68,6 +68,65 @@ CREATE TABLE IF NOT EXISTS users (
KEY idx_users_country_region (app_code, country, region_id) KEY idx_users_country_region (app_code, country, region_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS user_profile_stats (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
user_id BIGINT NOT NULL,
visitors_count BIGINT NOT NULL DEFAULT 0,
following_count BIGINT NOT NULL DEFAULT 0,
friends_count BIGINT NOT NULL DEFAULT 0,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS user_profile_visits (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
target_user_id BIGINT NOT NULL,
visitor_user_id BIGINT NOT NULL,
visit_count BIGINT NOT NULL DEFAULT 1,
first_visited_at_ms BIGINT NOT NULL,
last_visited_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, target_user_id, visitor_user_id),
KEY idx_user_profile_visits_visitor (app_code, visitor_user_id, last_visited_at_ms),
KEY idx_user_profile_visits_target_time (app_code, target_user_id, last_visited_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS user_follows (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
follower_user_id BIGINT NOT NULL,
followee_user_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
followed_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, follower_user_id, followee_user_id),
KEY idx_user_follows_followee_status (app_code, followee_user_id, status, updated_at_ms),
KEY idx_user_follows_follower_status (app_code, follower_user_id, status, followed_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS user_friend_applications (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
requester_user_id BIGINT NOT NULL,
target_user_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, requester_user_id, target_user_id),
KEY idx_friend_applications_target_status (app_code, target_user_id, status, updated_at_ms),
KEY idx_friend_applications_requester_status (app_code, requester_user_id, status, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS user_friendships (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
user_id BIGINT NOT NULL,
friend_user_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
friended_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, user_id, friend_user_id),
KEY idx_user_friendships_user_status (app_code, user_id, status, friended_at_ms),
KEY idx_user_friendships_friend_status (app_code, friend_user_id, status, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS display_user_id_sequences ( CREATE TABLE IF NOT EXISTS display_user_id_sequences (
app_code VARCHAR(32) NOT NULL PRIMARY KEY, app_code VARCHAR(32) NOT NULL PRIMARY KEY,
next_value BIGINT NOT NULL, next_value BIGINT NOT NULL,

View File

@ -215,6 +215,7 @@ func New(cfg config.Config) (*App, error) {
// 多个 protobuf service 共用一个 Server 适配器,领域逻辑仍拆在 auth/user/host service。 // 多个 protobuf service 共用一个 Server 适配器,领域逻辑仍拆在 auth/user/host service。
userv1.RegisterAuthServiceServer(server, userServer) userv1.RegisterAuthServiceServer(server, userServer)
userv1.RegisterUserServiceServer(server, userServer) userv1.RegisterUserServiceServer(server, userServer)
userv1.RegisterUserSocialServiceServer(server, userServer)
userv1.RegisterUserDeviceServiceServer(server, userServer) userv1.RegisterUserDeviceServiceServer(server, userServer)
userv1.RegisterAppRegistryServiceServer(server, userServer) userv1.RegisterAppRegistryServiceServer(server, userServer)
userv1.RegisterUserIdentityServiceServer(server, userServer) userv1.RegisterUserIdentityServiceServer(server, userServer)

View File

@ -178,6 +178,23 @@ type CoinSellerProfile struct {
UpdatedAtMs int64 UpdatedAtMs int64
} }
// UserRoleSummary 是面向 App 入口显隐的聚合 read model。
// 这里不承载团队统计或薪资指标,只表达当前用户是否拥有可用身份和待处理邀请数量。
type UserRoleSummary struct {
UserID int64
IsHost bool
IsAgency bool
IsBD bool
IsBDLeader bool
IsCoinSeller bool
HostStatus string
AgencyID int64
BDID int64
BDStatus string
CoinSellerStatus string
PendingRoleInvitations int64
}
// AgencyMembership 是 host 与 Agency 的有时效归属事实。 // AgencyMembership 是 host 与 Agency 的有时效归属事实。
type AgencyMembership struct { type AgencyMembership struct {
MembershipID int64 MembershipID int64

View File

@ -165,6 +165,66 @@ type User struct {
UpdatedAtMs int64 UpdatedAtMs int64
} }
// ProfileStats 是我的页顶部统计 read model只保存预聚合计数。
type ProfileStats struct {
// AppCode 是统计所属 App关系链和礼物统计都必须在 App 内隔离。
AppCode string
// UserID 是统计所属用户。
UserID int64
// VisitorsCount 是访问过该用户主页的用户计数投影。
VisitorsCount int64
// FollowingCount 是该用户关注的人数计数投影。
FollowingCount int64
// FriendsCount 是该用户好友人数计数投影。
FriendsCount int64
// UpdatedAtMs 是这份计数投影最后更新时间。
UpdatedAtMs int64
}
const (
// SocialStatusActive 表示关注或好友关系当前有效。
SocialStatusActive = "active"
// SocialStatusCancelled 表示关注关系已取消。
SocialStatusCancelled = "cancelled"
// SocialStatusDeleted 表示好友关系已删除。
SocialStatusDeleted = "deleted"
// FriendApplicationStatusPending 表示好友申请等待对方处理。
FriendApplicationStatusPending = "pending"
// FriendApplicationStatusAccepted 表示好友申请已同意。
FriendApplicationStatusAccepted = "accepted"
)
// ProfileVisitRecord 是访问某个用户主页的去重记录。
type ProfileVisitRecord struct {
VisitorUserID int64
TargetUserID int64
VisitCount int64
LastVisitedAtMs int64
}
// FollowRecord 是单向关注关系。
type FollowRecord struct {
FollowerUserID int64
FolloweeUserID int64
FollowedAtMs int64
}
// FriendRecord 是双向好友关系在单用户视角下的一条投影。
type FriendRecord struct {
UserID int64
FriendUserID int64
FriendedAtMs int64
}
// FriendApplication 是好友申请事实。
type FriendApplication struct {
RequesterUserID int64
TargetUserID int64
Status string
CreatedAtMs int64
UpdatedAtMs int64
}
// Identity 是 user_id 与当前 active display_user_id 的绑定投影。 // Identity 是 user_id 与当前 active display_user_id 的绑定投影。
type Identity struct { type Identity struct {
// AppCode 是短号所属 App。 // AppCode 是短号所属 App。

View File

@ -32,6 +32,7 @@ type Repository interface {
GetHostProfile(ctx context.Context, userID int64) (hostdomain.HostProfile, error) GetHostProfile(ctx context.Context, userID int64) (hostdomain.HostProfile, error)
GetBDProfile(ctx context.Context, userID int64) (hostdomain.BDProfile, error) GetBDProfile(ctx context.Context, userID int64) (hostdomain.BDProfile, error)
GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error) GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error)
GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error)
ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error)
ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error) ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error)
} }
@ -482,6 +483,18 @@ func (s *Service) GetCoinSellerProfile(ctx context.Context, userID int64) (hostd
return s.repository.GetCoinSellerProfile(ctx, userID) return s.repository.GetCoinSellerProfile(ctx, userID)
} }
// GetUserRoleSummary 读取 App 我的页使用的轻量身份摘要,避免 gateway 为入口显隐发起多个角色 RPC。
func (s *Service) GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error) {
if s.repository == nil {
return hostdomain.UserRoleSummary{}, xerr.New(xerr.Unavailable, "host repository is not configured")
}
if userID <= 0 {
return hostdomain.UserRoleSummary{}, xerr.New(xerr.InvalidArgument, "user_id is required")
}
return s.repository.GetUserRoleSummary(ctx, userID)
}
// ListAgencyMembers 读取 Agency 成员关系列表,第一阶段直接读事实表。 // ListAgencyMembers 读取 Agency 成员关系列表,第一阶段直接读事实表。
func (s *Service) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) { func (s *Service) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) {
if s.repository == nil { if s.repository == nil {

View File

@ -495,3 +495,62 @@ func TestAdminCreateCoinSellerCanCoexistWithBDAndAgency(t *testing.T) {
t.Fatalf("disabled coin seller mismatch: %+v", disabled) t.Fatalf("disabled coin seller mismatch: %+v", disabled)
} }
} }
func TestGetUserRoleSummaryAggregatesRolesAndPendingInvitations(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
seedActiveUser(t, repository, 931, 60)
seedActiveUser(t, repository, 932, 60)
repository.PutHostProfile(hostdomain.HostProfile{
UserID: 931,
Status: hostdomain.HostStatusActive,
RegionID: 60,
CurrentAgencyID: 933,
Source: hostdomain.HostSourceAdminCreateAgency,
})
repository.PutAgency(hostdomain.Agency{
AgencyID: 933,
OwnerUserID: 931,
RegionID: 60,
ParentBDUserID: 931,
Status: hostdomain.AgencyStatusActive,
JoinEnabled: true,
})
repository.PutBDProfile(hostdomain.BDProfile{
UserID: 931,
Role: hostdomain.BDRoleLeader,
RegionID: 60,
Status: hostdomain.BDStatusActive,
})
svc := newHostService(repository, 8000, 9000)
if _, err := svc.CreateCoinSeller(ctx, hostservice.CreateCoinSellerInput{
CommandID: "admin-create-coin-seller-931",
AdminUserID: 1,
TargetUserID: 931,
Reason: "summary test",
}); err != nil {
t.Fatalf("CreateCoinSeller failed: %v", err)
}
if _, err := svc.InviteBD(ctx, hostservice.InviteBDInput{
CommandID: "invite-bd-932",
InviterUserID: 931,
TargetUserID: 932,
}); err != nil {
t.Fatalf("InviteBD failed: %v", err)
}
summary, err := svc.GetUserRoleSummary(ctx, 931)
if err != nil {
t.Fatalf("GetUserRoleSummary failed: %v", err)
}
if !summary.IsHost || !summary.IsAgency || !summary.IsBD || !summary.IsBDLeader || !summary.IsCoinSeller || summary.AgencyID != 933 || summary.BDID != 931 {
t.Fatalf("role summary mismatch: %+v", summary)
}
pending, err := svc.GetUserRoleSummary(ctx, 932)
if err != nil {
t.Fatalf("GetUserRoleSummary pending failed: %v", err)
}
if pending.PendingRoleInvitations != 1 || pending.IsHost || pending.IsBD {
t.Fatalf("pending role summary mismatch: %+v", pending)
}
}

View File

@ -14,6 +14,28 @@ import (
type UserRepository interface { type UserRepository interface {
// GetUser 查询单个用户主状态。 // GetUser 查询单个用户主状态。
GetUser(ctx context.Context, userID int64) (userdomain.User, error) GetUser(ctx context.Context, userID int64) (userdomain.User, error)
// GetMyProfileStats 读取我的页顶部统计 read model。
GetMyProfileStats(ctx context.Context, userID int64) (userdomain.ProfileStats, error)
// RecordProfileVisit 记录一次用户主页访问,并更新被访问者计数。
RecordProfileVisit(ctx context.Context, visitorUserID int64, targetUserID int64, nowMs int64) (bool, userdomain.ProfileStats, error)
// ListProfileVisitors 分页读取访问当前用户主页的人。
ListProfileVisitors(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.ProfileVisitRecord, int64, error)
// FollowUser 创建或恢复单向关注,并更新关注计数。
FollowUser(ctx context.Context, followerUserID int64, followeeUserID int64, nowMs int64) (userdomain.ProfileStats, error)
// UnfollowUser 取消单向关注,并更新关注计数。
UnfollowUser(ctx context.Context, followerUserID int64, followeeUserID int64, nowMs int64) (userdomain.ProfileStats, error)
// ListFollowing 分页读取当前用户关注的人。
ListFollowing(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.FollowRecord, int64, error)
// ApplyFriend 创建好友申请;已有好友时不重复建申请。
ApplyFriend(ctx context.Context, requesterUserID int64, targetUserID int64, nowMs int64) (userdomain.FriendApplication, bool, error)
// AcceptFriendApplication 同意指定 requester 发来的好友申请,并写双向好友关系。
AcceptFriendApplication(ctx context.Context, accepterUserID int64, requesterUserID int64, nowMs int64) (userdomain.FriendRecord, error)
// DeleteFriend 删除双向好友关系。
DeleteFriend(ctx context.Context, userID int64, friendUserID int64, nowMs int64) (bool, error)
// ListFriends 分页读取好友列表。
ListFriends(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.FriendRecord, int64, error)
// ListFriendApplications 分页读取好友申请。
ListFriendApplications(ctx context.Context, userID int64, direction string, page int32, pageSize int32) ([]userdomain.FriendApplication, int64, error)
// BatchGetUsers 批量查询用户主状态,缺失用户不应返回占位对象。 // BatchGetUsers 批量查询用户主状态,缺失用户不应返回占位对象。
BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error) BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error)
// ListUserIDs 按稳定 user_id 游标读取低频后台任务目标用户。 // ListUserIDs 按稳定 user_id 游标读取低频后台任务目标用户。

View File

@ -137,6 +137,134 @@ func TestGetUserUsesRepository(t *testing.T) {
} }
} }
func TestGetMyProfileStatsReadsCounterTable(t *testing.T) {
repository := mysqltest.NewRepository(t)
svc := newUserService(repository)
stats, err := svc.GetMyProfileStats(context.Background(), 10001)
if err != nil {
t.Fatalf("GetMyProfileStats default failed: %v", err)
}
if stats.UserID != 10001 || stats.VisitorsCount != 0 || stats.FollowingCount != 0 || stats.FriendsCount != 0 {
t.Fatalf("missing stats should return zero projection: %+v", stats)
}
_, err = repository.RawDB().Exec(`
INSERT INTO user_profile_stats (
app_code, user_id, visitors_count, following_count, friends_count, updated_at_ms
) VALUES ('lalu', 10001, 12, 34, 56, 7000)`)
if err != nil {
t.Fatalf("seed profile stats failed: %v", err)
}
stats, err = svc.GetMyProfileStats(context.Background(), 10001)
if err != nil {
t.Fatalf("GetMyProfileStats failed: %v", err)
}
if stats.VisitorsCount != 12 || stats.FollowingCount != 34 || stats.FriendsCount != 56 || stats.UpdatedAtMs != 7000 {
t.Fatalf("stats mismatch: %+v", stats)
}
}
func TestSocialRelationsUseReadModelsAndIdempotentEdges(t *testing.T) {
repository := mysqltest.NewRepository(t)
for _, seeded := range []userdomain.User{
{UserID: 10001, CurrentDisplayUserID: "100001", Status: userdomain.StatusActive},
{UserID: 10002, CurrentDisplayUserID: "100002", Status: userdomain.StatusActive},
{UserID: 10003, CurrentDisplayUserID: "100003", Status: userdomain.StatusActive},
} {
repository.PutUser(completedUser(seeded))
}
now := time.UnixMilli(9000)
svc := newUserService(repository, userservice.WithClock(func() time.Time { return now }))
if _, _, err := svc.RecordProfileVisit(context.Background(), 10001, 10001); !xerr.IsCode(err, xerr.InvalidArgument) {
t.Fatalf("self visit should be rejected, got %v", err)
}
recorded, stats, err := svc.RecordProfileVisit(context.Background(), 10001, 10002)
if err != nil || !recorded || stats.VisitorsCount != 1 {
t.Fatalf("first visit mismatch: recorded=%v stats=%+v err=%v", recorded, stats, err)
}
recorded, stats, err = svc.RecordProfileVisit(context.Background(), 10001, 10002)
if err != nil || recorded || stats.VisitorsCount != 1 {
t.Fatalf("repeat visit should not increase unique visitors: recorded=%v stats=%+v err=%v", recorded, stats, err)
}
visitors, total, err := svc.ListProfileVisitors(context.Background(), 10002, 1, 20)
if err != nil || total != 1 || len(visitors) != 1 || visitors[0].VisitCount != 2 || visitors[0].VisitorUserID != 10001 {
t.Fatalf("visitors mismatch: visitors=%+v total=%d err=%v", visitors, total, err)
}
stats, err = svc.FollowUser(context.Background(), 10001, 10002)
if err != nil || stats.FollowingCount != 1 {
t.Fatalf("follow failed: stats=%+v err=%v", stats, err)
}
stats, err = svc.FollowUser(context.Background(), 10001, 10002)
if err != nil || stats.FollowingCount != 1 {
t.Fatalf("repeat follow should be idempotent: stats=%+v err=%v", stats, err)
}
following, total, err := svc.ListFollowing(context.Background(), 10001, 1, 20)
if err != nil || total != 1 || len(following) != 1 || following[0].FolloweeUserID != 10002 {
t.Fatalf("following mismatch: following=%+v total=%d err=%v", following, total, err)
}
stats, err = svc.UnfollowUser(context.Background(), 10001, 10002)
if err != nil || stats.FollowingCount != 0 {
t.Fatalf("unfollow failed: stats=%+v err=%v", stats, err)
}
stats, err = svc.UnfollowUser(context.Background(), 10001, 10002)
if err != nil || stats.FollowingCount != 0 {
t.Fatalf("repeat unfollow should stay zero: stats=%+v err=%v", stats, err)
}
application, alreadyFriends, err := svc.ApplyFriend(context.Background(), 10001, 10002)
if err != nil || alreadyFriends || application.Status != userdomain.FriendApplicationStatusPending {
t.Fatalf("apply friend mismatch: app=%+v already=%v err=%v", application, alreadyFriends, err)
}
friends, total, err := svc.ListFriends(context.Background(), 10001, 1, 20)
if err != nil || total != 0 || len(friends) != 0 {
t.Fatalf("friend application must not create friendship before accept: friends=%+v total=%d err=%v", friends, total, err)
}
applications, total, err := svc.ListFriendApplications(context.Background(), 10002, "incoming", 1, 20)
if err != nil || total != 1 || len(applications) != 1 || applications[0].RequesterUserID != 10001 {
t.Fatalf("incoming application mismatch: applications=%+v total=%d err=%v", applications, total, err)
}
friend, err := svc.AcceptFriendApplication(context.Background(), 10002, 10001)
if err != nil || friend.UserID != 10002 || friend.FriendUserID != 10001 {
t.Fatalf("accept friend failed: friend=%+v err=%v", friend, err)
}
friend, err = svc.AcceptFriendApplication(context.Background(), 10002, 10001)
if err != nil || friend.UserID != 10002 || friend.FriendUserID != 10001 {
t.Fatalf("repeat accept should be idempotent: friend=%+v err=%v", friend, err)
}
for _, userID := range []int64{10001, 10002} {
friends, total, err = svc.ListFriends(context.Background(), userID, 1, 20)
if err != nil || total != 1 || len(friends) != 1 {
t.Fatalf("bidirectional friendship missing for user %d: friends=%+v total=%d err=%v", userID, friends, total, err)
}
stats, err = svc.GetMyProfileStats(context.Background(), userID)
if err != nil || stats.FriendsCount != 1 {
t.Fatalf("friend count mismatch for user %d: stats=%+v err=%v", userID, stats, err)
}
}
deleted, err := svc.DeleteFriend(context.Background(), 10001, 10002)
if err != nil || !deleted {
t.Fatalf("delete friend failed: deleted=%v err=%v", deleted, err)
}
deleted, err = svc.DeleteFriend(context.Background(), 10001, 10002)
if err != nil || deleted {
t.Fatalf("repeat delete should be idempotent false: deleted=%v err=%v", deleted, err)
}
for _, userID := range []int64{10001, 10002} {
friends, total, err = svc.ListFriends(context.Background(), userID, 1, 20)
if err != nil || total != 0 || len(friends) != 0 {
t.Fatalf("friend should be removed for user %d: friends=%+v total=%d err=%v", userID, friends, total, err)
}
stats, err = svc.GetMyProfileStats(context.Background(), userID)
if err != nil || stats.FriendsCount != 0 {
t.Fatalf("friend count should be decremented for user %d: stats=%+v err=%v", userID, stats, err)
}
}
}
func TestListUserIDsUsesCursorAndTargetFilters(t *testing.T) { func TestListUserIDsUsesCursorAndTargetFilters(t *testing.T) {
repository := mysqltest.NewRepository(t) repository := mysqltest.NewRepository(t)
for _, user := range []userdomain.User{ for _, user := range []userdomain.User{

View File

@ -0,0 +1,157 @@
package user
import (
"context"
"strings"
"hyapp/pkg/xerr"
userdomain "hyapp/services/user-service/internal/domain/user"
)
const (
defaultSocialPageSize = 20
maxSocialPageSize = 100
)
// RecordProfileVisit 记录访问关系;自己访问自己不进入访问计数。
func (s *Service) RecordProfileVisit(ctx context.Context, visitorUserID int64, targetUserID int64) (bool, userdomain.ProfileStats, error) {
if visitorUserID <= 0 || targetUserID <= 0 {
return false, userdomain.ProfileStats{}, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if visitorUserID == targetUserID {
return false, userdomain.ProfileStats{}, xerr.New(xerr.InvalidArgument, "cannot visit self")
}
if s.userRepository == nil {
return false, userdomain.ProfileStats{}, xerr.New(xerr.Unavailable, "user repository is not configured")
}
return s.userRepository.RecordProfileVisit(ctx, visitorUserID, targetUserID, s.now().UnixMilli())
}
func (s *Service) ListProfileVisitors(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.ProfileVisitRecord, int64, error) {
if userID <= 0 {
return nil, 0, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if s.userRepository == nil {
return nil, 0, xerr.New(xerr.Unavailable, "user repository is not configured")
}
page, pageSize = normalizeSocialPage(page, pageSize)
return s.userRepository.ListProfileVisitors(ctx, userID, page, pageSize)
}
func (s *Service) FollowUser(ctx context.Context, followerUserID int64, followeeUserID int64) (userdomain.ProfileStats, error) {
if followerUserID <= 0 || followeeUserID <= 0 {
return userdomain.ProfileStats{}, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if followerUserID == followeeUserID {
return userdomain.ProfileStats{}, xerr.New(xerr.InvalidArgument, "cannot follow self")
}
if s.userRepository == nil {
return userdomain.ProfileStats{}, xerr.New(xerr.Unavailable, "user repository is not configured")
}
return s.userRepository.FollowUser(ctx, followerUserID, followeeUserID, s.now().UnixMilli())
}
func (s *Service) UnfollowUser(ctx context.Context, followerUserID int64, followeeUserID int64) (userdomain.ProfileStats, error) {
if followerUserID <= 0 || followeeUserID <= 0 {
return userdomain.ProfileStats{}, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if followerUserID == followeeUserID {
return userdomain.ProfileStats{}, xerr.New(xerr.InvalidArgument, "cannot unfollow self")
}
if s.userRepository == nil {
return userdomain.ProfileStats{}, xerr.New(xerr.Unavailable, "user repository is not configured")
}
return s.userRepository.UnfollowUser(ctx, followerUserID, followeeUserID, s.now().UnixMilli())
}
func (s *Service) ListFollowing(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.FollowRecord, int64, error) {
if userID <= 0 {
return nil, 0, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if s.userRepository == nil {
return nil, 0, xerr.New(xerr.Unavailable, "user repository is not configured")
}
page, pageSize = normalizeSocialPage(page, pageSize)
return s.userRepository.ListFollowing(ctx, userID, page, pageSize)
}
func (s *Service) ApplyFriend(ctx context.Context, requesterUserID int64, targetUserID int64) (userdomain.FriendApplication, bool, error) {
if requesterUserID <= 0 || targetUserID <= 0 {
return userdomain.FriendApplication{}, false, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if requesterUserID == targetUserID {
return userdomain.FriendApplication{}, false, xerr.New(xerr.InvalidArgument, "cannot add self as friend")
}
if s.userRepository == nil {
return userdomain.FriendApplication{}, false, xerr.New(xerr.Unavailable, "user repository is not configured")
}
return s.userRepository.ApplyFriend(ctx, requesterUserID, targetUserID, s.now().UnixMilli())
}
func (s *Service) AcceptFriendApplication(ctx context.Context, accepterUserID int64, requesterUserID int64) (userdomain.FriendRecord, error) {
if accepterUserID <= 0 || requesterUserID <= 0 {
return userdomain.FriendRecord{}, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if accepterUserID == requesterUserID {
return userdomain.FriendRecord{}, xerr.New(xerr.InvalidArgument, "cannot accept self friend request")
}
if s.userRepository == nil {
return userdomain.FriendRecord{}, xerr.New(xerr.Unavailable, "user repository is not configured")
}
return s.userRepository.AcceptFriendApplication(ctx, accepterUserID, requesterUserID, s.now().UnixMilli())
}
func (s *Service) DeleteFriend(ctx context.Context, userID int64, friendUserID int64) (bool, error) {
if userID <= 0 || friendUserID <= 0 {
return false, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if userID == friendUserID {
return false, xerr.New(xerr.InvalidArgument, "cannot delete self friend")
}
if s.userRepository == nil {
return false, xerr.New(xerr.Unavailable, "user repository is not configured")
}
return s.userRepository.DeleteFriend(ctx, userID, friendUserID, s.now().UnixMilli())
}
func (s *Service) ListFriends(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.FriendRecord, int64, error) {
if userID <= 0 {
return nil, 0, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if s.userRepository == nil {
return nil, 0, xerr.New(xerr.Unavailable, "user repository is not configured")
}
page, pageSize = normalizeSocialPage(page, pageSize)
return s.userRepository.ListFriends(ctx, userID, page, pageSize)
}
func (s *Service) ListFriendApplications(ctx context.Context, userID int64, direction string, page int32, pageSize int32) ([]userdomain.FriendApplication, int64, error) {
if userID <= 0 {
return nil, 0, xerr.New(xerr.InvalidArgument, "user_id is required")
}
direction = strings.ToLower(strings.TrimSpace(direction))
if direction == "" {
direction = "incoming"
}
if direction != "incoming" && direction != "outgoing" {
return nil, 0, xerr.New(xerr.InvalidArgument, "direction is invalid")
}
if s.userRepository == nil {
return nil, 0, xerr.New(xerr.Unavailable, "user repository is not configured")
}
page, pageSize = normalizeSocialPage(page, pageSize)
return s.userRepository.ListFriendApplications(ctx, userID, direction, page, pageSize)
}
func normalizeSocialPage(page int32, pageSize int32) (int32, int32) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = defaultSocialPageSize
}
if pageSize > maxSocialPageSize {
pageSize = maxSocialPageSize
}
return page, pageSize
}

View File

@ -35,6 +35,17 @@ func (s *Service) GetUser(ctx context.Context, userID int64) (userdomain.User, e
return s.refreshExpiredUser(ctx, user, "") return s.refreshExpiredUser(ctx, user, "")
} }
// GetMyProfileStats 读取我的页顶部统计计数;统计事实必须由专门 read model 维护。
func (s *Service) GetMyProfileStats(ctx context.Context, userID int64) (userdomain.ProfileStats, error) {
if userID <= 0 {
return userdomain.ProfileStats{}, xerr.New(xerr.InvalidArgument, "user_id is required")
}
if s.userRepository == nil {
return userdomain.ProfileStats{}, xerr.New(xerr.Unavailable, "user repository is not configured")
}
return s.userRepository.GetMyProfileStats(ctx, userID)
}
// BatchGetUsers 批量查询用户主状态。 // BatchGetUsers 批量查询用户主状态。
func (s *Service) BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error) { func (s *Service) BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error) {
if len(userIDs) == 0 { if len(userIDs) == 0 {

View File

@ -69,6 +69,84 @@ func (r *Repository) GetCoinSellerProfile(ctx context.Context, userID int64) (ho
return queryCoinSellerProfile(ctx, r.db, "WHERE user_id = ?", userID) return queryCoinSellerProfile(ctx, r.db, "WHERE user_id = ?", userID)
} }
// GetUserRoleSummary 用一组索引点查构造我的页角色摘要。
// 摘要只表达入口显隐需要的当前身份,不加载成员列表、团队统计或薪资数据。
func (r *Repository) GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error) {
app := appcode.FromContext(ctx)
summary := hostdomain.UserRoleSummary{UserID: userID}
var hostStatus string
var hostSource string
var hostAgencyID int64
var ownerAgencyID int64
var bdUserID int64
var bdRole string
var bdStatus string
var coinSellerStatus string
var merchantAssetType string
err := r.db.QueryRowContext(ctx, `
SELECT
COALESCE(h.status, ''),
COALESCE(h.source, ''),
COALESCE(h.current_agency_id, 0),
COALESCE(a.agency_id, 0),
COALESCE(b.user_id, 0),
COALESCE(b.role, ''),
COALESCE(b.status, ''),
COALESCE(c.status, ''),
COALESCE(c.merchant_asset_type, '')
FROM (SELECT ? AS user_id) u
LEFT JOIN host_profiles h
ON h.app_code = ? AND h.user_id = u.user_id
LEFT JOIN agencies a
ON a.app_code = ? AND a.owner_user_id = u.user_id AND a.status = ?
LEFT JOIN bd_profiles b
ON b.app_code = ? AND b.user_id = u.user_id
LEFT JOIN coin_seller_profiles c
ON c.app_code = ? AND c.user_id = u.user_id`,
userID,
app,
app, hostdomain.AgencyStatusActive,
app,
app,
).Scan(&hostStatus, &hostSource, &hostAgencyID, &ownerAgencyID, &bdUserID, &bdRole, &bdStatus, &coinSellerStatus, &merchantAssetType)
if err != nil {
return hostdomain.UserRoleSummary{}, err
}
summary.HostStatus = hostStatus
summary.IsHost = hostStatus == hostdomain.HostStatusActive
if ownerAgencyID > 0 {
summary.AgencyID = ownerAgencyID
} else {
summary.AgencyID = hostAgencyID
}
summary.IsAgency = ownerAgencyID > 0 || (summary.IsHost && (hostSource == hostdomain.HostSourceAgencyInvitation || hostSource == hostdomain.HostSourceAdminCreateAgency))
summary.BDStatus = bdStatus
if bdUserID > 0 && bdStatus == hostdomain.BDStatusActive {
summary.BDID = bdUserID
switch bdRole {
case hostdomain.BDRoleLeader:
summary.IsBD = true
summary.IsBDLeader = true
case hostdomain.BDRoleBD:
summary.IsBD = true
}
}
summary.CoinSellerStatus = coinSellerStatus
summary.IsCoinSeller = coinSellerStatus == hostdomain.CoinSellerStatusActive && merchantAssetType == hostdomain.CoinSellerMerchantAssetType
if err := r.db.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM role_invitations
WHERE app_code = ? AND target_user_id = ? AND status = ?`,
app, userID, hostdomain.InvitationStatusPending,
).Scan(&summary.PendingRoleInvitations); err != nil {
return hostdomain.UserRoleSummary{}, err
}
return summary, nil
}
// ListAgencyMembers 读取 Agency 成员关系列表。 // ListAgencyMembers 读取 Agency 成员关系列表。
func (r *Repository) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) { func (r *Repository) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) {
query := fmt.Sprintf(`SELECT %s FROM agency_memberships WHERE app_code = ? AND agency_id = ?`, agencyMembershipColumns) query := fmt.Sprintf(`SELECT %s FROM agency_memberships WHERE app_code = ? AND agency_id = ?`, agencyMembershipColumns)

View File

@ -32,6 +32,28 @@ func (r *Repository) GetUser(ctx context.Context, userID int64) (userdomain.User
return user, nil return user, nil
} }
// GetMyProfileStats 只读取预聚合统计表,不能实时扫描访客、关注或好友明细。
func (r *Repository) GetMyProfileStats(ctx context.Context, userID int64) (userdomain.ProfileStats, error) {
if r == nil || r.db == nil {
return userdomain.ProfileStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
appCode := appcode.FromContext(ctx)
stats := userdomain.ProfileStats{AppCode: appCode, UserID: userID}
err := r.db.QueryRowContext(ctx, `
SELECT visitors_count, following_count, friends_count, updated_at_ms
FROM user_profile_stats
WHERE app_code = ? AND user_id = ?`,
appCode, userID,
).Scan(&stats.VisitorsCount, &stats.FollowingCount, &stats.FriendsCount, &stats.UpdatedAtMs)
if err == sql.ErrNoRows {
return stats, nil
}
if err != nil {
return userdomain.ProfileStats{}, err
}
return stats, nil
}
// BatchGetUsers 批量读取用户主状态,缺失用户不会出现在返回 map 中。 // BatchGetUsers 批量读取用户主状态,缺失用户不会出现在返回 map 中。
func (r *Repository) BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error) { func (r *Repository) BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error) {

Some files were not shown because too many files have changed in this diff Show More