增加房间页面 我的, 以及我的页 聚合
This commit is contained in:
parent
a19857a4b5
commit
b95224bff1
@ -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
@ -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
|
||||||
|
|||||||
@ -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
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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{
|
||||||
|
|||||||
@ -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
@ -56,6 +56,23 @@ message CoinSellerProfile {
|
|||||||
int64 updated_at_ms = 6;
|
int64 updated_at_ms = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserRoleSummary 是 App 我的页和入口显隐使用的轻量角色摘要。
|
||||||
|
// 它把 host、Agency、BD 和币商身份收敛到一次 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
@ -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);
|
||||||
|
|||||||
@ -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
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
@ -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
|
||||||
@ -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 为准。
|
|
||||||
@ -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_id,room-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"
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
@ -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 反推历史归属。
|
||||||
|
|
||||||
交付:
|
交付:
|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
@ -24,7 +24,7 @@
|
|||||||
健康检查规则见:
|
健康检查规则见:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
docs/health-checks.md
|
docs/健康检查.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Shutdown State Machine
|
## Shutdown State Machine
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# Health Check Development Guide
|
# Health Check Development Guide
|
||||||
|
|
||||||
本文档只定义部署稳定性需要的健康检查能力。健康检查不承载业务判断,不替代监控告警,也不用于修复故障。进程下线和 drain 细节见 `docs/graceful-shutdown.md`。
|
本文档只定义部署稳定性需要的健康检查能力。健康检查不承载业务判断,不替代监控告警,也不用于修复故障。进程下线和 drain 细节见 `docs/优雅停机.md`。
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
185
docs/我的页概览接口.md
Normal file
185
docs/我的页概览接口.md
Normal 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 |
|
||||||
@ -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` | 创建国家主数据 |
|
||||||
@ -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 骨架。
|
||||||
@ -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
113
docs/社交关系接口.md
Normal 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` 找回。
|
||||||
@ -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`。
|
||||||
@ -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 |
|
||||||
@ -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
|
||||||
@ -5,7 +5,7 @@
|
|||||||
Room Cell 的完整状态机见:
|
Room Cell 的完整状态机见:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
docs/room-cell-state-machines.md
|
docs/房间Cell状态机.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
@ -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。
|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
@ -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"),
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 注册表解析 client;gateway 用它把包名转换成内部 app_code。
|
// SetAppRegistryClient 注入 App 注册表解析 client;gateway 用它把包名转换成内部 app_code。
|
||||||
func (h *Handler) SetAppRegistryClient(appRegistryClient client.AppRegistryClient) {
|
func (h *Handler) SetAppRegistryClient(appRegistryClient client.AppRegistryClient) {
|
||||||
h.appRegistryClient = appRegistryClient
|
h.appRegistryClient = appRegistryClient
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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})
|
||||||
|
|||||||
@ -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_id;presence 和麦位状态由 room-service 用读模型和 Room Cell 快照判断。
|
// gateway 只传鉴权 user_id;presence 和麦位状态由 room-service 用读模型和 Room Cell 快照判断。
|
||||||
func (h *Handler) getCurrentRoom(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getCurrentRoom(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
|||||||
@ -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{}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
136
services/gateway-service/internal/transport/http/vip_handler.go
Normal file
136
services/gateway-service/internal/transport/http/vip_handler.go
Normal 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
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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 只支持公共发现列表 tab,Mine 关系流走 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
|
||||||
|
}
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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,不扫描房间列表或快照。
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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,只在读模型命中后用快照做二次校验。
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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。
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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 游标读取低频后台任务目标用户。
|
||||||
|
|||||||
@ -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{
|
||||||
|
|||||||
157
services/user-service/internal/service/user/social.go
Normal file
157
services/user-service/internal/service/user/social.go
Normal 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
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
Loading…
x
Reference in New Issue
Block a user