增加房间页面 我的, 以及我的页 聚合
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 的实际消费链路按独立消费位点接入。
|
||||
- `cron-service` 当前负责后台调度、任务运行记录,以及通过内部 gRPC 触发 owner 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
|
||||
docs/voice-room-basic-loop-implementation.md
|
||||
docs/语音房基础闭环实现.md
|
||||
```
|
||||
|
||||
## 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.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.2
|
||||
// - protoc v5.27.3
|
||||
// source: proto/activity/v1/activity.proto
|
||||
|
||||
package activityv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.34.2
|
||||
// protoc v5.29.2
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc v5.27.3
|
||||
// source: proto/events/room/v1/events.proto
|
||||
|
||||
package roomeventsv1
|
||||
@ -38,11 +38,9 @@ type EventEnvelope struct {
|
||||
|
||||
func (x *EventEnvelope) Reset() {
|
||||
*x = EventEnvelope{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EventEnvelope) String() string {
|
||||
@ -53,7 +51,7 @@ func (*EventEnvelope) ProtoMessage() {}
|
||||
|
||||
func (x *EventEnvelope) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -135,11 +133,9 @@ type RoomCreated struct {
|
||||
|
||||
func (x *RoomCreated) Reset() {
|
||||
*x = RoomCreated{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomCreated) String() string {
|
||||
@ -150,7 +146,7 @@ func (*RoomCreated) ProtoMessage() {}
|
||||
|
||||
func (x *RoomCreated) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -226,11 +222,9 @@ type RoomUserJoined struct {
|
||||
|
||||
func (x *RoomUserJoined) Reset() {
|
||||
*x = RoomUserJoined{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomUserJoined) String() string {
|
||||
@ -241,7 +235,7 @@ func (*RoomUserJoined) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserJoined) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -281,11 +275,9 @@ type RoomUserLeft struct {
|
||||
|
||||
func (x *RoomUserLeft) Reset() {
|
||||
*x = RoomUserLeft{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomUserLeft) String() string {
|
||||
@ -296,7 +288,7 @@ func (*RoomUserLeft) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserLeft) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -330,11 +322,9 @@ type RoomClosed struct {
|
||||
|
||||
func (x *RoomClosed) Reset() {
|
||||
*x = RoomClosed{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomClosed) String() string {
|
||||
@ -345,7 +335,7 @@ func (*RoomClosed) ProtoMessage() {}
|
||||
|
||||
func (x *RoomClosed) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -397,11 +387,9 @@ type RoomMicChanged struct {
|
||||
|
||||
func (x *RoomMicChanged) Reset() {
|
||||
*x = RoomMicChanged{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomMicChanged) String() string {
|
||||
@ -412,7 +400,7 @@ func (*RoomMicChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomMicChanged) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -510,11 +498,9 @@ type RoomMicSeatLocked struct {
|
||||
|
||||
func (x *RoomMicSeatLocked) Reset() {
|
||||
*x = RoomMicSeatLocked{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomMicSeatLocked) String() string {
|
||||
@ -525,7 +511,7 @@ func (*RoomMicSeatLocked) ProtoMessage() {}
|
||||
|
||||
func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -573,11 +559,9 @@ type RoomChatEnabledChanged struct {
|
||||
|
||||
func (x *RoomChatEnabledChanged) Reset() {
|
||||
*x = RoomChatEnabledChanged{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomChatEnabledChanged) String() string {
|
||||
@ -588,7 +572,7 @@ func (*RoomChatEnabledChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -630,11 +614,9 @@ type RoomAdminChanged struct {
|
||||
|
||||
func (x *RoomAdminChanged) Reset() {
|
||||
*x = RoomAdminChanged{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomAdminChanged) String() string {
|
||||
@ -645,7 +627,7 @@ func (*RoomAdminChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -694,11 +676,9 @@ type RoomHostTransferred struct {
|
||||
|
||||
func (x *RoomHostTransferred) Reset() {
|
||||
*x = RoomHostTransferred{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomHostTransferred) String() string {
|
||||
@ -709,7 +689,7 @@ func (*RoomHostTransferred) ProtoMessage() {}
|
||||
|
||||
func (x *RoomHostTransferred) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -758,11 +738,9 @@ type RoomUserMuted struct {
|
||||
|
||||
func (x *RoomUserMuted) Reset() {
|
||||
*x = RoomUserMuted{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomUserMuted) String() string {
|
||||
@ -773,7 +751,7 @@ func (*RoomUserMuted) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -821,11 +799,9 @@ type RoomUserKicked struct {
|
||||
|
||||
func (x *RoomUserKicked) Reset() {
|
||||
*x = RoomUserKicked{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomUserKicked) String() string {
|
||||
@ -836,7 +812,7 @@ func (*RoomUserKicked) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -877,11 +853,9 @@ type RoomUserUnbanned struct {
|
||||
|
||||
func (x *RoomUserUnbanned) Reset() {
|
||||
*x = RoomUserUnbanned{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomUserUnbanned) String() string {
|
||||
@ -892,7 +866,7 @@ func (*RoomUserUnbanned) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -939,11 +913,9 @@ type RoomGiftSent struct {
|
||||
|
||||
func (x *RoomGiftSent) Reset() {
|
||||
*x = RoomGiftSent{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomGiftSent) String() string {
|
||||
@ -954,7 +926,7 @@ func (*RoomGiftSent) ProtoMessage() {}
|
||||
|
||||
func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1037,11 +1009,9 @@ type RoomHeatChanged struct {
|
||||
|
||||
func (x *RoomHeatChanged) Reset() {
|
||||
*x = RoomHeatChanged{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomHeatChanged) String() string {
|
||||
@ -1052,7 +1022,7 @@ func (*RoomHeatChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1094,11 +1064,9 @@ type RoomRankChanged struct {
|
||||
|
||||
func (x *RoomRankChanged) Reset() {
|
||||
*x = RoomRankChanged{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomRankChanged) String() string {
|
||||
@ -1109,7 +1077,7 @@ func (*RoomRankChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
|
||||
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))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
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 {
|
||||
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{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -383,6 +383,18 @@ message ListRoomsRequest {
|
||||
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 状态。
|
||||
message RoomListItem {
|
||||
string room_id = 1;
|
||||
@ -407,6 +419,21 @@ message ListRoomsResponse {
|
||||
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。
|
||||
// 该查询必须由 gateway 写入鉴权后的 user_id,客户端不能提交 user_id。
|
||||
message GetCurrentRoomRequest {
|
||||
@ -473,6 +500,8 @@ service RoomGuardService {
|
||||
// RoomQueryService 承载不会改变 Room Cell 状态的读模型查询。
|
||||
service RoomQueryService {
|
||||
rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse);
|
||||
rpc ListRoomFeeds(ListRoomFeedsRequest) returns (ListRoomsResponse);
|
||||
rpc GetMyRoom(GetMyRoomRequest) returns (GetMyRoomResponse);
|
||||
rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse);
|
||||
rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.2
|
||||
// - protoc v5.27.3
|
||||
// source: proto/room/v1/room.proto
|
||||
|
||||
package roomv1
|
||||
@ -916,6 +916,8 @@ var RoomGuardService_ServiceDesc = grpc.ServiceDesc{
|
||||
|
||||
const (
|
||||
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_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot"
|
||||
)
|
||||
@ -927,6 +929,8 @@ const (
|
||||
// RoomQueryService 承载不会改变 Room Cell 状态的读模型查询。
|
||||
type RoomQueryServiceClient interface {
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetCurrentRoomResponse)
|
||||
@ -976,6 +1000,8 @@ func (c *roomQueryServiceClient) GetRoomSnapshot(ctx context.Context, in *GetRoo
|
||||
// RoomQueryService 承载不会改变 Room Cell 状态的读模型查询。
|
||||
type RoomQueryServiceServer interface {
|
||||
ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error)
|
||||
ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error)
|
||||
GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error)
|
||||
GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error)
|
||||
GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error)
|
||||
mustEmbedUnimplementedRoomQueryServiceServer()
|
||||
@ -991,6 +1017,12 @@ type UnimplementedRoomQueryServiceServer struct{}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
in := new(GetCurrentRoomRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -1083,6 +1151,14 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ListRooms",
|
||||
Handler: _RoomQueryService_ListRooms_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListRoomFeeds",
|
||||
Handler: _RoomQueryService_ListRoomFeeds_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetMyRoom",
|
||||
Handler: _RoomQueryService_GetMyRoom_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetCurrentRoom",
|
||||
Handler: _RoomQueryService_GetCurrentRoom_Handler,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.34.2
|
||||
// protoc v5.29.2
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc v5.27.3
|
||||
// source: proto/user/v1/auth.proto
|
||||
|
||||
package userv1
|
||||
@ -33,11 +33,9 @@ type LoginPasswordRequest struct {
|
||||
|
||||
func (x *LoginPasswordRequest) Reset() {
|
||||
*x = LoginPasswordRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LoginPasswordRequest) String() string {
|
||||
@ -48,7 +46,7 @@ func (*LoginPasswordRequest) ProtoMessage() {}
|
||||
|
||||
func (x *LoginPasswordRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -114,11 +112,9 @@ type LoginThirdPartyRequest struct {
|
||||
|
||||
func (x *LoginThirdPartyRequest) Reset() {
|
||||
*x = LoginThirdPartyRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LoginThirdPartyRequest) String() string {
|
||||
@ -129,7 +125,7 @@ func (*LoginThirdPartyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *LoginThirdPartyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -298,11 +294,9 @@ type AuthResponse struct {
|
||||
|
||||
func (x *AuthResponse) Reset() {
|
||||
*x = AuthResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthResponse) String() string {
|
||||
@ -313,7 +307,7 @@ func (*AuthResponse) ProtoMessage() {}
|
||||
|
||||
func (x *AuthResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -369,11 +363,9 @@ type SetPasswordRequest struct {
|
||||
|
||||
func (x *SetPasswordRequest) Reset() {
|
||||
*x = SetPasswordRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SetPasswordRequest) String() string {
|
||||
@ -384,7 +376,7 @@ func (*SetPasswordRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SetPasswordRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -431,11 +423,9 @@ type SetPasswordResponse struct {
|
||||
|
||||
func (x *SetPasswordResponse) Reset() {
|
||||
*x = SetPasswordResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SetPasswordResponse) String() string {
|
||||
@ -446,7 +436,7 @@ func (*SetPasswordResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SetPasswordResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -480,11 +470,9 @@ type RefreshTokenRequest struct {
|
||||
|
||||
func (x *RefreshTokenRequest) Reset() {
|
||||
*x = RefreshTokenRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RefreshTokenRequest) String() string {
|
||||
@ -495,7 +483,7 @@ func (*RefreshTokenRequest) ProtoMessage() {}
|
||||
|
||||
func (x *RefreshTokenRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[5]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -535,11 +523,9 @@ type RefreshTokenResponse struct {
|
||||
|
||||
func (x *RefreshTokenResponse) Reset() {
|
||||
*x = RefreshTokenResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RefreshTokenResponse) String() string {
|
||||
@ -550,7 +536,7 @@ func (*RefreshTokenResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RefreshTokenResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[6]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -585,11 +571,9 @@ type LogoutRequest struct {
|
||||
|
||||
func (x *LogoutRequest) Reset() {
|
||||
*x = LogoutRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LogoutRequest) String() string {
|
||||
@ -600,7 +584,7 @@ func (*LogoutRequest) ProtoMessage() {}
|
||||
|
||||
func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[7]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -647,11 +631,9 @@ type LogoutResponse struct {
|
||||
|
||||
func (x *LogoutResponse) Reset() {
|
||||
*x = LogoutResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LogoutResponse) String() string {
|
||||
@ -662,7 +644,7 @@ func (*LogoutResponse) ProtoMessage() {}
|
||||
|
||||
func (x *LogoutResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[8]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -700,11 +682,9 @@ type RecordLoginBlockedRequest struct {
|
||||
|
||||
func (x *RecordLoginBlockedRequest) Reset() {
|
||||
*x = RecordLoginBlockedRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RecordLoginBlockedRequest) String() string {
|
||||
@ -715,7 +695,7 @@ func (*RecordLoginBlockedRequest) ProtoMessage() {}
|
||||
|
||||
func (x *RecordLoginBlockedRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[9]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -783,11 +763,9 @@ type RecordLoginBlockedResponse struct {
|
||||
|
||||
func (x *RecordLoginBlockedResponse) Reset() {
|
||||
*x = RecordLoginBlockedResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RecordLoginBlockedResponse) String() string {
|
||||
@ -798,7 +776,7 @@ func (*RecordLoginBlockedResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RecordLoginBlockedResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_auth_proto_msgTypes[10]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
@ -1039,140 +1017,6 @@ func file_proto_user_v1_auth_proto_init() {
|
||||
return
|
||||
}
|
||||
file_proto_user_v1_user_proto_init()
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_proto_user_v1_auth_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*LoginPasswordRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proto_user_v1_auth_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*LoginThirdPartyRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proto_user_v1_auth_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*AuthResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proto_user_v1_auth_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*SetPasswordRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proto_user_v1_auth_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*SetPasswordResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proto_user_v1_auth_proto_msgTypes[5].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*RefreshTokenRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proto_user_v1_auth_proto_msgTypes[6].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*RefreshTokenResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proto_user_v1_auth_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*LogoutRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proto_user_v1_auth_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*LogoutResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proto_user_v1_auth_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*RecordLoginBlockedRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proto_user_v1_auth_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*RecordLoginBlockedResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.2
|
||||
// - protoc v5.27.3
|
||||
// source: proto/user/v1/auth.proto
|
||||
|
||||
package userv1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -56,6 +56,23 @@ message CoinSellerProfile {
|
||||
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 的有时效关系事实。
|
||||
message AgencyMembership {
|
||||
int64 membership_id = 1;
|
||||
@ -225,6 +242,15 @@ message GetCoinSellerProfileResponse {
|
||||
CoinSellerProfile coin_seller_profile = 1;
|
||||
}
|
||||
|
||||
message GetUserRoleSummaryRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
}
|
||||
|
||||
message GetUserRoleSummaryResponse {
|
||||
UserRoleSummary summary = 1;
|
||||
}
|
||||
|
||||
message GetAgencyMembersRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 agency_id = 2;
|
||||
@ -364,6 +390,7 @@ service UserHostService {
|
||||
rpc GetHostProfile(GetHostProfileRequest) returns (GetHostProfileResponse);
|
||||
rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse);
|
||||
rpc GetCoinSellerProfile(GetCoinSellerProfileRequest) returns (GetCoinSellerProfileResponse);
|
||||
rpc GetUserRoleSummary(GetUserRoleSummaryRequest) returns (GetUserRoleSummaryResponse);
|
||||
rpc GetAgencyMembers(GetAgencyMembersRequest) returns (GetAgencyMembersResponse);
|
||||
rpc GetAgencyApplications(GetAgencyApplicationsRequest) returns (GetAgencyApplicationsResponse);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.2
|
||||
// - protoc v5.27.3
|
||||
// source: proto/user/v1/host.proto
|
||||
|
||||
package userv1
|
||||
@ -29,6 +29,7 @@ const (
|
||||
UserHostService_GetHostProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetHostProfile"
|
||||
UserHostService_GetBDProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetBDProfile"
|
||||
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_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)
|
||||
GetBDProfile(ctx context.Context, in *GetBDProfileRequest, opts ...grpc.CallOption) (*GetBDProfileResponse, 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)
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetAgencyMembersResponse)
|
||||
@ -197,6 +209,7 @@ type UserHostServiceServer interface {
|
||||
GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, error)
|
||||
GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error)
|
||||
GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error)
|
||||
GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error)
|
||||
GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error)
|
||||
GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error)
|
||||
mustEmbedUnimplementedUserHostServiceServer()
|
||||
@ -239,6 +252,9 @@ func (UnimplementedUserHostServiceServer) GetBDProfile(context.Context, *GetBDPr
|
||||
func (UnimplementedUserHostServiceServer) GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
in := new(GetAgencyMembersRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -529,6 +563,10 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetCoinSellerProfile",
|
||||
Handler: _UserHostService_GetCoinSellerProfile_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetUserRoleSummary",
|
||||
Handler: _UserHostService_GetUserRoleSummary_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetAgencyMembers",
|
||||
Handler: _UserHostService_GetAgencyMembers_Handler,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -171,6 +171,166 @@ message GetUserResponse {
|
||||
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 批量查询用户主状态。
|
||||
message BatchGetUsersRequest {
|
||||
RequestMeta meta = 1;
|
||||
@ -461,6 +621,7 @@ message ExpirePrettyDisplayUserIDResponse {
|
||||
// UserService 提供用户主状态、基础资料和注册资料读写接口。
|
||||
service UserService {
|
||||
rpc GetUser(GetUserRequest) returns (GetUserResponse);
|
||||
rpc GetMyProfileStats(GetMyProfileStatsRequest) returns (GetMyProfileStatsResponse);
|
||||
rpc BatchGetUsers(BatchGetUsersRequest) returns (BatchGetUsersResponse);
|
||||
rpc ListUserIDs(ListUserIDsRequest) returns (ListUserIDsResponse);
|
||||
rpc GetUserMicLifetimeStats(GetUserMicLifetimeStatsRequest) returns (GetUserMicLifetimeStatsResponse);
|
||||
@ -469,6 +630,20 @@ service UserService {
|
||||
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 修改。
|
||||
service UserCronService {
|
||||
rpc ProcessLoginIPRiskBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.2
|
||||
// - protoc v5.27.3
|
||||
// source: proto/user/v1/user.proto
|
||||
|
||||
package userv1
|
||||
@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
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_ListUserIDs_FullMethodName = "/hyapp.user.v1.UserService/ListUserIDs"
|
||||
UserService_GetUserMicLifetimeStats_FullMethodName = "/hyapp.user.v1.UserService/GetUserMicLifetimeStats"
|
||||
@ -35,6 +36,7 @@ const (
|
||||
// UserService 提供用户主状态、基础资料和注册资料读写接口。
|
||||
type UserServiceClient interface {
|
||||
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)
|
||||
ListUserIDs(ctx context.Context, in *ListUserIDsRequest, opts ...grpc.CallOption) (*ListUserIDsResponse, 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
|
||||
}
|
||||
|
||||
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) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(BatchGetUsersResponse)
|
||||
@ -128,6 +140,7 @@ func (c *userServiceClient) CompleteOnboarding(ctx context.Context, in *Complete
|
||||
// UserService 提供用户主状态、基础资料和注册资料读写接口。
|
||||
type UserServiceServer interface {
|
||||
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
|
||||
GetMyProfileStats(context.Context, *GetMyProfileStatsRequest) (*GetMyProfileStatsResponse, error)
|
||||
BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersResponse, error)
|
||||
ListUserIDs(context.Context, *ListUserIDsRequest) (*ListUserIDsResponse, error)
|
||||
GetUserMicLifetimeStats(context.Context, *GetUserMicLifetimeStatsRequest) (*GetUserMicLifetimeStatsResponse, error)
|
||||
@ -147,6 +160,9 @@ type UnimplementedUserServiceServer struct{}
|
||||
func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
in := new(BatchGetUsersRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -323,6 +357,10 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetUser",
|
||||
Handler: _UserService_GetUser_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetMyProfileStats",
|
||||
Handler: _UserService_GetMyProfileStats_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BatchGetUsers",
|
||||
Handler: _UserService_BatchGetUsers_Handler,
|
||||
@ -352,6 +390,454 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
|
||||
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 (
|
||||
UserCronService_ProcessLoginIPRiskBatch_FullMethodName = "/hyapp.user.v1.UserCronService/ProcessLoginIPRiskBatch"
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 领取任务奖励时调用的钱包入账命令。
|
||||
message CreditTaskRewardRequest {
|
||||
string command_id = 1;
|
||||
@ -627,5 +828,14 @@ service WalletService {
|
||||
rpc EquipUserResource(EquipUserResourceRequest) returns (EquipUserResourceResponse);
|
||||
rpc ListResourceGrants(ListResourceGrantsRequest) returns (ListResourceGrantsResponse);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.29.2
|
||||
// - protoc v5.27.3
|
||||
// source: proto/wallet/v1/wallet.proto
|
||||
|
||||
package walletv1
|
||||
@ -44,6 +44,15 @@ const (
|
||||
WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource"
|
||||
WalletService_ListResourceGrants_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGrants"
|
||||
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"
|
||||
)
|
||||
|
||||
@ -78,6 +87,15 @@ type WalletServiceClient interface {
|
||||
EquipUserResource(ctx context.Context, in *EquipUserResourceRequest, opts ...grpc.CallOption) (*EquipUserResourceResponse, error)
|
||||
ListResourceGrants(ctx context.Context, in *ListResourceGrantsRequest, opts ...grpc.CallOption) (*ListResourceGrantsResponse, 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)
|
||||
}
|
||||
|
||||
@ -339,6 +357,96 @@ func (c *walletServiceClient) ListRechargeBills(ctx context.Context, in *ListRec
|
||||
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) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CreditTaskRewardResponse)
|
||||
@ -380,6 +488,15 @@ type WalletServiceServer interface {
|
||||
EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error)
|
||||
ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, 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)
|
||||
mustEmbedUnimplementedWalletServiceServer()
|
||||
}
|
||||
@ -466,6 +583,33 @@ func (UnimplementedWalletServiceServer) ListResourceGrants(context.Context, *Lis
|
||||
func (UnimplementedWalletServiceServer) ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
in := new(CreditTaskRewardRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -1065,6 +1371,42 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ListRechargeBills",
|
||||
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",
|
||||
Handler: _WalletService_CreditTaskReward_Handler,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
相关文档:
|
||||
|
||||
```text
|
||||
docs/voice-room-basic-loop-implementation.md
|
||||
docs/voice-room-client-api-flow.md
|
||||
docs/app-message-tab-architecture.md
|
||||
docs/room-outbox-compensation-development.md
|
||||
docs/语音房基础闭环实现.md
|
||||
docs/语音房客户端接口流程.md
|
||||
docs/App消息Tab架构.md
|
||||
docs/房间Outbox补偿开发.md
|
||||
```
|
||||
|
||||
## 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
|
||||
summary: 我的页首屏聚合摘要
|
||||
operationId: getMyOverview
|
||||
description: 聚合当前用户资料、固定三类钱包余额、Host/Agency/BD/币商身份、消息红点和入口显隐。该接口不查流水、不扫成员、不计算工资;钱包、角色和消息上游失败时按字段 `unavailable=true` 局部降级。
|
||||
description: 聚合当前用户资料、顶部计数、金币钱包卡片、VIP、Host/Agency/BD/币商身份和入口显隐。该接口不查消息红点、邀请码、钻石、美元余额、流水、成员或工资;钱包、VIP、计数和角色上游失败时按字段 `unavailable=true` 局部降级。
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
@ -911,7 +911,6 @@ paths:
|
||||
enum:
|
||||
- hot
|
||||
- new
|
||||
- me
|
||||
- name: cursor
|
||||
in: query
|
||||
required: false
|
||||
@ -943,6 +942,69 @@ paths:
|
||||
$ref: "#/responses/Forbidden"
|
||||
"502":
|
||||
$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:
|
||||
get:
|
||||
tags:
|
||||
@ -2421,14 +2483,15 @@ definitions:
|
||||
type: object
|
||||
required:
|
||||
- is_host
|
||||
- is_agent
|
||||
- is_agency
|
||||
- is_bd
|
||||
- is_bd_leader
|
||||
- is_coin_seller
|
||||
- pending_role_invitations
|
||||
properties:
|
||||
is_host:
|
||||
type: boolean
|
||||
is_agent:
|
||||
is_agency:
|
||||
type: boolean
|
||||
is_bd:
|
||||
type: boolean
|
||||
@ -2436,147 +2499,143 @@ definitions:
|
||||
type: boolean
|
||||
is_coin_seller:
|
||||
type: boolean
|
||||
unavailable:
|
||||
type: boolean
|
||||
description: 角色上游不可用时为 true;此时所有特权身份按 false 返回。
|
||||
MyOverviewAssetBalanceData:
|
||||
type: object
|
||||
required:
|
||||
- asset_type
|
||||
- available_amount
|
||||
- frozen_amount
|
||||
- version
|
||||
properties:
|
||||
asset_type:
|
||||
host_status:
|
||||
type: string
|
||||
enum:
|
||||
- COIN
|
||||
- DIAMOND
|
||||
- USD_BALANCE
|
||||
available_amount:
|
||||
agency_id:
|
||||
type: integer
|
||||
format: int64
|
||||
frozen_amount:
|
||||
bd_id:
|
||||
type: integer
|
||||
format: int64
|
||||
version:
|
||||
type: integer
|
||||
format: int64
|
||||
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:
|
||||
bd_status:
|
||||
type: string
|
||||
coin_seller_status:
|
||||
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:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 当前没有独立 counter 时返回 0,不扫邀请列表。
|
||||
unavailable:
|
||||
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:
|
||||
type: object
|
||||
required:
|
||||
- key
|
||||
- title
|
||||
- visible
|
||||
- enabled
|
||||
- route
|
||||
- icon_key
|
||||
- badge_count
|
||||
- order
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
enum:
|
||||
- wallet
|
||||
- host_center
|
||||
- store
|
||||
- bag
|
||||
- level
|
||||
- task
|
||||
- apply_host
|
||||
- host_center
|
||||
- agency_center
|
||||
- bd_center
|
||||
- bd_leader_center
|
||||
- coin_seller_center
|
||||
- backpack
|
||||
- settings
|
||||
- support
|
||||
title:
|
||||
type: string
|
||||
- language
|
||||
visible:
|
||||
type: boolean
|
||||
enabled:
|
||||
type: boolean
|
||||
route:
|
||||
type: string
|
||||
icon_key:
|
||||
type: string
|
||||
badge_count:
|
||||
type: integer
|
||||
format: int64
|
||||
order:
|
||||
type: integer
|
||||
format: int32
|
||||
MyOverviewData:
|
||||
type: object
|
||||
required:
|
||||
- profile
|
||||
- profile_stats
|
||||
- wallet
|
||||
- invite
|
||||
- vip
|
||||
- roles
|
||||
- badges
|
||||
- entries
|
||||
- server_time_ms
|
||||
properties:
|
||||
profile:
|
||||
$ref: "#/definitions/UserProfileData"
|
||||
profile_stats:
|
||||
$ref: "#/definitions/MyOverviewProfileStatsData"
|
||||
wallet:
|
||||
$ref: "#/definitions/MyOverviewWalletData"
|
||||
invite:
|
||||
$ref: "#/definitions/MyOverviewInviteData"
|
||||
vip:
|
||||
$ref: "#/definitions/MyOverviewVIPData"
|
||||
roles:
|
||||
$ref: "#/definitions/MyOverviewRolesData"
|
||||
badges:
|
||||
$ref: "#/definitions/MyOverviewBadgesData"
|
||||
entries:
|
||||
type: array
|
||||
items:
|
||||
@ -3697,6 +3756,19 @@ definitions:
|
||||
$ref: "#/definitions/RoomListItemData"
|
||||
next_cursor:
|
||||
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:
|
||||
type: object
|
||||
required:
|
||||
@ -4248,6 +4320,13 @@ definitions:
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/definitions/RoomListData"
|
||||
MyRoomEnvelope:
|
||||
allOf:
|
||||
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/definitions/MyRoomData"
|
||||
CurrentRoomEnvelope:
|
||||
allOf:
|
||||
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 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 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。
|
||||
|
||||
@ -284,7 +284,7 @@ stateDiagram-v2
|
||||
|
||||
### 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 `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 事实,不重复迁移。
|
||||
这属于允许改变用户区域的后台关系管理路径之一;完整来源见 [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` 并提交生成文件。
|
||||
|
||||
@ -466,7 +466,7 @@ Outbox consumers can update App message inbox, BI, risk systems, and export jobs
|
||||
|
||||
## 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 |
|
||||
| --- | --- | --- | --- |
|
||||
@ -495,7 +495,7 @@ Outbox consumers can update App message inbox, BI, risk systems, and export jobs
|
||||
|
||||
## 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
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 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 关系快照和有效上麦/礼物统计;如果先做工资后台,会得到一套没有可靠输入的数据系统。
|
||||
|
||||
@ -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 时通知目标用户。
|
||||
- BD/BD Leader 邀请 Agency 或 BD 时通知被邀请人。
|
||||
@ -111,7 +111,7 @@
|
||||
|
||||
## 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 归属和收益展示。
|
||||
|
||||
后台政策、工资周期、工资单审批、钱包入账和调整单实现单独放在 [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` 负责余额和提现。
|
||||
|
||||
@ -282,7 +282,7 @@ agency_memberships(
|
||||
用户所属区域来自 `user-service.users.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`。
|
||||
用户区域允许变化的完整来源以 [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 |
|
||||
| --- | --- |
|
||||
@ -390,7 +390,7 @@ Leader inviting Agency uses the same Agency creation flow as BD, but `parent_bd_
|
||||
|
||||
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
|
||||
|
||||
@ -585,7 +585,7 @@ Outbox consumers can update search indexes, notifications, BI, admin views, and
|
||||
|
||||
## 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 |
|
||||
| --- | --- | --- | --- |
|
||||
@ -616,7 +616,7 @@ Host/Agency/BD 的 App 可见通知统一进入 App `消息` tab 的 `system`
|
||||
|
||||
## 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
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
健康检查规则见:
|
||||
|
||||
```text
|
||||
docs/health-checks.md
|
||||
docs/健康检查.md
|
||||
```
|
||||
|
||||
## Shutdown State Machine
|
||||
@ -1,6 +1,6 @@
|
||||
# Health Check Development Guide
|
||||
|
||||
本文档只定义部署稳定性需要的健康检查能力。健康检查不承载业务判断,不替代监控告警,也不用于修复故障。进程下线和 drain 细节见 `docs/graceful-shutdown.md`。
|
||||
本文档只定义部署稳定性需要的健康检查能力。健康检查不承载业务判断,不替代监控告警,也不用于修复故障。进程下线和 drain 细节见 `docs/优雅停机.md`。
|
||||
|
||||
## Goals
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Lucky Gift Activity Service Development Guide
|
||||
|
||||
本文档定义幸运礼物在 `activity-service` 中的开发落地。产品规则见 `docs/lucky-gift-product.md`。
|
||||
本文档定义幸运礼物在 `activity-service` 中的开发落地。产品规则见 `docs/幸运礼物产品方案.md`。
|
||||
|
||||
## 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/host-identity` | users | `getMyHostIdentity` | 查询当前用户 Host/Agency/BD 身份 |
|
||||
| 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/profile/update` | users | `updateMyProfile` | 修改当前用户基础资料 |
|
||||
| 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/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/snapshot` | rooms | `getRoomSnapshot` | 查询房间完整快照 |
|
||||
| POST | `/api/v1/rooms/create` | rooms | `createRoom` | 创建房间 |
|
||||
@ -65,7 +78,16 @@
|
||||
| POST | `/api/v1/messages/read-all` | messages | `markInboxSectionRead` | 标记分区全部已读 |
|
||||
| POST | `/api/v1/messages/{message_id}/read` | messages | `markInboxMessageRead` | 标记单条消息已读 |
|
||||
| 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` | 币商给玩家转金币 |
|
||||
| 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
|
||||
|
||||
@ -201,11 +223,22 @@
|
||||
| POST | `/hyapp.user.v1.AuthService/RefreshToken` | auth | `userAuthRefreshToken` | 刷新 token |
|
||||
| POST | `/hyapp.user.v1.AuthService/Logout` | auth | `userAuthLogout` | 登出并失效 refresh session |
|
||||
| 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/ListUserIDs` | users | `userListUserIDs` | 后台任务按 user_id 游标查询目标用户 |
|
||||
| POST | `/hyapp.user.v1.UserService/UpdateUserProfile` | users | `userUpdateUserProfile` | 修改用户基础资料 |
|
||||
| POST | `/hyapp.user.v1.UserService/ChangeUserCountry` | users | `userChangeUserCountry` | 修改用户国家 |
|
||||
| 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/DeletePushToken` | user-devices | `userDeletePushToken` | 失效当前登录用户的系统推送 token |
|
||||
| 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 映射。
|
||||
3. 新增 `api/proto/user/v1/auth.proto` 和 `user.proto`。
|
||||
4. 补 `user-service` 的 app/service/domain/storage/transport 骨架。
|
||||
@ -1,6 +1,6 @@
|
||||
# Login And Registration Technical Design
|
||||
|
||||
本文档定义 v1 登录、三方注册登录、密码设置、邀请码归因和 token 生命周期。它只描述当前架构事实和接口边界,不记录实现流水账。邀请码详细架构见 [Registration Invite Code Architecture](./registration-invite-code-architecture.md)。
|
||||
本文档定义 v1 登录、三方注册登录、密码设置、邀请码归因和 token 生命周期。它只描述当前架构事实和接口边界,不记录实现流水账。邀请码详细架构见 [Registration Invite Code Architecture](./注册邀请码架构.md)。
|
||||
|
||||
## 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
|
||||
|
||||
1. 新增 `docs/tencent-rtc-token-uid-mapping-development.md` 并确认产品边界。
|
||||
1. 新增 `docs/腾讯RTC票据UID映射开发.md` 并确认产品边界。
|
||||
2. 抽出通用 Tencent UserSig 算法到 `pkg/tencentsig`,保持 IM usersig 测试通过。
|
||||
3. 新增 `pkg/tencentrtc`,定义 RTC token config/result 和 `GenerateToken`。
|
||||
4. 扩展 gateway config:`tencent_rtc`。
|
||||
@ -33,7 +33,7 @@
|
||||
| 靓号租约 | `DONE` | pretty display id lease 表和接口存在 | 购买/支付入口未形成完整商业闭环 |
|
||||
| 用户资料更新 | `DONE` | 支持 username/avatar/birth,国家独立接口 | 房间内资料变更实时刷新未做 |
|
||||
| 国家修改和审计 | `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
|
||||
|
||||
@ -139,7 +139,7 @@
|
||||
| --- | --- | --- | --- |
|
||||
| activity-service 底座 | `PARTIAL` | gRPC Ping/GetActivityStatus、MySQL activity/outbox 表存在 | 尚未消费 room outbox |
|
||||
| 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 | 活动规则、时间、奖池、风控配置未实现 |
|
||||
| LuckyGift Check | `TODO` | 文档有设计,proto/code 未实现 | `CheckLuckyGift` RPC 和规则校验 |
|
||||
| LuckyGift Draw | `TODO` | 文档有设计,proto/code 未实现 | 三层奖池、一层风控、幂等 draw |
|
||||
@ -142,7 +142,7 @@ sequenceDiagram
|
||||
- `visible_region_id` 来自创建者当前 `users.region_id`。
|
||||
- 创建者没有区域时写 `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`,并写审计。
|
||||
|
||||
### Why Not Dynamic By Owner Region
|
||||
@ -5,7 +5,7 @@
|
||||
Room Cell 的完整状态机见:
|
||||
|
||||
```text
|
||||
docs/room-cell-state-machines.md
|
||||
docs/房间Cell状态机.md
|
||||
```
|
||||
|
||||
## Scope
|
||||
@ -34,8 +34,13 @@ sequenceDiagram
|
||||
G-->>C: IM user_sig
|
||||
C->>IM: login(user_id, user_sig)
|
||||
|
||||
C->>G: GET /api/v1/rooms
|
||||
G->>R: ListRooms(viewer region from user-service)
|
||||
C->>G: GET /api/v1/rooms/me
|
||||
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
|
||||
G-->>C: room cards + im_group_id
|
||||
|
||||
@ -64,11 +69,12 @@ sequenceDiagram
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `GET /api/v1/im/usersig` | yes | 用户级 IM 登录票据,可在 App 启动后预取并按 TTL 复用 |
|
||||
| 2 | Tencent IM SDK `login` | yes | 客户端先建立 IM 身份,房间群仍等 JoinRoom 成功后再进 |
|
||||
| 3 | `GET /api/v1/rooms` | yes for discovery | 房间卡片返回 `im_group_id`,只用于展示和准备,不授予入群权限 |
|
||||
| 4 | `POST /api/v1/rooms/join` | yes | 建立业务 presence,并返回首屏房间数据和内嵌 RTC token |
|
||||
| 5 | Tencent IM SDK `joinGroup(im_group_id)` | yes | 必须在 JoinRoom 成功后执行,IM 回调守卫会校验 room-service presence |
|
||||
| 6 | Tencent RTC SDK `enterRoom` | yes for voice room audio | 使用 JoinRoom 响应里的 `rtc.token` 进入真实音频频道旁听 |
|
||||
| 7 | `POST /api/v1/rooms/heartbeat` | yes | 刷新业务 presence,避免 stale timeout |
|
||||
| 3 | `GET /api/v1/rooms/me` | yes for Mine top card | 直接读取 owner 房间权威元数据,不依赖发现页投影 |
|
||||
| 4 | `GET /api/v1/rooms/feeds?tab=visited\|friend\|following` | yes for Mine lists | Mine 列表 tab;`visited` 由 JoinRoom 成功写入,`friend/following` 读取关系流读模型 |
|
||||
| 5 | `POST /api/v1/rooms/join` | yes | 建立业务 presence,并返回首屏房间数据和内嵌 RTC token |
|
||||
| 6 | Tencent IM SDK `joinGroup(im_group_id)` | yes | 必须在 JoinRoom 成功后执行,IM 回调守卫会校验 room-service presence |
|
||||
| 7 | Tencent RTC SDK `enterRoom` | yes for voice room audio | 使用 JoinRoom 响应里的 `rtc.token` 进入真实音频频道旁听 |
|
||||
| 8 | `POST /api/v1/rooms/heartbeat` | yes | 刷新业务 presence,避免 stale timeout |
|
||||
|
||||
如果 JoinRoom 响应里的 `rtc.available=false`,客户端可以先展示房间页并重试 `POST /api/v1/rtc/token`;RTC 失败不能回滚已经成功的业务 presence。
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
本文定义钱包服务的目标架构、账务模型、核心流程和落地顺序。当前 `wallet-service` 只有单币种 `coin` 扣费能力,新需求需要升级为多资产账户、外部支付订单、币商充值、主播奖励和提现审核的一体化账务边界。
|
||||
|
||||
资源、资源组、用户资源赠送和礼物从资源库选择的边界见 [Resource And Resource Group Architecture](./resource-group-gift-architecture.md)。本文只保留钱包账务和礼物价格结算边界。
|
||||
资源、资源组、用户资源赠送和礼物从资源库选择的边界见 [Resource And Resource Group Architecture](./资源组与礼物架构.md)。本文只保留钱包账务和礼物价格结算边界。
|
||||
|
||||
## Goals
|
||||
|
||||
@ -72,6 +72,9 @@ var catalog = map[Code]Spec{
|
||||
CoinSellerStockAmountInvalid: spec(codes.InvalidArgument, httpStatusBadRequest, CoinSellerStockAmountInvalid, "invalid argument"),
|
||||
CoinSellerPaymentRefDuplicated: spec(codes.AlreadyExists, httpStatusConflict, CoinSellerPaymentRefDuplicated, "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"),
|
||||
EventAlreadyConsumed: spec(codes.AlreadyExists, httpStatusConflict, EventAlreadyConsumed, "conflict"),
|
||||
|
||||
@ -88,6 +88,12 @@ const (
|
||||
CoinSellerPaymentRefDuplicated Code = "COIN_SELLER_PAYMENT_REF_DUPLICATED"
|
||||
// IdempotencyConflict 表示 command_id 相同但业务 payload 不一致。
|
||||
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 Code = "RULE_NOT_ACTIVE"
|
||||
|
||||
@ -76,6 +76,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
var userDeviceClient client.UserDeviceClient = client.NewGRPCUserDeviceClient(userConn)
|
||||
var userCountryQueryClient client.UserCountryQueryClient = client.NewGRPCUserCountryQueryClient(userConn)
|
||||
var userHostClient client.UserHostClient = client.NewGRPCUserHostClient(userConn)
|
||||
var userSocialClient client.UserSocialClient = client.NewGRPCUserSocialClient(userConn)
|
||||
var appRegistryClient client.AppRegistryClient = client.NewGRPCAppRegistryClient(userConn)
|
||||
var walletClient client.WalletClient = client.NewGRPCWalletClient(walletConn)
|
||||
var messageClient client.MessageInboxClient = client.NewGRPCMessageInboxClient(activityConn)
|
||||
@ -100,6 +101,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
handler.SetUserDeviceClient(userDeviceClient)
|
||||
handler.SetUserCountryQueryClient(userCountryQueryClient)
|
||||
handler.SetUserHostClient(userHostClient)
|
||||
handler.SetUserSocialClient(userSocialClient)
|
||||
handler.SetAppRegistryClient(appRegistryClient)
|
||||
handler.SetWalletClient(walletClient)
|
||||
handler.SetMessageInboxClient(messageClient)
|
||||
|
||||
@ -38,6 +38,8 @@ type RoomGuardClient interface {
|
||||
// RoomQueryClient 抽象 gateway 对 room-service 读模型查询的依赖。
|
||||
type RoomQueryClient interface {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
return c.client.GetCurrentRoom(ctx, req)
|
||||
}
|
||||
|
||||
@ -29,12 +29,27 @@ type UserIdentityClient interface {
|
||||
// UserProfileClient 抽象 gateway 对 user-service 用户资料能力的依赖。
|
||||
type UserProfileClient interface {
|
||||
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)
|
||||
CompleteOnboarding(ctx context.Context, req *userv1.CompleteOnboardingRequest) (*userv1.CompleteOnboardingResponse, error)
|
||||
UpdateUserProfile(ctx context.Context, req *userv1.UpdateUserProfileRequest) (*userv1.UpdateUserProfileResponse, 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 的依赖。
|
||||
type UserDeviceClient interface {
|
||||
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)
|
||||
GetBDProfile(ctx context.Context, req *userv1.GetBDProfileRequest) (*userv1.GetBDProfileResponse, 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 注册表的解析依赖。
|
||||
@ -70,6 +86,10 @@ type grpcUserProfileClient struct {
|
||||
client userv1.UserServiceClient
|
||||
}
|
||||
|
||||
type grpcUserSocialClient struct {
|
||||
client userv1.UserSocialServiceClient
|
||||
}
|
||||
|
||||
type grpcUserDeviceClient struct {
|
||||
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。
|
||||
func NewGRPCUserDeviceClient(conn *grpc.ClientConn) UserDeviceClient {
|
||||
return &grpcUserDeviceClient{
|
||||
@ -179,6 +206,10 @@ func (c *grpcUserProfileClient) GetUser(ctx context.Context, req *userv1.GetUser
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
return c.client.ResolveApp(ctx, req)
|
||||
}
|
||||
|
||||
@ -11,6 +11,15 @@ import (
|
||||
// WalletClient 抽象 gateway 对 wallet-service 余额查询能力的依赖。
|
||||
type WalletClient interface {
|
||||
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)
|
||||
ListResources(ctx context.Context, req *walletv1.ListResourcesRequest) (*walletv1.ListResourcesResponse, 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
userIdentityClient client.UserIdentityClient
|
||||
userProfileClient client.UserProfileClient
|
||||
userSocialClient client.UserSocialClient
|
||||
userDeviceClient client.UserDeviceClient
|
||||
userCountryClient client.UserCountryQueryClient
|
||||
userHostClient client.UserHostClient
|
||||
@ -145,6 +146,11 @@ func (h *Handler) SetUserHostClient(userHostClient client.UserHostClient) {
|
||||
h.userHostClient = userHostClient
|
||||
}
|
||||
|
||||
// SetUserSocialClient 注入 user-service 访问、关注和好友关系 client。
|
||||
func (h *Handler) SetUserSocialClient(userSocialClient client.UserSocialClient) {
|
||||
h.userSocialClient = userSocialClient
|
||||
}
|
||||
|
||||
// SetAppRegistryClient 注入 App 注册表解析 client;gateway 用它把包名转换成内部 app_code。
|
||||
func (h *Handler) SetAppRegistryClient(appRegistryClient client.AppRegistryClient) {
|
||||
h.appRegistryClient = appRegistryClient
|
||||
|
||||
@ -77,6 +77,36 @@ func (h *Handler) getMyHostIdentity(writer http.ResponseWriter, request *http.Re
|
||||
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) {
|
||||
if profile == nil || profile.GetStatus() != hostIdentityActiveStatus {
|
||||
return
|
||||
|
||||
@ -6,7 +6,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
@ -16,55 +15,62 @@ import (
|
||||
|
||||
const overviewAggregationTimeout = 200 * time.Millisecond
|
||||
|
||||
var overviewAssetTypes = []string{"COIN", "DIAMOND", "USD_BALANCE"}
|
||||
|
||||
type myOverviewData struct {
|
||||
Profile userProfileData `json:"profile"`
|
||||
Wallet overviewWalletData `json:"wallet"`
|
||||
Invite overviewInviteData `json:"invite"`
|
||||
Roles overviewRolesData `json:"roles"`
|
||||
Badges overviewBadgesData `json:"badges"`
|
||||
Entries []overviewEntryData `json:"entries"`
|
||||
ServerTimeMS int64 `json:"server_time_ms"`
|
||||
Profile userProfileData `json:"profile"`
|
||||
ProfileStats overviewProfileStats `json:"profile_stats"`
|
||||
Wallet overviewWalletData `json:"wallet"`
|
||||
VIP overviewVIPData `json:"vip"`
|
||||
Roles overviewRolesData `json:"roles"`
|
||||
Entries []overviewEntryData `json:"entries"`
|
||||
ServerTimeMS int64 `json:"server_time_ms"`
|
||||
}
|
||||
|
||||
type overviewWalletData struct {
|
||||
Balances []assetBalanceData `json:"balances"`
|
||||
RechargeEnabled bool `json:"recharge_enabled"`
|
||||
DiamondExchangeEnabled bool `json:"diamond_exchange_enabled"`
|
||||
WithdrawEnabled bool `json:"withdraw_enabled"`
|
||||
Unavailable bool `json:"unavailable,omitempty"`
|
||||
CoinAmount int64 `json:"coin_amount"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
Unavailable bool `json:"unavailable,omitempty"`
|
||||
}
|
||||
|
||||
type overviewRolesData struct {
|
||||
IsHost bool `json:"is_host"`
|
||||
IsAgent bool `json:"is_agent"`
|
||||
IsBD bool `json:"is_bd"`
|
||||
IsBDLeader bool `json:"is_bd_leader"`
|
||||
IsCoinSeller bool `json:"is_coin_seller"`
|
||||
Unavailable bool `json:"unavailable,omitempty"`
|
||||
IsHost bool `json:"is_host"`
|
||||
IsAgency bool `json:"is_agency"`
|
||||
IsBD bool `json:"is_bd"`
|
||||
IsBDLeader bool `json:"is_bd_leader"`
|
||||
IsCoinSeller bool `json:"is_coin_seller"`
|
||||
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 {
|
||||
MyInviteCode string `json:"my_invite_code"`
|
||||
InviteEnabled bool `json:"invite_enabled"`
|
||||
InviteCount int64 `json:"invite_count"`
|
||||
ValidInviteCount int64 `json:"valid_invite_count"`
|
||||
ValidInviteThresholdCoin int64 `json:"valid_invite_threshold_coin"`
|
||||
}
|
||||
|
||||
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 overviewProfileStats struct {
|
||||
VisitorsCount int64 `json:"visitors_count"`
|
||||
FollowingCount int64 `json:"following_count"`
|
||||
FriendsCount int64 `json:"friends_count"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
Unavailable bool `json:"unavailable,omitempty"`
|
||||
}
|
||||
|
||||
type overviewEntryData struct {
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
Visible bool `json:"visible"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Route string `json:"route"`
|
||||
IconKey string `json:"icon_key"`
|
||||
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 编排和入口显隐规则,不查数据库。
|
||||
@ -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
|
||||
wg.Add(1)
|
||||
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")
|
||||
return
|
||||
}
|
||||
walletResp, walletErr = h.walletClient.GetBalances(ctx, &walletv1.GetBalancesRequest{
|
||||
RequestId: requestIDFromContext(request.Context()),
|
||||
UserId: userID,
|
||||
AssetTypes: append([]string(nil), overviewAssetTypes...),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
walletResp, walletErr = h.walletClient.GetWalletValueSummary(ctx, &walletv1.GetWalletValueSummaryRequest{
|
||||
RequestId: requestIDFromContext(request.Context()),
|
||||
UserId: userID,
|
||||
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)
|
||||
}()
|
||||
|
||||
var tabsResp *activityv1.ListMessageTabsResponse
|
||||
var tabsErr error
|
||||
var statsResp *userv1.GetMyProfileStatsResponse
|
||||
var statsErr error
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if h.messageClient == nil {
|
||||
tabsErr = xerr.New(xerr.Unavailable, "message service is not configured")
|
||||
return
|
||||
}
|
||||
tabsResp, tabsErr = h.messageClient.ListMessageTabs(ctx, &activityv1.ListMessageTabsRequest{
|
||||
Meta: activityMeta(request.WithContext(ctx)),
|
||||
statsResp, statsErr = h.userProfileClient.GetMyProfileStats(ctx, &userv1.GetMyProfileStatsRequest{
|
||||
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
||||
UserId: userID,
|
||||
})
|
||||
}()
|
||||
@ -145,173 +162,102 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
|
||||
roles = overviewRolesData{Unavailable: true}
|
||||
}
|
||||
wallet := overviewWallet(walletResp, walletErr)
|
||||
badges := overviewBadges(tabsResp, tabsErr)
|
||||
vip := overviewVIP(vipResp, vipErr)
|
||||
profileStats := overviewStats(statsResp, statsErr)
|
||||
|
||||
writeOK(writer, request, myOverviewData{
|
||||
Profile: profileData(profileResp.GetUser(), 0),
|
||||
ProfileStats: profileStats,
|
||||
Wallet: wallet,
|
||||
Invite: overviewInvite(profileResp.GetUser().GetInvite()),
|
||||
VIP: vip,
|
||||
Roles: roles,
|
||||
Badges: badges,
|
||||
Entries: overviewEntries(roles),
|
||||
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) {
|
||||
if h.userHostClient == nil {
|
||||
return overviewRolesData{}, xerr.New(xerr.Unavailable, "user host service is not configured")
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var hostProfile *userv1.HostProfile
|
||||
var bdProfile *userv1.BDProfile
|
||||
var coinSellerProfile *userv1.CoinSellerProfile
|
||||
var roleErr error
|
||||
var roleErrOnce sync.Once
|
||||
|
||||
setRoleErr := func(err error) {
|
||||
if err == nil || xerr.ReasonFromGRPC(err) == xerr.NotFound {
|
||||
return
|
||||
}
|
||||
roleErrOnce.Do(func() {
|
||||
roleErr = err
|
||||
})
|
||||
resp, err := h.userHostClient.GetUserRoleSummary(ctx, &userv1.GetUserRoleSummaryRequest{
|
||||
Meta: authRequestMeta(request.WithContext(ctx), ""),
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return overviewRolesData{}, err
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
summary := resp.GetSummary()
|
||||
return overviewRolesData{
|
||||
IsHost: identity.IsHost,
|
||||
IsAgent: identity.IsAgent,
|
||||
IsBD: identity.IsBD,
|
||||
IsBDLeader: identity.IsBDLeader,
|
||||
IsCoinSeller: identity.IsCoinSeller,
|
||||
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(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func overviewWallet(resp *walletv1.GetBalancesResponse, err error) overviewWalletData {
|
||||
func overviewWallet(resp *walletv1.GetWalletValueSummaryResponse, err error) overviewWalletData {
|
||||
if err != nil {
|
||||
return overviewWalletData{
|
||||
Balances: []assetBalanceData{},
|
||||
Unavailable: true,
|
||||
}
|
||||
return overviewWalletData{Unavailable: true}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
summary := resp.GetSummary()
|
||||
return overviewWalletData{
|
||||
Balances: balances,
|
||||
RechargeEnabled: true,
|
||||
DiamondExchangeEnabled: true,
|
||||
WithdrawEnabled: true,
|
||||
CoinAmount: summary.GetCoinAmount(),
|
||||
UpdatedAtMS: summary.GetUpdatedAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func overviewBadges(resp *activityv1.ListMessageTabsResponse, err error) overviewBadgesData {
|
||||
func overviewVIP(resp *walletv1.GetMyVipResponse, err error) overviewVIPData {
|
||||
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
|
||||
for _, section := range resp.GetSections() {
|
||||
switch section.GetSection() {
|
||||
case "system":
|
||||
badges.UnreadSystemMessages = section.GetUnreadCount()
|
||||
case "activity":
|
||||
badges.UnreadActivityMessages = section.GetUnreadCount()
|
||||
}
|
||||
func overviewStats(resp *userv1.GetMyProfileStatsResponse, err error) overviewProfileStats {
|
||||
if err != nil {
|
||||
return overviewProfileStats{Unavailable: true}
|
||||
}
|
||||
return overviewStatsFromProto(resp.GetStats())
|
||||
}
|
||||
|
||||
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 {
|
||||
return []overviewEntryData{
|
||||
{Key: "wallet", Title: "Wallet", Visible: true},
|
||||
{Key: "host_center", Title: "Host Center", Visible: roles.IsHost},
|
||||
{Key: "apply_host", Title: "Apply Host", Visible: !roles.IsHost},
|
||||
{Key: "agency_center", Title: "Agency Center", Visible: roles.IsAgent},
|
||||
{Key: "bd_center", Title: "BD Center", Visible: roles.IsBD},
|
||||
{Key: "bd_leader_center", Title: "BD Leader Center", Visible: roles.IsBDLeader},
|
||||
{Key: "coin_seller_center", Title: "Coin Seller", Visible: roles.IsCoinSeller},
|
||||
{Key: "backpack", Title: "Backpack", Visible: true},
|
||||
{Key: "settings", Title: "Settings", Visible: true},
|
||||
{Key: "support", Title: "Support", Visible: true},
|
||||
{Key: "store", Visible: true, Enabled: true, Route: "/store", IconKey: "store", Order: 10},
|
||||
{Key: "bag", Visible: true, Enabled: true, Route: "/bag", IconKey: "bag", Order: 20},
|
||||
{Key: "level", Visible: true, Enabled: true, Route: "/level", IconKey: "level", Order: 30},
|
||||
{Key: "task", Visible: true, Enabled: true, Route: "/task", IconKey: "task", Order: 40},
|
||||
{Key: "apply_host", Visible: !roles.IsHost, Enabled: true, Route: "/host/apply", IconKey: "host_apply", Order: 50},
|
||||
{Key: "host_center", Visible: roles.IsHost, Enabled: roles.IsHost, Route: "/host", IconKey: "host_center", Order: 60},
|
||||
{Key: "agency_center", Visible: roles.IsAgency, Enabled: roles.IsAgency, Route: "/agency", IconKey: "agency", Order: 70},
|
||||
{Key: "bd_center", Visible: roles.IsBD, Enabled: roles.IsBD, Route: "/bd", IconKey: "bd", Order: 80},
|
||||
{Key: "bd_leader_center", Visible: roles.IsBDLeader, Enabled: roles.IsBDLeader, Route: "/bd-leader", IconKey: "bd_leader", Order: 90},
|
||||
{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"
|
||||
"testing"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
"hyapp/pkg/xerr"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
)
|
||||
|
||||
func TestGetMyOverviewComposesProfileWalletRolesBadgesAndEntries(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{regionID: 10}
|
||||
walletClient := &fakeWalletClient{resp: &walletv1.GetBalancesResponse{Balances: []*walletv1.AssetBalance{
|
||||
{AssetType: "COIN", AvailableAmount: 12000, Version: 8},
|
||||
{AssetType: "GIFT_POINT", AvailableAmount: 999},
|
||||
{AssetType: "DIAMOND", AvailableAmount: 300, Version: 3},
|
||||
{AssetType: "USD_BALANCE", AvailableAmount: 2500, FrozenAmount: 500, Version: 2},
|
||||
}}}
|
||||
hostClient := &fakeUserHostClient{
|
||||
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"},
|
||||
func TestGetMyOverviewComposesProfileWalletVIPStatsRolesAndEntries(t *testing.T) {
|
||||
profileClient := &fakeUserProfileClient{
|
||||
regionID: 10,
|
||||
statsResp: &userv1.GetMyProfileStatsResponse{Stats: &userv1.UserProfileStats{
|
||||
UserId: 42,
|
||||
VisitorsCount: 1234,
|
||||
FollowingCount: 2345,
|
||||
FriendsCount: 3456,
|
||||
UpdatedAtMs: 7000,
|
||||
}},
|
||||
}
|
||||
messageClient := &fakeMessageInboxClient{tabsResp: &activityv1.ListMessageTabsResponse{Sections: []*activityv1.MessageTabSection{
|
||||
{Section: "user", Title: "用户", UnreadCount: 99, Source: "tencent_im_sdk"},
|
||||
{Section: "system", Title: "系统", UnreadCount: 2, Source: "backend"},
|
||||
{Section: "activity", Title: "活动", UnreadCount: 1, Source: "backend"},
|
||||
}}}
|
||||
walletClient := &fakeWalletClient{
|
||||
valueSummaryResp: &walletv1.GetWalletValueSummaryResponse{Summary: &walletv1.WalletValueSummary{CoinAmount: 12000, UpdatedAtMs: 8000}},
|
||||
myVipResp: &walletv1.GetMyVipResponse{Vip: &walletv1.UserVip{UserId: 42, Level: 3, Name: "VIP3", Active: true, StartedAtMs: 9000, ExpiresAtMs: 19000}},
|
||||
}
|
||||
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.SetWalletClient(walletClient)
|
||||
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" {
|
||||
t.Fatalf("profile request mismatch: %+v", profileClient.lastGet)
|
||||
}
|
||||
if walletClient.last == nil || walletClient.last.GetUserId() != 42 || walletClient.last.GetAppCode() != "lalu" {
|
||||
t.Fatalf("wallet request mismatch: %+v", walletClient.last)
|
||||
if profileClient.lastStats == nil || profileClient.lastStats.GetUserId() != 42 || profileClient.lastStats.GetMeta().GetAppCode() != "lalu" {
|
||||
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" {
|
||||
t.Fatalf("overview wallet asset types mismatch: %+v", got)
|
||||
if walletClient.lastValueSummary == nil || walletClient.lastValueSummary.GetUserId() != 42 || walletClient.lastValueSummary.GetAppCode() != "lalu" {
|
||||
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 {
|
||||
t.Fatalf("role requests mismatch: host=%+v bd=%+v coin=%+v", hostClient.lastHost, hostClient.lastBD, hostClient.last)
|
||||
if walletClient.lastMyVip == nil || walletClient.lastMyVip.GetUserId() != 42 || walletClient.lastMyVip.GetAppCode() != "lalu" {
|
||||
t.Fatalf("vip request mismatch: %+v", walletClient.lastMyVip)
|
||||
}
|
||||
if messageClient.lastTabs == nil || messageClient.lastTabs.GetUserId() != 42 || messageClient.lastTabs.GetMeta().GetRequestId() != "req-overview" {
|
||||
t.Fatalf("message tabs request mismatch: %+v", messageClient.lastTabs)
|
||||
if hostClient.lastRoleSummary == nil || hostClient.lastRoleSummary.GetUserId() != 42 || hostClient.lastRoleSummary.GetMeta().GetAppCode() != "lalu" {
|
||||
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)
|
||||
if profile["user_id"] != "42" || profile["username"] != "hy" || profile["region_id"] != float64(10) {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
if wallet["recharge_enabled"] != true || wallet["diamond_exchange_enabled"] != true || wallet["withdraw_enabled"] != true {
|
||||
t.Fatalf("wallet feature flags mismatch: %+v", wallet)
|
||||
if wallet["coin_amount"] != float64(12000) || wallet["updated_at_ms"] != float64(8000) {
|
||||
t.Fatalf("wallet value summary mismatch: %+v", wallet)
|
||||
}
|
||||
invite := data["invite"].(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) {
|
||||
t.Fatalf("invite overview mismatch: %+v", invite)
|
||||
vip := data["vip"].(map[string]any)
|
||||
if vip["level"] != float64(3) || vip["name"] != "VIP3" || vip["active"] != true {
|
||||
t.Fatalf("vip summary mismatch: %+v", vip)
|
||||
}
|
||||
balances := wallet["balances"].([]any)
|
||||
if len(balances) != 3 {
|
||||
t.Fatalf("overview must return three fixed balances, got %+v", balances)
|
||||
if _, ok := data["invite"]; ok {
|
||||
t.Fatalf("my overview must not include invite: %+v", data["invite"])
|
||||
}
|
||||
if balanceAssetType(balances[0]) != "COIN" || balanceAvailable(balances[0]) != 12000 ||
|
||||
balanceAssetType(balances[1]) != "DIAMOND" || balanceAvailable(balances[1]) != 300 ||
|
||||
balanceAssetType(balances[2]) != "USD_BALANCE" || balanceAvailable(balances[2]) != 2500 {
|
||||
t.Fatalf("balance projection mismatch: %+v", balances)
|
||||
if _, ok := data["badges"]; ok {
|
||||
t.Fatalf("my overview must not include message badges: %+v", data["badges"])
|
||||
}
|
||||
|
||||
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, "agency_center") ||
|
||||
!overviewEntryVisible(entries, "bd_center") ||
|
||||
!overviewEntryVisible(entries, "bd_leader_center") ||
|
||||
!overviewEntryVisible(entries, "coin_seller_center") ||
|
||||
!overviewEntryVisible(entries, "settings") ||
|
||||
!overviewEntryVisible(entries, "language") ||
|
||||
overviewEntryVisible(entries, "apply_host") {
|
||||
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.SetWalletClient(&fakeWalletClient{resp: &walletv1.GetBalancesResponse{}})
|
||||
handler.SetWalletClient(&fakeWalletClient{})
|
||||
handler.SetUserHostClient(&fakeUserHostClient{})
|
||||
handler.SetMessageInboxClient(&fakeMessageInboxClient{})
|
||||
|
||||
data := performOverviewRequest(t, handler, "req-overview-empty-wallet")
|
||||
|
||||
wallet := data["wallet"].(map[string]any)
|
||||
balances := wallet["balances"].([]any)
|
||||
if len(balances) != 3 {
|
||||
t.Fatalf("missing wallet rows must still project three balances: %+v", balances)
|
||||
if wallet["coin_amount"] != float64(0) || wallet["updated_at_ms"] != float64(0) {
|
||||
t.Fatalf("missing wallet summary should be zero projection: %+v", wallet)
|
||||
}
|
||||
for _, balance := range balances {
|
||||
item := balance.(map[string]any)
|
||||
if item["available_amount"] != float64(0) || item["frozen_amount"] != float64(0) || item["version"] != float64(0) {
|
||||
t.Fatalf("missing balance should be zero projection: %+v", item)
|
||||
}
|
||||
stats := data["profile_stats"].(map[string]any)
|
||||
if stats["visitors_count"] != float64(0) || stats["following_count"] != float64(0) || stats["friends_count"] != float64(0) {
|
||||
t.Fatalf("missing stats should be zero projection: %+v", stats)
|
||||
}
|
||||
roles := data["roles"].(map[string]any)
|
||||
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) {
|
||||
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.SetUserHostClient(&fakeUserHostClient{hostErr: xerr.ToGRPCError(xerr.New(xerr.Internal, "role service down"))})
|
||||
handler.SetMessageInboxClient(&fakeMessageInboxClient{err: errors.New("message down")})
|
||||
handler.SetUserHostClient(&fakeUserHostClient{roleSummaryErr: xerr.ToGRPCError(xerr.New(xerr.Internal, "role service down"))})
|
||||
handler.SetMessageInboxClient(&fakeMessageInboxClient{})
|
||||
|
||||
data := performOverviewRequest(t, handler, "req-overview-degraded")
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
if roles["unavailable"] != true || roles["is_host"] != false || roles["is_bd"] != false || roles["is_coin_seller"] != false {
|
||||
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)
|
||||
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)
|
||||
@ -161,7 +176,6 @@ func TestGetMyOverviewFailsWhenProfileFails(t *testing.T) {
|
||||
})
|
||||
handler.SetWalletClient(&fakeWalletClient{})
|
||||
handler.SetUserHostClient(&fakeUserHostClient{})
|
||||
handler.SetMessageInboxClient(&fakeMessageInboxClient{})
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/users/me/overview", nil)
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
@ -208,15 +222,3 @@ func overviewEntryVisible(entries []any, key string) bool {
|
||||
}
|
||||
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 {
|
||||
lastGet *userv1.GetUserRequest
|
||||
lastStats *userv1.GetMyProfileStatsRequest
|
||||
lastBatch *userv1.BatchGetUsersRequest
|
||||
getRequests []*userv1.GetUserRequest
|
||||
lastComplete *userv1.CompleteOnboardingRequest
|
||||
lastUpdate *userv1.UpdateUserProfileRequest
|
||||
lastCountry *userv1.ChangeUserCountryRequest
|
||||
getErr error
|
||||
statsResp *userv1.GetMyProfileStatsResponse
|
||||
statsErr error
|
||||
completeErr error
|
||||
countryErr error
|
||||
regionID int64
|
||||
@ -210,12 +213,18 @@ type fakeUserIdentityClient struct {
|
||||
|
||||
type fakeRoomQueryClient struct {
|
||||
lastList *roomv1.ListRoomsRequest
|
||||
lastFeeds *roomv1.ListRoomFeedsRequest
|
||||
lastMyRoom *roomv1.GetMyRoomRequest
|
||||
lastCurrent *roomv1.GetCurrentRoomRequest
|
||||
lastSnapshot *roomv1.GetRoomSnapshotRequest
|
||||
resp *roomv1.ListRoomsResponse
|
||||
feedsResp *roomv1.ListRoomsResponse
|
||||
myRoomResp *roomv1.GetMyRoomResponse
|
||||
currentResp *roomv1.GetCurrentRoomResponse
|
||||
snapshotResp *roomv1.GetRoomSnapshotResponse
|
||||
err error
|
||||
feedsErr error
|
||||
myRoomErr error
|
||||
currentErr error
|
||||
snapshotErr error
|
||||
}
|
||||
@ -234,24 +243,48 @@ type fakeUserCountryQueryClient struct {
|
||||
}
|
||||
|
||||
type fakeUserHostClient struct {
|
||||
last *userv1.GetCoinSellerProfileRequest
|
||||
profile *userv1.CoinSellerProfile
|
||||
err error
|
||||
lastHost *userv1.GetHostProfileRequest
|
||||
hostProfile *userv1.HostProfile
|
||||
hostErr error
|
||||
lastBD *userv1.GetBDProfileRequest
|
||||
bdProfile *userv1.BDProfile
|
||||
bdErr error
|
||||
last *userv1.GetCoinSellerProfileRequest
|
||||
profile *userv1.CoinSellerProfile
|
||||
err error
|
||||
lastHost *userv1.GetHostProfileRequest
|
||||
hostProfile *userv1.HostProfile
|
||||
hostErr error
|
||||
lastBD *userv1.GetBDProfileRequest
|
||||
bdProfile *userv1.BDProfile
|
||||
bdErr error
|
||||
lastRoleSummary *userv1.GetUserRoleSummaryRequest
|
||||
roleSummary *userv1.UserRoleSummary
|
||||
roleSummaryErr error
|
||||
}
|
||||
|
||||
type fakeWalletClient struct {
|
||||
last *walletv1.GetBalancesRequest
|
||||
resp *walletv1.GetBalancesResponse
|
||||
err error
|
||||
lastTransfer *walletv1.TransferCoinFromSellerRequest
|
||||
transferResp *walletv1.TransferCoinFromSellerResponse
|
||||
transferErr error
|
||||
last *walletv1.GetBalancesRequest
|
||||
resp *walletv1.GetBalancesResponse
|
||||
err error
|
||||
lastOverview *walletv1.GetWalletOverviewRequest
|
||||
overviewResp *walletv1.GetWalletOverviewResponse
|
||||
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 {
|
||||
@ -372,6 +405,17 @@ func (f *fakeUserProfileClient) GetUser(_ context.Context, req *userv1.GetUserRe
|
||||
}}, 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) {
|
||||
f.lastBatch = req
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
f.lastCurrent = req
|
||||
if f.currentErr != nil {
|
||||
@ -675,6 +744,17 @@ func (f *fakeUserHostClient) GetCoinSellerProfile(_ context.Context, req *userv1
|
||||
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) {
|
||||
f.last = req
|
||||
if f.err != nil {
|
||||
@ -687,6 +767,131 @@ func (f *fakeWalletClient) GetBalances(_ context.Context, req *walletv1.GetBalan
|
||||
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) {
|
||||
f.lastTransfer = req
|
||||
if f.transferErr != nil {
|
||||
@ -1097,7 +1302,7 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||
handler.SetRoomQueryClient(queryClient)
|
||||
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("X-Request-ID", "req-room-list")
|
||||
recorder := httptest.NewRecorder()
|
||||
@ -1116,7 +1321,7 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
|
||||
if queryClient.lastList.GetViewerUserId() != 42 || queryClient.lastList.GetVisibleRegionId() != 1001 {
|
||||
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)
|
||||
}
|
||||
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) {
|
||||
queryClient := &fakeRoomQueryClient{}
|
||||
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")
|
||||
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())
|
||||
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, "", ""),
|
||||
ViewerUserId: viewerUserID,
|
||||
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
||||
Tab: request.URL.Query().Get("tab"),
|
||||
Tab: tab,
|
||||
Cursor: request.URL.Query().Get("cursor"),
|
||||
Limit: limit,
|
||||
Query: roomListQuery(request),
|
||||
@ -53,6 +58,75 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
|
||||
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 {
|
||||
if query := strings.TrimSpace(request.URL.Query().Get("q")); query != "" {
|
||||
return query
|
||||
@ -77,6 +151,25 @@ func parseRoomListLimit(raw string) (int32, bool) {
|
||||
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 查询当前登录用户是否仍有可恢复房间。
|
||||
// gateway 只传鉴权 user_id;presence 和麦位状态由 room-service 用读模型和 Room Cell 快照判断。
|
||||
func (h *Handler) getCurrentRoom(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
@ -15,6 +15,12 @@ type roomListData struct {
|
||||
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 {
|
||||
RoomID string `json:"room_id"`
|
||||
IMGroupID string `json:"im_group_id"`
|
||||
@ -118,28 +124,47 @@ func roomListDataFromProto(resp *roomv1.ListRoomsResponse) roomListData {
|
||||
}
|
||||
items := make([]roomListItemData, 0, len(resp.GetRooms()))
|
||||
for _, room := range resp.GetRooms() {
|
||||
roomID := room.GetRoomId()
|
||||
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(),
|
||||
})
|
||||
items = append(items, roomListItemDataFromProto(room))
|
||||
}
|
||||
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 {
|
||||
if result == nil {
|
||||
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/profiles:batch", h.profileAPIHandler(jwtVerifier, h.batchUserProfiles))
|
||||
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/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/profile/update", h.profileAPIHandler(jwtVerifier, h.updateMyProfile))
|
||||
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/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.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/current", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getCurrentRoom)))
|
||||
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+"/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+"/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/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+"/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
|
||||
}
|
||||
|
||||
@ -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_claim (app_code, status, lock_until_ms, created_at_ms)
|
||||
) 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 (
|
||||
roomListTabHot = "hot"
|
||||
roomListTabNew = "new"
|
||||
roomListTabMe = "me"
|
||||
roomListTabVisited = "visited"
|
||||
roomListTabFriend = "friend"
|
||||
roomListTabFollowing = "following"
|
||||
defaultRoomListLimit = 20
|
||||
maxRoomListLimit = 50
|
||||
maxRoomListQueryRunes = 64
|
||||
@ -29,10 +31,11 @@ type roomListCursor struct {
|
||||
Query string `json:"query,omitempty"`
|
||||
SortScore int64 `json:"sort_score,omitempty"`
|
||||
CreatedAtMS int64 `json:"created_at_ms,omitempty"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms,omitempty"`
|
||||
RoomID string `json:"room_id"`
|
||||
}
|
||||
|
||||
// ListRooms 查询当前用户区域内的房间列表读模型。
|
||||
// ListRooms 查询当前用户区域内的公共房间发现列表读模型。
|
||||
func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
viewerUserID := req.GetViewerUserId()
|
||||
@ -55,16 +58,11 @@ func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ownerUserID := int64(0)
|
||||
if tab == roomListTabMe {
|
||||
ownerUserID = viewerUserID
|
||||
}
|
||||
|
||||
entries, err := s.repository.ListRoomListEntries(ctx, RoomListQuery{
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
VisibleRegionID: normalizeVisibleRegionID(req.GetVisibleRegionId()),
|
||||
Tab: tab,
|
||||
OwnerUserID: ownerUserID,
|
||||
Query: query,
|
||||
Limit: limit + 1,
|
||||
CursorSortScore: cursor.SortScore,
|
||||
@ -75,6 +73,81 @@ func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (
|
||||
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 := ""
|
||||
if len(entries) > limit {
|
||||
// 多查一条用于判断是否存在下一页;返回数据只包含请求的 limit 条。
|
||||
@ -91,7 +164,7 @@ func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (
|
||||
return &roomv1.ListRoomsResponse{
|
||||
Rooms: items,
|
||||
NextCursor: nextCursor,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// projectRoomListBestEffort 根据最新快照刷新房间列表投影。
|
||||
@ -164,15 +237,26 @@ func roomListSortScore(heat int64, onlineCount int32, occupiedSeatCount int32) i
|
||||
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 {
|
||||
switch strings.ToLower(strings.TrimSpace(tab)) {
|
||||
case "", roomListTabHot:
|
||||
return roomListTabHot
|
||||
case roomListTabNew:
|
||||
return roomListTabNew
|
||||
case roomListTabMe:
|
||||
return roomListTabMe
|
||||
default:
|
||||
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:
|
||||
return ""
|
||||
}
|
||||
@ -240,6 +324,7 @@ func encodeRoomListCursor(tab string, query string, entry RoomListEntry) string
|
||||
Query: query,
|
||||
SortScore: entry.SortScore,
|
||||
CreatedAtMS: entry.CreatedAtMS,
|
||||
UpdatedAtMS: entry.UpdatedAtMS,
|
||||
RoomID: entry.RoomID,
|
||||
}
|
||||
|
||||
@ -271,3 +356,39 @@ func roomListItemToProto(entry RoomListEntry) *roomv1.RoomListItem {
|
||||
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 (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||
roomv1 "hyapp.local/api/proto/room/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/idgen"
|
||||
"hyapp/pkg/logx"
|
||||
"hyapp/pkg/tencentim"
|
||||
"hyapp/pkg/xerr"
|
||||
"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 {
|
||||
return nil, err
|
||||
}
|
||||
s.projectRoomVisitBestEffort(ctx, cmd.ActorUserID(), cmd.RoomID())
|
||||
|
||||
return &roomv1.JoinRoomResponse{
|
||||
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
|
||||
}
|
||||
|
||||
// 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。
|
||||
func (s *Service) RoomHeartbeat(ctx context.Context, req *roomv1.RoomHeartbeatRequest) (*roomv1.RoomHeartbeatResponse, error) {
|
||||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||||
|
||||
@ -146,9 +146,9 @@ type RoomListQuery struct {
|
||||
AppCode string
|
||||
// VisibleRegionID 是服务端解析出的用户区域,客户端不能伪造。
|
||||
VisibleRegionID int64
|
||||
// Tab 只允许 hot/new/me,决定过滤和排序字段。
|
||||
// Tab 只允许 hot/new,决定公共发现列表排序字段。
|
||||
Tab string
|
||||
// OwnerUserID 非 0 时只返回该用户创建的房间。
|
||||
// OwnerUserID 保留给内部定向读模型;公开 Mine 顶部卡片走 GetMyRoom。
|
||||
OwnerUserID int64
|
||||
// Query 非空时按 room_id 或 title 做包含匹配。
|
||||
Query string
|
||||
@ -158,10 +158,49 @@ type RoomListQuery struct {
|
||||
CursorSortScore int64
|
||||
// CursorCreatedAtMS 是 new tab 的游标创建时间。
|
||||
CursorCreatedAtMS int64
|
||||
// CursorUpdatedAtMS 是用户关系 feed tab 的游标更新时间。
|
||||
CursorUpdatedAtMS int64
|
||||
// CursorRoomID 是同分或同时间下的稳定翻页边界。
|
||||
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 在首版需要的全部持久化读写。
|
||||
type Repository interface {
|
||||
// SaveRoomMeta 保存房间基础元数据,创建房间时必须先建立它。
|
||||
@ -196,6 +235,10 @@ type Repository interface {
|
||||
UpsertRoomListEntry(ctx context.Context, entry RoomListEntry) error
|
||||
// ListRoomListEntries 按区域和 tab 查询房间列表,不访问 Room Cell 内存。
|
||||
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(ctx context.Context, snapshot RoomPresenceSnapshot) error
|
||||
// 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) {
|
||||
ctx := context.Background()
|
||||
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_claim (app_code, status, lock_until_ms, created_at_ms)
|
||||
) 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 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_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_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 {
|
||||
@ -812,11 +823,84 @@ func (r *Repository) ListRoomListEntries(ctx context.Context, query roomservice.
|
||||
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 = `
|
||||
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
|
||||
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) {
|
||||
where := []string{"app_code = ?", "visible_region_id = ?", "status = ?"}
|
||||
args := []any{appcode.Normalize(query.AppCode), query.VisibleRegionID, "active"}
|
||||
@ -829,7 +913,7 @@ func buildRoomListQuerySQL(query roomservice.RoomListQuery) (string, []any) {
|
||||
like := "%" + escapeRoomListLike(query.Query) + "%"
|
||||
args = append(args, like, like, like)
|
||||
}
|
||||
if query.Tab == "new" || query.Tab == "me" {
|
||||
if query.Tab == "new" {
|
||||
if query.CursorRoomID != "" {
|
||||
where = append(where, "(created_at_ms < ? OR (created_at_ms = ? AND room_id > ?))")
|
||||
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
|
||||
}
|
||||
|
||||
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 投影用户当前房间读模型。
|
||||
func (r *Repository) ProjectRoomPresence(ctx context.Context, snapshot roomservice.RoomPresenceSnapshot) error {
|
||||
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.
|
||||
func (r *Repository) OutboxRecord(eventID string) (outbox.Record, bool) {
|
||||
r.t.Helper()
|
||||
|
||||
@ -187,6 +187,20 @@ func (s *Server) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*
|
||||
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 代理到用户当前房间恢复探测读模型。
|
||||
func (s *Server) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) {
|
||||
// 恢复探测不修改 Room Cell,只在读模型命中后用快照做二次校验。
|
||||
|
||||
@ -68,6 +68,65 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
KEY idx_users_country_region (app_code, country, region_id)
|
||||
) 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 (
|
||||
app_code VARCHAR(32) NOT NULL PRIMARY KEY,
|
||||
next_value BIGINT NOT NULL,
|
||||
|
||||
@ -215,6 +215,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
// 多个 protobuf service 共用一个 Server 适配器,领域逻辑仍拆在 auth/user/host service。
|
||||
userv1.RegisterAuthServiceServer(server, userServer)
|
||||
userv1.RegisterUserServiceServer(server, userServer)
|
||||
userv1.RegisterUserSocialServiceServer(server, userServer)
|
||||
userv1.RegisterUserDeviceServiceServer(server, userServer)
|
||||
userv1.RegisterAppRegistryServiceServer(server, userServer)
|
||||
userv1.RegisterUserIdentityServiceServer(server, userServer)
|
||||
|
||||
@ -178,6 +178,23 @@ type CoinSellerProfile struct {
|
||||
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 的有时效归属事实。
|
||||
type AgencyMembership struct {
|
||||
MembershipID int64
|
||||
|
||||
@ -165,6 +165,66 @@ type User struct {
|
||||
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 的绑定投影。
|
||||
type Identity struct {
|
||||
// AppCode 是短号所属 App。
|
||||
|
||||
@ -32,6 +32,7 @@ type Repository interface {
|
||||
GetHostProfile(ctx context.Context, userID int64) (hostdomain.HostProfile, error)
|
||||
GetBDProfile(ctx context.Context, userID int64) (hostdomain.BDProfile, 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)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 成员关系列表,第一阶段直接读事实表。
|
||||
func (s *Service) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) {
|
||||
if s.repository == nil {
|
||||
|
||||
@ -495,3 +495,62 @@ func TestAdminCreateCoinSellerCanCoexistWithBDAndAgency(t *testing.T) {
|
||||
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 {
|
||||
// GetUser 查询单个用户主状态。
|
||||
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(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error)
|
||||
// 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) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
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, "")
|
||||
}
|
||||
|
||||
// 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 批量查询用户主状态。
|
||||
func (s *Service) BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 成员关系列表。
|
||||
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)
|
||||
|
||||
@ -32,6 +32,28 @@ func (r *Repository) GetUser(ctx context.Context, userID int64) (userdomain.User
|
||||
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 中。
|
||||
|
||||
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