Merge branch 'test'
This commit is contained in:
commit
f33e965caf
File diff suppressed because it is too large
Load Diff
@ -825,6 +825,7 @@ message LevelTrackOverview {
|
||||
int64 display_badge_resource_id = 10;
|
||||
int64 reward_pending_count = 11;
|
||||
int32 sort_order = 12;
|
||||
int32 display_badge_source_level = 13;
|
||||
}
|
||||
|
||||
message GetMyLevelOverviewRequest {
|
||||
@ -850,6 +851,35 @@ message GetLevelTrackResponse {
|
||||
int64 server_time_ms = 4;
|
||||
}
|
||||
|
||||
// LevelDisplayTrackProfile 是资料卡和房间展示使用的单轨道等级投影。
|
||||
message LevelDisplayTrackProfile {
|
||||
string track = 1;
|
||||
int32 level = 2;
|
||||
int64 tier_id = 3;
|
||||
int64 avatar_frame_resource_id = 4;
|
||||
int64 badge_resource_id = 5;
|
||||
int32 badge_source_level = 6;
|
||||
int64 updated_at_ms = 7;
|
||||
}
|
||||
|
||||
// UserLevelDisplayProfile 固定返回财富、游戏、魅力三条轨道,避免客户端按数组猜槽位。
|
||||
message UserLevelDisplayProfile {
|
||||
int64 user_id = 1;
|
||||
LevelDisplayTrackProfile wealth = 2;
|
||||
LevelDisplayTrackProfile game = 3;
|
||||
LevelDisplayTrackProfile charm = 4;
|
||||
}
|
||||
|
||||
message BatchGetUserLevelDisplayProfilesRequest {
|
||||
RequestMeta meta = 1;
|
||||
repeated int64 user_ids = 2;
|
||||
}
|
||||
|
||||
message BatchGetUserLevelDisplayProfilesResponse {
|
||||
repeated UserLevelDisplayProfile profiles = 1;
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
// LevelRewardJob 是等级或等级段奖励的发放状态。
|
||||
message LevelRewardJob {
|
||||
string reward_job_id = 1;
|
||||
@ -905,6 +935,24 @@ message ConsumeLevelEventResponse {
|
||||
int64 reward_job_count = 6;
|
||||
}
|
||||
|
||||
message RegistrationLevelBadgeGrant {
|
||||
string track = 1;
|
||||
int32 level = 2;
|
||||
int64 resource_id = 3;
|
||||
string grant_id = 4;
|
||||
}
|
||||
|
||||
message IssueRegistrationLevelBadgesRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
string command_id = 3;
|
||||
}
|
||||
|
||||
message IssueRegistrationLevelBadgesResponse {
|
||||
repeated RegistrationLevelBadgeGrant grants = 1;
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
message UpsertLevelTrackRequest {
|
||||
RequestMeta meta = 1;
|
||||
string track = 2;
|
||||
@ -1383,8 +1431,10 @@ service TaskService {
|
||||
service GrowthLevelService {
|
||||
rpc GetMyLevelOverview(GetMyLevelOverviewRequest) returns (GetMyLevelOverviewResponse);
|
||||
rpc GetLevelTrack(GetLevelTrackRequest) returns (GetLevelTrackResponse);
|
||||
rpc BatchGetUserLevelDisplayProfiles(BatchGetUserLevelDisplayProfilesRequest) returns (BatchGetUserLevelDisplayProfilesResponse);
|
||||
rpc ListLevelRewards(ListLevelRewardsRequest) returns (ListLevelRewardsResponse);
|
||||
rpc ConsumeLevelEvent(ConsumeLevelEventRequest) returns (ConsumeLevelEventResponse);
|
||||
rpc IssueRegistrationLevelBadges(IssueRegistrationLevelBadgesRequest) returns (IssueRegistrationLevelBadgesResponse);
|
||||
}
|
||||
|
||||
// AchievementService owns App achievement reads, badge display and internal achievement event consumption.
|
||||
|
||||
@ -861,10 +861,12 @@ var TaskService_ServiceDesc = grpc.ServiceDesc{
|
||||
}
|
||||
|
||||
const (
|
||||
GrowthLevelService_GetMyLevelOverview_FullMethodName = "/hyapp.activity.v1.GrowthLevelService/GetMyLevelOverview"
|
||||
GrowthLevelService_GetLevelTrack_FullMethodName = "/hyapp.activity.v1.GrowthLevelService/GetLevelTrack"
|
||||
GrowthLevelService_ListLevelRewards_FullMethodName = "/hyapp.activity.v1.GrowthLevelService/ListLevelRewards"
|
||||
GrowthLevelService_ConsumeLevelEvent_FullMethodName = "/hyapp.activity.v1.GrowthLevelService/ConsumeLevelEvent"
|
||||
GrowthLevelService_GetMyLevelOverview_FullMethodName = "/hyapp.activity.v1.GrowthLevelService/GetMyLevelOverview"
|
||||
GrowthLevelService_GetLevelTrack_FullMethodName = "/hyapp.activity.v1.GrowthLevelService/GetLevelTrack"
|
||||
GrowthLevelService_BatchGetUserLevelDisplayProfiles_FullMethodName = "/hyapp.activity.v1.GrowthLevelService/BatchGetUserLevelDisplayProfiles"
|
||||
GrowthLevelService_ListLevelRewards_FullMethodName = "/hyapp.activity.v1.GrowthLevelService/ListLevelRewards"
|
||||
GrowthLevelService_ConsumeLevelEvent_FullMethodName = "/hyapp.activity.v1.GrowthLevelService/ConsumeLevelEvent"
|
||||
GrowthLevelService_IssueRegistrationLevelBadges_FullMethodName = "/hyapp.activity.v1.GrowthLevelService/IssueRegistrationLevelBadges"
|
||||
)
|
||||
|
||||
// GrowthLevelServiceClient is the client API for GrowthLevelService service.
|
||||
@ -875,8 +877,10 @@ const (
|
||||
type GrowthLevelServiceClient interface {
|
||||
GetMyLevelOverview(ctx context.Context, in *GetMyLevelOverviewRequest, opts ...grpc.CallOption) (*GetMyLevelOverviewResponse, error)
|
||||
GetLevelTrack(ctx context.Context, in *GetLevelTrackRequest, opts ...grpc.CallOption) (*GetLevelTrackResponse, error)
|
||||
BatchGetUserLevelDisplayProfiles(ctx context.Context, in *BatchGetUserLevelDisplayProfilesRequest, opts ...grpc.CallOption) (*BatchGetUserLevelDisplayProfilesResponse, error)
|
||||
ListLevelRewards(ctx context.Context, in *ListLevelRewardsRequest, opts ...grpc.CallOption) (*ListLevelRewardsResponse, error)
|
||||
ConsumeLevelEvent(ctx context.Context, in *ConsumeLevelEventRequest, opts ...grpc.CallOption) (*ConsumeLevelEventResponse, error)
|
||||
IssueRegistrationLevelBadges(ctx context.Context, in *IssueRegistrationLevelBadgesRequest, opts ...grpc.CallOption) (*IssueRegistrationLevelBadgesResponse, error)
|
||||
}
|
||||
|
||||
type growthLevelServiceClient struct {
|
||||
@ -907,6 +911,16 @@ func (c *growthLevelServiceClient) GetLevelTrack(ctx context.Context, in *GetLev
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *growthLevelServiceClient) BatchGetUserLevelDisplayProfiles(ctx context.Context, in *BatchGetUserLevelDisplayProfilesRequest, opts ...grpc.CallOption) (*BatchGetUserLevelDisplayProfilesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(BatchGetUserLevelDisplayProfilesResponse)
|
||||
err := c.cc.Invoke(ctx, GrowthLevelService_BatchGetUserLevelDisplayProfiles_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *growthLevelServiceClient) ListLevelRewards(ctx context.Context, in *ListLevelRewardsRequest, opts ...grpc.CallOption) (*ListLevelRewardsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListLevelRewardsResponse)
|
||||
@ -927,6 +941,16 @@ func (c *growthLevelServiceClient) ConsumeLevelEvent(ctx context.Context, in *Co
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *growthLevelServiceClient) IssueRegistrationLevelBadges(ctx context.Context, in *IssueRegistrationLevelBadgesRequest, opts ...grpc.CallOption) (*IssueRegistrationLevelBadgesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(IssueRegistrationLevelBadgesResponse)
|
||||
err := c.cc.Invoke(ctx, GrowthLevelService_IssueRegistrationLevelBadges_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GrowthLevelServiceServer is the server API for GrowthLevelService service.
|
||||
// All implementations must embed UnimplementedGrowthLevelServiceServer
|
||||
// for forward compatibility.
|
||||
@ -935,8 +959,10 @@ func (c *growthLevelServiceClient) ConsumeLevelEvent(ctx context.Context, in *Co
|
||||
type GrowthLevelServiceServer interface {
|
||||
GetMyLevelOverview(context.Context, *GetMyLevelOverviewRequest) (*GetMyLevelOverviewResponse, error)
|
||||
GetLevelTrack(context.Context, *GetLevelTrackRequest) (*GetLevelTrackResponse, error)
|
||||
BatchGetUserLevelDisplayProfiles(context.Context, *BatchGetUserLevelDisplayProfilesRequest) (*BatchGetUserLevelDisplayProfilesResponse, error)
|
||||
ListLevelRewards(context.Context, *ListLevelRewardsRequest) (*ListLevelRewardsResponse, error)
|
||||
ConsumeLevelEvent(context.Context, *ConsumeLevelEventRequest) (*ConsumeLevelEventResponse, error)
|
||||
IssueRegistrationLevelBadges(context.Context, *IssueRegistrationLevelBadgesRequest) (*IssueRegistrationLevelBadgesResponse, error)
|
||||
mustEmbedUnimplementedGrowthLevelServiceServer()
|
||||
}
|
||||
|
||||
@ -953,12 +979,18 @@ func (UnimplementedGrowthLevelServiceServer) GetMyLevelOverview(context.Context,
|
||||
func (UnimplementedGrowthLevelServiceServer) GetLevelTrack(context.Context, *GetLevelTrackRequest) (*GetLevelTrackResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetLevelTrack not implemented")
|
||||
}
|
||||
func (UnimplementedGrowthLevelServiceServer) BatchGetUserLevelDisplayProfiles(context.Context, *BatchGetUserLevelDisplayProfilesRequest) (*BatchGetUserLevelDisplayProfilesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchGetUserLevelDisplayProfiles not implemented")
|
||||
}
|
||||
func (UnimplementedGrowthLevelServiceServer) ListLevelRewards(context.Context, *ListLevelRewardsRequest) (*ListLevelRewardsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListLevelRewards not implemented")
|
||||
}
|
||||
func (UnimplementedGrowthLevelServiceServer) ConsumeLevelEvent(context.Context, *ConsumeLevelEventRequest) (*ConsumeLevelEventResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ConsumeLevelEvent not implemented")
|
||||
}
|
||||
func (UnimplementedGrowthLevelServiceServer) IssueRegistrationLevelBadges(context.Context, *IssueRegistrationLevelBadgesRequest) (*IssueRegistrationLevelBadgesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method IssueRegistrationLevelBadges not implemented")
|
||||
}
|
||||
func (UnimplementedGrowthLevelServiceServer) mustEmbedUnimplementedGrowthLevelServiceServer() {}
|
||||
func (UnimplementedGrowthLevelServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@ -1016,6 +1048,24 @@ func _GrowthLevelService_GetLevelTrack_Handler(srv interface{}, ctx context.Cont
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GrowthLevelService_BatchGetUserLevelDisplayProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BatchGetUserLevelDisplayProfilesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GrowthLevelServiceServer).BatchGetUserLevelDisplayProfiles(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: GrowthLevelService_BatchGetUserLevelDisplayProfiles_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GrowthLevelServiceServer).BatchGetUserLevelDisplayProfiles(ctx, req.(*BatchGetUserLevelDisplayProfilesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GrowthLevelService_ListLevelRewards_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListLevelRewardsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -1052,6 +1102,24 @@ func _GrowthLevelService_ConsumeLevelEvent_Handler(srv interface{}, ctx context.
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _GrowthLevelService_IssueRegistrationLevelBadges_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(IssueRegistrationLevelBadgesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(GrowthLevelServiceServer).IssueRegistrationLevelBadges(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: GrowthLevelService_IssueRegistrationLevelBadges_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(GrowthLevelServiceServer).IssueRegistrationLevelBadges(ctx, req.(*IssueRegistrationLevelBadgesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// GrowthLevelService_ServiceDesc is the grpc.ServiceDesc for GrowthLevelService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -1067,6 +1135,10 @@ var GrowthLevelService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetLevelTrack",
|
||||
Handler: _GrowthLevelService_GetLevelTrack_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BatchGetUserLevelDisplayProfiles",
|
||||
Handler: _GrowthLevelService_BatchGetUserLevelDisplayProfiles_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListLevelRewards",
|
||||
Handler: _GrowthLevelService_ListLevelRewards_Handler,
|
||||
@ -1075,6 +1147,10 @@ var GrowthLevelService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ConsumeLevelEvent",
|
||||
Handler: _GrowthLevelService_ConsumeLevelEvent_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "IssueRegistrationLevelBadges",
|
||||
Handler: _GrowthLevelService_IssueRegistrationLevelBadges_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/activity/v1/activity.proto",
|
||||
|
||||
@ -348,19 +348,128 @@ func (x *RoomBackgroundChanged) GetRoomBackgroundUrl() string {
|
||||
}
|
||||
|
||||
// RoomUserJoined 表达用户业务态进房成功。
|
||||
type RoomEntryVehicleSnapshot struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ResourceId int64 `protobuf:"varint,1,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"`
|
||||
ResourceCode string `protobuf:"bytes,2,opt,name=resource_code,json=resourceCode,proto3" json:"resource_code,omitempty"`
|
||||
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
|
||||
AssetUrl string `protobuf:"bytes,4,opt,name=asset_url,json=assetUrl,proto3" json:"asset_url,omitempty"`
|
||||
PreviewUrl string `protobuf:"bytes,5,opt,name=preview_url,json=previewUrl,proto3" json:"preview_url,omitempty"`
|
||||
AnimationUrl string `protobuf:"bytes,6,opt,name=animation_url,json=animationUrl,proto3" json:"animation_url,omitempty"`
|
||||
MetadataJson string `protobuf:"bytes,7,opt,name=metadata_json,json=metadataJson,proto3" json:"metadata_json,omitempty"`
|
||||
EntitlementId string `protobuf:"bytes,8,opt,name=entitlement_id,json=entitlementId,proto3" json:"entitlement_id,omitempty"`
|
||||
ExpiresAtMs int64 `protobuf:"varint,9,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) Reset() {
|
||||
*x = RoomEntryVehicleSnapshot{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RoomEntryVehicleSnapshot) ProtoMessage() {}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RoomEntryVehicleSnapshot.ProtoReflect.Descriptor instead.
|
||||
func (*RoomEntryVehicleSnapshot) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) GetResourceId() int64 {
|
||||
if x != nil {
|
||||
return x.ResourceId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) GetResourceCode() string {
|
||||
if x != nil {
|
||||
return x.ResourceCode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) GetAssetUrl() string {
|
||||
if x != nil {
|
||||
return x.AssetUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) GetPreviewUrl() string {
|
||||
if x != nil {
|
||||
return x.PreviewUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) GetAnimationUrl() string {
|
||||
if x != nil {
|
||||
return x.AnimationUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) GetMetadataJson() string {
|
||||
if x != nil {
|
||||
return x.MetadataJson
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) GetEntitlementId() string {
|
||||
if x != nil {
|
||||
return x.EntitlementId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomEntryVehicleSnapshot) GetExpiresAtMs() int64 {
|
||||
if x != nil {
|
||||
return x.ExpiresAtMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type RoomUserJoined struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"`
|
||||
// visible_region_id 是用户进房时所在房间的国家/区域桶,统计服务不能反查 room-service。
|
||||
VisibleRegionId int64 `protobuf:"varint,3,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
|
||||
VisibleRegionId int64 `protobuf:"varint,3,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
|
||||
EntryVehicle *RoomEntryVehicleSnapshot `protobuf:"bytes,4,opt,name=entry_vehicle,json=entryVehicle,proto3" json:"entry_vehicle,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RoomUserJoined) Reset() {
|
||||
*x = RoomUserJoined{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -372,7 +481,7 @@ func (x *RoomUserJoined) String() string {
|
||||
func (*RoomUserJoined) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserJoined) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -385,7 +494,7 @@ func (x *RoomUserJoined) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomUserJoined.ProtoReflect.Descriptor instead.
|
||||
func (*RoomUserJoined) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{4}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *RoomUserJoined) GetUserId() int64 {
|
||||
@ -409,6 +518,13 @@ func (x *RoomUserJoined) GetVisibleRegionId() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomUserJoined) GetEntryVehicle() *RoomEntryVehicleSnapshot {
|
||||
if x != nil {
|
||||
return x.EntryVehicle
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RoomUserLeft 表达用户离房成功。
|
||||
type RoomUserLeft struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -419,7 +535,7 @@ type RoomUserLeft struct {
|
||||
|
||||
func (x *RoomUserLeft) Reset() {
|
||||
*x = RoomUserLeft{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -431,7 +547,7 @@ func (x *RoomUserLeft) String() string {
|
||||
func (*RoomUserLeft) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserLeft) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -444,7 +560,7 @@ func (x *RoomUserLeft) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomUserLeft.ProtoReflect.Descriptor instead.
|
||||
func (*RoomUserLeft) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{5}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *RoomUserLeft) GetUserId() int64 {
|
||||
@ -465,7 +581,7 @@ type RoomClosed struct {
|
||||
|
||||
func (x *RoomClosed) Reset() {
|
||||
*x = RoomClosed{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -477,7 +593,7 @@ func (x *RoomClosed) String() string {
|
||||
func (*RoomClosed) ProtoMessage() {}
|
||||
|
||||
func (x *RoomClosed) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -490,7 +606,7 @@ func (x *RoomClosed) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomClosed.ProtoReflect.Descriptor instead.
|
||||
func (*RoomClosed) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{6}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *RoomClosed) GetActorUserId() int64 {
|
||||
@ -531,7 +647,7 @@ type RoomMicChanged struct {
|
||||
|
||||
func (x *RoomMicChanged) Reset() {
|
||||
*x = RoomMicChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -543,7 +659,7 @@ func (x *RoomMicChanged) String() string {
|
||||
func (*RoomMicChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomMicChanged) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -556,7 +672,7 @@ func (x *RoomMicChanged) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomMicChanged.ProtoReflect.Descriptor instead.
|
||||
func (*RoomMicChanged) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{7}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *RoomMicChanged) GetActorUserId() int64 {
|
||||
@ -655,7 +771,7 @@ type RoomMicSeatLocked struct {
|
||||
|
||||
func (x *RoomMicSeatLocked) Reset() {
|
||||
*x = RoomMicSeatLocked{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -667,7 +783,7 @@ func (x *RoomMicSeatLocked) String() string {
|
||||
func (*RoomMicSeatLocked) ProtoMessage() {}
|
||||
|
||||
func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -680,7 +796,7 @@ func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomMicSeatLocked.ProtoReflect.Descriptor instead.
|
||||
func (*RoomMicSeatLocked) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{8}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *RoomMicSeatLocked) GetActorUserId() int64 {
|
||||
@ -715,7 +831,7 @@ type RoomChatEnabledChanged struct {
|
||||
|
||||
func (x *RoomChatEnabledChanged) Reset() {
|
||||
*x = RoomChatEnabledChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -727,7 +843,7 @@ func (x *RoomChatEnabledChanged) String() string {
|
||||
func (*RoomChatEnabledChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -740,7 +856,7 @@ func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomChatEnabledChanged.ProtoReflect.Descriptor instead.
|
||||
func (*RoomChatEnabledChanged) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{9}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *RoomChatEnabledChanged) GetActorUserId() int64 {
|
||||
@ -768,7 +884,7 @@ type RoomPasswordChanged struct {
|
||||
|
||||
func (x *RoomPasswordChanged) Reset() {
|
||||
*x = RoomPasswordChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -780,7 +896,7 @@ func (x *RoomPasswordChanged) String() string {
|
||||
func (*RoomPasswordChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomPasswordChanged) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -793,7 +909,7 @@ func (x *RoomPasswordChanged) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomPasswordChanged.ProtoReflect.Descriptor instead.
|
||||
func (*RoomPasswordChanged) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{10}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *RoomPasswordChanged) GetActorUserId() int64 {
|
||||
@ -822,7 +938,7 @@ type RoomAdminChanged struct {
|
||||
|
||||
func (x *RoomAdminChanged) Reset() {
|
||||
*x = RoomAdminChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -834,7 +950,7 @@ func (x *RoomAdminChanged) String() string {
|
||||
func (*RoomAdminChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -847,7 +963,7 @@ func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomAdminChanged.ProtoReflect.Descriptor instead.
|
||||
func (*RoomAdminChanged) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{11}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *RoomAdminChanged) GetActorUserId() int64 {
|
||||
@ -883,7 +999,7 @@ type RoomUserMuted struct {
|
||||
|
||||
func (x *RoomUserMuted) Reset() {
|
||||
*x = RoomUserMuted{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -895,7 +1011,7 @@ func (x *RoomUserMuted) String() string {
|
||||
func (*RoomUserMuted) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -908,7 +1024,7 @@ func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomUserMuted.ProtoReflect.Descriptor instead.
|
||||
func (*RoomUserMuted) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{12}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{13}
|
||||
}
|
||||
|
||||
func (x *RoomUserMuted) GetActorUserId() int64 {
|
||||
@ -943,7 +1059,7 @@ type RoomUserKicked struct {
|
||||
|
||||
func (x *RoomUserKicked) Reset() {
|
||||
*x = RoomUserKicked{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -955,7 +1071,7 @@ func (x *RoomUserKicked) String() string {
|
||||
func (*RoomUserKicked) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -968,7 +1084,7 @@ func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomUserKicked.ProtoReflect.Descriptor instead.
|
||||
func (*RoomUserKicked) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{13}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{14}
|
||||
}
|
||||
|
||||
func (x *RoomUserKicked) GetActorUserId() int64 {
|
||||
@ -996,7 +1112,7 @@ type RoomUserUnbanned struct {
|
||||
|
||||
func (x *RoomUserUnbanned) Reset() {
|
||||
*x = RoomUserUnbanned{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1008,7 +1124,7 @@ func (x *RoomUserUnbanned) String() string {
|
||||
func (*RoomUserUnbanned) ProtoMessage() {}
|
||||
|
||||
func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1021,7 +1137,7 @@ func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomUserUnbanned.ProtoReflect.Descriptor instead.
|
||||
func (*RoomUserUnbanned) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{14}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{15}
|
||||
}
|
||||
|
||||
func (x *RoomUserUnbanned) GetActorUserId() int64 {
|
||||
@ -1056,7 +1172,7 @@ type RoomGiftSent struct {
|
||||
|
||||
func (x *RoomGiftSent) Reset() {
|
||||
*x = RoomGiftSent{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1068,7 +1184,7 @@ func (x *RoomGiftSent) String() string {
|
||||
func (*RoomGiftSent) ProtoMessage() {}
|
||||
|
||||
func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1081,7 +1197,7 @@ func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomGiftSent.ProtoReflect.Descriptor instead.
|
||||
func (*RoomGiftSent) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{15}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{16}
|
||||
}
|
||||
|
||||
func (x *RoomGiftSent) GetSenderUserId() int64 {
|
||||
@ -1158,7 +1274,7 @@ type RoomHeatChanged struct {
|
||||
|
||||
func (x *RoomHeatChanged) Reset() {
|
||||
*x = RoomHeatChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1170,7 +1286,7 @@ func (x *RoomHeatChanged) String() string {
|
||||
func (*RoomHeatChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1183,7 +1299,7 @@ func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomHeatChanged.ProtoReflect.Descriptor instead.
|
||||
func (*RoomHeatChanged) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{16}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{17}
|
||||
}
|
||||
|
||||
func (x *RoomHeatChanged) GetDelta() int64 {
|
||||
@ -1212,7 +1328,7 @@ type RoomRankChanged struct {
|
||||
|
||||
func (x *RoomRankChanged) Reset() {
|
||||
*x = RoomRankChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1224,7 +1340,7 @@ func (x *RoomRankChanged) String() string {
|
||||
func (*RoomRankChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1237,7 +1353,7 @@ func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomRankChanged.ProtoReflect.Descriptor instead.
|
||||
func (*RoomRankChanged) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{17}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{18}
|
||||
}
|
||||
|
||||
func (x *RoomRankChanged) GetUserId() int64 {
|
||||
@ -1277,7 +1393,7 @@ type RoomTreasureRewardGrant struct {
|
||||
|
||||
func (x *RoomTreasureRewardGrant) Reset() {
|
||||
*x = RoomTreasureRewardGrant{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1289,7 +1405,7 @@ func (x *RoomTreasureRewardGrant) String() string {
|
||||
func (*RoomTreasureRewardGrant) ProtoMessage() {}
|
||||
|
||||
func (x *RoomTreasureRewardGrant) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1302,7 +1418,7 @@ func (x *RoomTreasureRewardGrant) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomTreasureRewardGrant.ProtoReflect.Descriptor instead.
|
||||
func (*RoomTreasureRewardGrant) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{18}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19}
|
||||
}
|
||||
|
||||
func (x *RoomTreasureRewardGrant) GetRewardRole() string {
|
||||
@ -1384,7 +1500,7 @@ type RoomTreasureProgressChanged struct {
|
||||
|
||||
func (x *RoomTreasureProgressChanged) Reset() {
|
||||
*x = RoomTreasureProgressChanged{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1396,7 +1512,7 @@ func (x *RoomTreasureProgressChanged) String() string {
|
||||
func (*RoomTreasureProgressChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomTreasureProgressChanged) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1409,7 +1525,7 @@ func (x *RoomTreasureProgressChanged) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomTreasureProgressChanged.ProtoReflect.Descriptor instead.
|
||||
func (*RoomTreasureProgressChanged) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20}
|
||||
}
|
||||
|
||||
func (x *RoomTreasureProgressChanged) GetBoxId() string {
|
||||
@ -1530,7 +1646,7 @@ type RoomTreasureCountdownStarted struct {
|
||||
|
||||
func (x *RoomTreasureCountdownStarted) Reset() {
|
||||
*x = RoomTreasureCountdownStarted{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1542,7 +1658,7 @@ func (x *RoomTreasureCountdownStarted) String() string {
|
||||
func (*RoomTreasureCountdownStarted) ProtoMessage() {}
|
||||
|
||||
func (x *RoomTreasureCountdownStarted) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1555,7 +1671,7 @@ func (x *RoomTreasureCountdownStarted) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomTreasureCountdownStarted.ProtoReflect.Descriptor instead.
|
||||
func (*RoomTreasureCountdownStarted) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21}
|
||||
}
|
||||
|
||||
func (x *RoomTreasureCountdownStarted) GetBoxId() string {
|
||||
@ -1653,7 +1769,7 @@ type RoomTreasureOpened struct {
|
||||
|
||||
func (x *RoomTreasureOpened) Reset() {
|
||||
*x = RoomTreasureOpened{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1665,7 +1781,7 @@ func (x *RoomTreasureOpened) String() string {
|
||||
func (*RoomTreasureOpened) ProtoMessage() {}
|
||||
|
||||
func (x *RoomTreasureOpened) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1678,7 +1794,7 @@ func (x *RoomTreasureOpened) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomTreasureOpened.ProtoReflect.Descriptor instead.
|
||||
func (*RoomTreasureOpened) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{22}
|
||||
}
|
||||
|
||||
func (x *RoomTreasureOpened) GetBoxId() string {
|
||||
@ -1756,7 +1872,7 @@ type RoomTreasureRewardGranted struct {
|
||||
|
||||
func (x *RoomTreasureRewardGranted) Reset() {
|
||||
*x = RoomTreasureRewardGranted{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[23]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1768,7 +1884,7 @@ func (x *RoomTreasureRewardGranted) String() string {
|
||||
func (*RoomTreasureRewardGranted) ProtoMessage() {}
|
||||
|
||||
func (x *RoomTreasureRewardGranted) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[23]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1781,7 +1897,7 @@ func (x *RoomTreasureRewardGranted) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomTreasureRewardGranted.ProtoReflect.Descriptor instead.
|
||||
func (*RoomTreasureRewardGranted) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{22}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{23}
|
||||
}
|
||||
|
||||
func (x *RoomTreasureRewardGranted) GetBoxId() string {
|
||||
@ -1840,11 +1956,24 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
|
||||
"\x15RoomBackgroundChanged\x12\"\n" +
|
||||
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12#\n" +
|
||||
"\rbackground_id\x18\x02 \x01(\x03R\fbackgroundId\x12.\n" +
|
||||
"\x13room_background_url\x18\x03 \x01(\tR\x11roomBackgroundUrl\"i\n" +
|
||||
"\x13room_background_url\x18\x03 \x01(\tR\x11roomBackgroundUrl\"\xc7\x02\n" +
|
||||
"\x18RoomEntryVehicleSnapshot\x12\x1f\n" +
|
||||
"\vresource_id\x18\x01 \x01(\x03R\n" +
|
||||
"resourceId\x12#\n" +
|
||||
"\rresource_code\x18\x02 \x01(\tR\fresourceCode\x12\x12\n" +
|
||||
"\x04name\x18\x03 \x01(\tR\x04name\x12\x1b\n" +
|
||||
"\tasset_url\x18\x04 \x01(\tR\bassetUrl\x12\x1f\n" +
|
||||
"\vpreview_url\x18\x05 \x01(\tR\n" +
|
||||
"previewUrl\x12#\n" +
|
||||
"\ranimation_url\x18\x06 \x01(\tR\fanimationUrl\x12#\n" +
|
||||
"\rmetadata_json\x18\a \x01(\tR\fmetadataJson\x12%\n" +
|
||||
"\x0eentitlement_id\x18\b \x01(\tR\rentitlementId\x12\"\n" +
|
||||
"\rexpires_at_ms\x18\t \x01(\x03R\vexpiresAtMs\"\xbe\x01\n" +
|
||||
"\x0eRoomUserJoined\x12\x17\n" +
|
||||
"\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x12\n" +
|
||||
"\x04role\x18\x02 \x01(\tR\x04role\x12*\n" +
|
||||
"\x11visible_region_id\x18\x03 \x01(\x03R\x0fvisibleRegionId\"'\n" +
|
||||
"\x11visible_region_id\x18\x03 \x01(\x03R\x0fvisibleRegionId\x12S\n" +
|
||||
"\rentry_vehicle\x18\x04 \x01(\v2..hyapp.events.room.v1.RoomEntryVehicleSnapshotR\fentryVehicle\"'\n" +
|
||||
"\fRoomUserLeft\x12\x17\n" +
|
||||
"\auser_id\x18\x01 \x01(\x03R\x06userId\"H\n" +
|
||||
"\n" +
|
||||
@ -1985,40 +2114,42 @@ func file_proto_events_room_v1_events_proto_rawDescGZIP() []byte {
|
||||
return file_proto_events_room_v1_events_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
|
||||
var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 24)
|
||||
var file_proto_events_room_v1_events_proto_goTypes = []any{
|
||||
(*EventEnvelope)(nil), // 0: hyapp.events.room.v1.EventEnvelope
|
||||
(*RoomCreated)(nil), // 1: hyapp.events.room.v1.RoomCreated
|
||||
(*RoomProfileUpdated)(nil), // 2: hyapp.events.room.v1.RoomProfileUpdated
|
||||
(*RoomBackgroundChanged)(nil), // 3: hyapp.events.room.v1.RoomBackgroundChanged
|
||||
(*RoomUserJoined)(nil), // 4: hyapp.events.room.v1.RoomUserJoined
|
||||
(*RoomUserLeft)(nil), // 5: hyapp.events.room.v1.RoomUserLeft
|
||||
(*RoomClosed)(nil), // 6: hyapp.events.room.v1.RoomClosed
|
||||
(*RoomMicChanged)(nil), // 7: hyapp.events.room.v1.RoomMicChanged
|
||||
(*RoomMicSeatLocked)(nil), // 8: hyapp.events.room.v1.RoomMicSeatLocked
|
||||
(*RoomChatEnabledChanged)(nil), // 9: hyapp.events.room.v1.RoomChatEnabledChanged
|
||||
(*RoomPasswordChanged)(nil), // 10: hyapp.events.room.v1.RoomPasswordChanged
|
||||
(*RoomAdminChanged)(nil), // 11: hyapp.events.room.v1.RoomAdminChanged
|
||||
(*RoomUserMuted)(nil), // 12: hyapp.events.room.v1.RoomUserMuted
|
||||
(*RoomUserKicked)(nil), // 13: hyapp.events.room.v1.RoomUserKicked
|
||||
(*RoomUserUnbanned)(nil), // 14: hyapp.events.room.v1.RoomUserUnbanned
|
||||
(*RoomGiftSent)(nil), // 15: hyapp.events.room.v1.RoomGiftSent
|
||||
(*RoomHeatChanged)(nil), // 16: hyapp.events.room.v1.RoomHeatChanged
|
||||
(*RoomRankChanged)(nil), // 17: hyapp.events.room.v1.RoomRankChanged
|
||||
(*RoomTreasureRewardGrant)(nil), // 18: hyapp.events.room.v1.RoomTreasureRewardGrant
|
||||
(*RoomTreasureProgressChanged)(nil), // 19: hyapp.events.room.v1.RoomTreasureProgressChanged
|
||||
(*RoomTreasureCountdownStarted)(nil), // 20: hyapp.events.room.v1.RoomTreasureCountdownStarted
|
||||
(*RoomTreasureOpened)(nil), // 21: hyapp.events.room.v1.RoomTreasureOpened
|
||||
(*RoomTreasureRewardGranted)(nil), // 22: hyapp.events.room.v1.RoomTreasureRewardGranted
|
||||
(*RoomEntryVehicleSnapshot)(nil), // 4: hyapp.events.room.v1.RoomEntryVehicleSnapshot
|
||||
(*RoomUserJoined)(nil), // 5: hyapp.events.room.v1.RoomUserJoined
|
||||
(*RoomUserLeft)(nil), // 6: hyapp.events.room.v1.RoomUserLeft
|
||||
(*RoomClosed)(nil), // 7: hyapp.events.room.v1.RoomClosed
|
||||
(*RoomMicChanged)(nil), // 8: hyapp.events.room.v1.RoomMicChanged
|
||||
(*RoomMicSeatLocked)(nil), // 9: hyapp.events.room.v1.RoomMicSeatLocked
|
||||
(*RoomChatEnabledChanged)(nil), // 10: hyapp.events.room.v1.RoomChatEnabledChanged
|
||||
(*RoomPasswordChanged)(nil), // 11: hyapp.events.room.v1.RoomPasswordChanged
|
||||
(*RoomAdminChanged)(nil), // 12: hyapp.events.room.v1.RoomAdminChanged
|
||||
(*RoomUserMuted)(nil), // 13: hyapp.events.room.v1.RoomUserMuted
|
||||
(*RoomUserKicked)(nil), // 14: hyapp.events.room.v1.RoomUserKicked
|
||||
(*RoomUserUnbanned)(nil), // 15: hyapp.events.room.v1.RoomUserUnbanned
|
||||
(*RoomGiftSent)(nil), // 16: hyapp.events.room.v1.RoomGiftSent
|
||||
(*RoomHeatChanged)(nil), // 17: hyapp.events.room.v1.RoomHeatChanged
|
||||
(*RoomRankChanged)(nil), // 18: hyapp.events.room.v1.RoomRankChanged
|
||||
(*RoomTreasureRewardGrant)(nil), // 19: hyapp.events.room.v1.RoomTreasureRewardGrant
|
||||
(*RoomTreasureProgressChanged)(nil), // 20: hyapp.events.room.v1.RoomTreasureProgressChanged
|
||||
(*RoomTreasureCountdownStarted)(nil), // 21: hyapp.events.room.v1.RoomTreasureCountdownStarted
|
||||
(*RoomTreasureOpened)(nil), // 22: hyapp.events.room.v1.RoomTreasureOpened
|
||||
(*RoomTreasureRewardGranted)(nil), // 23: hyapp.events.room.v1.RoomTreasureRewardGranted
|
||||
}
|
||||
var file_proto_events_room_v1_events_proto_depIdxs = []int32{
|
||||
18, // 0: hyapp.events.room.v1.RoomTreasureOpened.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
|
||||
18, // 1: hyapp.events.room.v1.RoomTreasureRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
4, // 0: hyapp.events.room.v1.RoomUserJoined.entry_vehicle:type_name -> hyapp.events.room.v1.RoomEntryVehicleSnapshot
|
||||
19, // 1: hyapp.events.room.v1.RoomTreasureOpened.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
|
||||
19, // 2: hyapp.events.room.v1.RoomTreasureRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
|
||||
3, // [3:3] is the sub-list for method output_type
|
||||
3, // [3:3] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
3, // [3:3] is the sub-list for extension extendee
|
||||
0, // [0:3] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proto_events_room_v1_events_proto_init() }
|
||||
@ -2032,7 +2163,7 @@ func file_proto_events_room_v1_events_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_events_room_v1_events_proto_rawDesc), len(file_proto_events_room_v1_events_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 23,
|
||||
NumMessages: 24,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@ -45,11 +45,24 @@ message RoomBackgroundChanged {
|
||||
}
|
||||
|
||||
// RoomUserJoined 表达用户业务态进房成功。
|
||||
message RoomEntryVehicleSnapshot {
|
||||
int64 resource_id = 1;
|
||||
string resource_code = 2;
|
||||
string name = 3;
|
||||
string asset_url = 4;
|
||||
string preview_url = 5;
|
||||
string animation_url = 6;
|
||||
string metadata_json = 7;
|
||||
string entitlement_id = 8;
|
||||
int64 expires_at_ms = 9;
|
||||
}
|
||||
|
||||
message RoomUserJoined {
|
||||
int64 user_id = 1;
|
||||
string role = 2;
|
||||
// visible_region_id 是用户进房时所在房间的国家/区域桶,统计服务不能反查 room-service。
|
||||
int64 visible_region_id = 3;
|
||||
RoomEntryVehicleSnapshot entry_vehicle = 4;
|
||||
}
|
||||
|
||||
// RoomUserLeft 表达用户离房成功。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -96,6 +96,8 @@ message LuckyGiftDrawResult {
|
||||
int64 created_at_ms = 17;
|
||||
string wallet_transaction_id = 18;
|
||||
int64 coin_balance_after = 19;
|
||||
// target_user_id 是本次幸运礼物抽奖对应的收礼用户;多目标送礼用它关联结果。
|
||||
int64 target_user_id = 20;
|
||||
}
|
||||
|
||||
// RoomTreasureRewardItem 是后台配置给客户端展示的宝箱奖励候选项。
|
||||
@ -426,11 +428,24 @@ message UpdateRoomProfileResponse {
|
||||
}
|
||||
|
||||
// JoinRoomRequest 把用户 presence 接入房间业务态。
|
||||
message RoomEntryVehicleSnapshot {
|
||||
int64 resource_id = 1;
|
||||
string resource_code = 2;
|
||||
string name = 3;
|
||||
string asset_url = 4;
|
||||
string preview_url = 5;
|
||||
string animation_url = 6;
|
||||
string metadata_json = 7;
|
||||
string entitlement_id = 8;
|
||||
int64 expires_at_ms = 9;
|
||||
}
|
||||
|
||||
message JoinRoomRequest {
|
||||
RequestMeta meta = 1;
|
||||
string role = 2;
|
||||
// password 只用于本次入房校验;room-service 不把明文写入 command log 或快照。
|
||||
string password = 3;
|
||||
RoomEntryVehicleSnapshot entry_vehicle = 4;
|
||||
}
|
||||
|
||||
// JoinRoomResponse 返回加入后的房间快照。
|
||||
@ -796,7 +811,10 @@ message SendGiftResponse {
|
||||
repeated RankItem gift_rank = 4;
|
||||
RoomSnapshot room = 5;
|
||||
RoomTreasureState treasure = 6;
|
||||
// lucky_gift 是本次送礼的聚合幸运礼物结果;多目标时倍率和返奖金额按所有目标累加。
|
||||
LuckyGiftDrawResult lucky_gift = 7;
|
||||
// lucky_gifts 是每个收礼目标的独立抽奖结果;多目标客户端用 target_user_id 对齐明细表现。
|
||||
repeated LuckyGiftDrawResult lucky_gifts = 8;
|
||||
}
|
||||
|
||||
// CheckSpeakPermissionRequest 让腾讯云 IM 发言回调或 gateway 在公屏前同步问房间业务态。
|
||||
|
||||
@ -2556,6 +2556,102 @@ func (x *GetUserRoleSummaryResponse) GetSummary() *UserRoleSummary {
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetAgencyRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
AgencyId int64 `protobuf:"varint,2,opt,name=agency_id,json=agencyId,proto3" json:"agency_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetAgencyRequest) Reset() {
|
||||
*x = GetAgencyRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[33]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetAgencyRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetAgencyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetAgencyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[33]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetAgencyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetAgencyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{33}
|
||||
}
|
||||
|
||||
func (x *GetAgencyRequest) GetMeta() *RequestMeta {
|
||||
if x != nil {
|
||||
return x.Meta
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetAgencyRequest) GetAgencyId() int64 {
|
||||
if x != nil {
|
||||
return x.AgencyId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetAgencyResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Agency *Agency `protobuf:"bytes,1,opt,name=agency,proto3" json:"agency,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetAgencyResponse) Reset() {
|
||||
*x = GetAgencyResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[34]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetAgencyResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetAgencyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetAgencyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[34]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetAgencyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetAgencyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{34}
|
||||
}
|
||||
|
||||
func (x *GetAgencyResponse) GetAgency() *Agency {
|
||||
if x != nil {
|
||||
return x.Agency
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CheckBusinessCapabilityRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
@ -2567,7 +2663,7 @@ type CheckBusinessCapabilityRequest struct {
|
||||
|
||||
func (x *CheckBusinessCapabilityRequest) Reset() {
|
||||
*x = CheckBusinessCapabilityRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[33]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[35]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2579,7 +2675,7 @@ func (x *CheckBusinessCapabilityRequest) String() string {
|
||||
func (*CheckBusinessCapabilityRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CheckBusinessCapabilityRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[33]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[35]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2592,7 +2688,7 @@ func (x *CheckBusinessCapabilityRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CheckBusinessCapabilityRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CheckBusinessCapabilityRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{33}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{35}
|
||||
}
|
||||
|
||||
func (x *CheckBusinessCapabilityRequest) GetMeta() *RequestMeta {
|
||||
@ -2626,7 +2722,7 @@ type CheckBusinessCapabilityResponse struct {
|
||||
|
||||
func (x *CheckBusinessCapabilityResponse) Reset() {
|
||||
*x = CheckBusinessCapabilityResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[34]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[36]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2638,7 +2734,7 @@ func (x *CheckBusinessCapabilityResponse) String() string {
|
||||
func (*CheckBusinessCapabilityResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CheckBusinessCapabilityResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[34]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[36]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2651,7 +2747,7 @@ func (x *CheckBusinessCapabilityResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CheckBusinessCapabilityResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CheckBusinessCapabilityResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{34}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{36}
|
||||
}
|
||||
|
||||
func (x *CheckBusinessCapabilityResponse) GetAllowed() bool {
|
||||
@ -2679,7 +2775,7 @@ type GetAgencyMembersRequest struct {
|
||||
|
||||
func (x *GetAgencyMembersRequest) Reset() {
|
||||
*x = GetAgencyMembersRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[35]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[37]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2691,7 +2787,7 @@ func (x *GetAgencyMembersRequest) String() string {
|
||||
func (*GetAgencyMembersRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetAgencyMembersRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[35]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[37]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2704,7 +2800,7 @@ func (x *GetAgencyMembersRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetAgencyMembersRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetAgencyMembersRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{35}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{37}
|
||||
}
|
||||
|
||||
func (x *GetAgencyMembersRequest) GetMeta() *RequestMeta {
|
||||
@ -2737,7 +2833,7 @@ type GetAgencyMembersResponse struct {
|
||||
|
||||
func (x *GetAgencyMembersResponse) Reset() {
|
||||
*x = GetAgencyMembersResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[36]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[38]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2749,7 +2845,7 @@ func (x *GetAgencyMembersResponse) String() string {
|
||||
func (*GetAgencyMembersResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetAgencyMembersResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[36]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[38]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2762,7 +2858,7 @@ func (x *GetAgencyMembersResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetAgencyMembersResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetAgencyMembersResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{36}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{38}
|
||||
}
|
||||
|
||||
func (x *GetAgencyMembersResponse) GetMemberships() []*AgencyMembership {
|
||||
@ -2783,7 +2879,7 @@ type GetAgencyApplicationsRequest struct {
|
||||
|
||||
func (x *GetAgencyApplicationsRequest) Reset() {
|
||||
*x = GetAgencyApplicationsRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[37]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[39]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2795,7 +2891,7 @@ func (x *GetAgencyApplicationsRequest) String() string {
|
||||
func (*GetAgencyApplicationsRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetAgencyApplicationsRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[37]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[39]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2808,7 +2904,7 @@ func (x *GetAgencyApplicationsRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetAgencyApplicationsRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetAgencyApplicationsRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{37}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{39}
|
||||
}
|
||||
|
||||
func (x *GetAgencyApplicationsRequest) GetMeta() *RequestMeta {
|
||||
@ -2841,7 +2937,7 @@ type GetAgencyApplicationsResponse struct {
|
||||
|
||||
func (x *GetAgencyApplicationsResponse) Reset() {
|
||||
*x = GetAgencyApplicationsResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[38]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[40]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2853,7 +2949,7 @@ func (x *GetAgencyApplicationsResponse) String() string {
|
||||
func (*GetAgencyApplicationsResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetAgencyApplicationsResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[38]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[40]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2866,7 +2962,7 @@ func (x *GetAgencyApplicationsResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use GetAgencyApplicationsResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetAgencyApplicationsResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{38}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{40}
|
||||
}
|
||||
|
||||
func (x *GetAgencyApplicationsResponse) GetApplications() []*AgencyApplication {
|
||||
@ -2890,7 +2986,7 @@ type CreateBDLeaderRequest struct {
|
||||
|
||||
func (x *CreateBDLeaderRequest) Reset() {
|
||||
*x = CreateBDLeaderRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[39]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[41]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2902,7 +2998,7 @@ func (x *CreateBDLeaderRequest) String() string {
|
||||
func (*CreateBDLeaderRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreateBDLeaderRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[39]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[41]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2915,7 +3011,7 @@ func (x *CreateBDLeaderRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateBDLeaderRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreateBDLeaderRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{39}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{41}
|
||||
}
|
||||
|
||||
func (x *CreateBDLeaderRequest) GetMeta() *RequestMeta {
|
||||
@ -2969,7 +3065,7 @@ type CreateBDLeaderResponse struct {
|
||||
|
||||
func (x *CreateBDLeaderResponse) Reset() {
|
||||
*x = CreateBDLeaderResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[40]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[42]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2981,7 +3077,7 @@ func (x *CreateBDLeaderResponse) String() string {
|
||||
func (*CreateBDLeaderResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreateBDLeaderResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[40]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[42]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2994,7 +3090,7 @@ func (x *CreateBDLeaderResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateBDLeaderResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreateBDLeaderResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{40}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{42}
|
||||
}
|
||||
|
||||
func (x *CreateBDLeaderResponse) GetBdProfile() *BDProfile {
|
||||
@ -3018,7 +3114,7 @@ type CreateBDRequest struct {
|
||||
|
||||
func (x *CreateBDRequest) Reset() {
|
||||
*x = CreateBDRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[41]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[43]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3030,7 +3126,7 @@ func (x *CreateBDRequest) String() string {
|
||||
func (*CreateBDRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreateBDRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[41]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[43]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3043,7 +3139,7 @@ func (x *CreateBDRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateBDRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreateBDRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{41}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{43}
|
||||
}
|
||||
|
||||
func (x *CreateBDRequest) GetMeta() *RequestMeta {
|
||||
@ -3097,7 +3193,7 @@ type CreateBDResponse struct {
|
||||
|
||||
func (x *CreateBDResponse) Reset() {
|
||||
*x = CreateBDResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[42]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[44]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3109,7 +3205,7 @@ func (x *CreateBDResponse) String() string {
|
||||
func (*CreateBDResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreateBDResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[42]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[44]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3122,7 +3218,7 @@ func (x *CreateBDResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateBDResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreateBDResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{42}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{44}
|
||||
}
|
||||
|
||||
func (x *CreateBDResponse) GetBdProfile() *BDProfile {
|
||||
@ -3146,7 +3242,7 @@ type SetBDStatusRequest struct {
|
||||
|
||||
func (x *SetBDStatusRequest) Reset() {
|
||||
*x = SetBDStatusRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[43]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[45]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3158,7 +3254,7 @@ func (x *SetBDStatusRequest) String() string {
|
||||
func (*SetBDStatusRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SetBDStatusRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[43]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[45]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3171,7 +3267,7 @@ func (x *SetBDStatusRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use SetBDStatusRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SetBDStatusRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{43}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{45}
|
||||
}
|
||||
|
||||
func (x *SetBDStatusRequest) GetMeta() *RequestMeta {
|
||||
@ -3225,7 +3321,7 @@ type SetBDStatusResponse struct {
|
||||
|
||||
func (x *SetBDStatusResponse) Reset() {
|
||||
*x = SetBDStatusResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[44]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[46]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3237,7 +3333,7 @@ func (x *SetBDStatusResponse) String() string {
|
||||
func (*SetBDStatusResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SetBDStatusResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[44]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[46]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3250,7 +3346,7 @@ func (x *SetBDStatusResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use SetBDStatusResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SetBDStatusResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{44}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{46}
|
||||
}
|
||||
|
||||
func (x *SetBDStatusResponse) GetBdProfile() *BDProfile {
|
||||
@ -3273,7 +3369,7 @@ type CreateCoinSellerRequest struct {
|
||||
|
||||
func (x *CreateCoinSellerRequest) Reset() {
|
||||
*x = CreateCoinSellerRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[45]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[47]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3285,7 +3381,7 @@ func (x *CreateCoinSellerRequest) String() string {
|
||||
func (*CreateCoinSellerRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreateCoinSellerRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[45]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[47]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3298,7 +3394,7 @@ func (x *CreateCoinSellerRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateCoinSellerRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreateCoinSellerRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{45}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{47}
|
||||
}
|
||||
|
||||
func (x *CreateCoinSellerRequest) GetMeta() *RequestMeta {
|
||||
@ -3345,7 +3441,7 @@ type CreateCoinSellerResponse struct {
|
||||
|
||||
func (x *CreateCoinSellerResponse) Reset() {
|
||||
*x = CreateCoinSellerResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[46]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[48]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3357,7 +3453,7 @@ func (x *CreateCoinSellerResponse) String() string {
|
||||
func (*CreateCoinSellerResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreateCoinSellerResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[46]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[48]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3370,7 +3466,7 @@ func (x *CreateCoinSellerResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateCoinSellerResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreateCoinSellerResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{46}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{48}
|
||||
}
|
||||
|
||||
func (x *CreateCoinSellerResponse) GetCoinSellerProfile() *CoinSellerProfile {
|
||||
@ -3394,7 +3490,7 @@ type SetCoinSellerStatusRequest struct {
|
||||
|
||||
func (x *SetCoinSellerStatusRequest) Reset() {
|
||||
*x = SetCoinSellerStatusRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[47]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[49]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3406,7 +3502,7 @@ func (x *SetCoinSellerStatusRequest) String() string {
|
||||
func (*SetCoinSellerStatusRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SetCoinSellerStatusRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[47]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[49]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3419,7 +3515,7 @@ func (x *SetCoinSellerStatusRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use SetCoinSellerStatusRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SetCoinSellerStatusRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{47}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{49}
|
||||
}
|
||||
|
||||
func (x *SetCoinSellerStatusRequest) GetMeta() *RequestMeta {
|
||||
@ -3473,7 +3569,7 @@ type SetCoinSellerStatusResponse struct {
|
||||
|
||||
func (x *SetCoinSellerStatusResponse) Reset() {
|
||||
*x = SetCoinSellerStatusResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[48]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[50]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3485,7 +3581,7 @@ func (x *SetCoinSellerStatusResponse) String() string {
|
||||
func (*SetCoinSellerStatusResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SetCoinSellerStatusResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[48]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[50]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3498,7 +3594,7 @@ func (x *SetCoinSellerStatusResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use SetCoinSellerStatusResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SetCoinSellerStatusResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{48}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{50}
|
||||
}
|
||||
|
||||
func (x *SetCoinSellerStatusResponse) GetCoinSellerProfile() *CoinSellerProfile {
|
||||
@ -3525,7 +3621,7 @@ type CreateAgencyRequest struct {
|
||||
|
||||
func (x *CreateAgencyRequest) Reset() {
|
||||
*x = CreateAgencyRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[49]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[51]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3537,7 +3633,7 @@ func (x *CreateAgencyRequest) String() string {
|
||||
func (*CreateAgencyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreateAgencyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[49]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[51]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3550,7 +3646,7 @@ func (x *CreateAgencyRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateAgencyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreateAgencyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{49}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{51}
|
||||
}
|
||||
|
||||
func (x *CreateAgencyRequest) GetMeta() *RequestMeta {
|
||||
@ -3627,7 +3723,7 @@ type CreateAgencyResponse struct {
|
||||
|
||||
func (x *CreateAgencyResponse) Reset() {
|
||||
*x = CreateAgencyResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[50]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[52]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3639,7 +3735,7 @@ func (x *CreateAgencyResponse) String() string {
|
||||
func (*CreateAgencyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreateAgencyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[50]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[52]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3652,7 +3748,7 @@ func (x *CreateAgencyResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CreateAgencyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreateAgencyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{50}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{52}
|
||||
}
|
||||
|
||||
func (x *CreateAgencyResponse) GetAgency() *Agency {
|
||||
@ -3689,7 +3785,7 @@ type CloseAgencyRequest struct {
|
||||
|
||||
func (x *CloseAgencyRequest) Reset() {
|
||||
*x = CloseAgencyRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[51]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[53]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3701,7 +3797,7 @@ func (x *CloseAgencyRequest) String() string {
|
||||
func (*CloseAgencyRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CloseAgencyRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[51]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[53]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3714,7 +3810,7 @@ func (x *CloseAgencyRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CloseAgencyRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CloseAgencyRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{51}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{53}
|
||||
}
|
||||
|
||||
func (x *CloseAgencyRequest) GetMeta() *RequestMeta {
|
||||
@ -3761,7 +3857,7 @@ type CloseAgencyResponse struct {
|
||||
|
||||
func (x *CloseAgencyResponse) Reset() {
|
||||
*x = CloseAgencyResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[52]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[54]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3773,7 +3869,7 @@ func (x *CloseAgencyResponse) String() string {
|
||||
func (*CloseAgencyResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CloseAgencyResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[52]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[54]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3786,7 +3882,7 @@ func (x *CloseAgencyResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CloseAgencyResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CloseAgencyResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{52}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{54}
|
||||
}
|
||||
|
||||
func (x *CloseAgencyResponse) GetAgency() *Agency {
|
||||
@ -3810,7 +3906,7 @@ type SetAgencyJoinEnabledRequest struct {
|
||||
|
||||
func (x *SetAgencyJoinEnabledRequest) Reset() {
|
||||
*x = SetAgencyJoinEnabledRequest{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[53]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[55]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3822,7 +3918,7 @@ func (x *SetAgencyJoinEnabledRequest) String() string {
|
||||
func (*SetAgencyJoinEnabledRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SetAgencyJoinEnabledRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[53]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[55]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3835,7 +3931,7 @@ func (x *SetAgencyJoinEnabledRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use SetAgencyJoinEnabledRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SetAgencyJoinEnabledRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{53}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{55}
|
||||
}
|
||||
|
||||
func (x *SetAgencyJoinEnabledRequest) GetMeta() *RequestMeta {
|
||||
@ -3889,7 +3985,7 @@ type SetAgencyJoinEnabledResponse struct {
|
||||
|
||||
func (x *SetAgencyJoinEnabledResponse) Reset() {
|
||||
*x = SetAgencyJoinEnabledResponse{}
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[54]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[56]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -3901,7 +3997,7 @@ func (x *SetAgencyJoinEnabledResponse) String() string {
|
||||
func (*SetAgencyJoinEnabledResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SetAgencyJoinEnabledResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[54]
|
||||
mi := &file_proto_user_v1_host_proto_msgTypes[56]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -3914,7 +4010,7 @@ func (x *SetAgencyJoinEnabledResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use SetAgencyJoinEnabledResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SetAgencyJoinEnabledResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{54}
|
||||
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{56}
|
||||
}
|
||||
|
||||
func (x *SetAgencyJoinEnabledResponse) GetAgency() *Agency {
|
||||
@ -4169,7 +4265,12 @@ const file_proto_user_v1_host_proto_rawDesc = "" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x17\n" +
|
||||
"\auser_id\x18\x02 \x01(\x03R\x06userId\"V\n" +
|
||||
"\x1aGetUserRoleSummaryResponse\x128\n" +
|
||||
"\asummary\x18\x01 \x01(\v2\x1e.hyapp.user.v1.UserRoleSummaryR\asummary\"\x94\x01\n" +
|
||||
"\asummary\x18\x01 \x01(\v2\x1e.hyapp.user.v1.UserRoleSummaryR\asummary\"_\n" +
|
||||
"\x10GetAgencyRequest\x12.\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x1b\n" +
|
||||
"\tagency_id\x18\x02 \x01(\x03R\bagencyId\"B\n" +
|
||||
"\x11GetAgencyResponse\x12-\n" +
|
||||
"\x06agency\x18\x01 \x01(\v2\x15.hyapp.user.v1.AgencyR\x06agency\"\x94\x01\n" +
|
||||
"\x1eCheckBusinessCapabilityRequest\x12.\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\"\n" +
|
||||
"\ractor_user_id\x18\x02 \x01(\x03R\vactorUserId\x12\x1e\n" +
|
||||
@ -4278,7 +4379,7 @@ const file_proto_user_v1_host_proto_rawDesc = "" +
|
||||
"\fjoin_enabled\x18\x05 \x01(\bR\vjoinEnabled\x12\x16\n" +
|
||||
"\x06reason\x18\x06 \x01(\tR\x06reason\"M\n" +
|
||||
"\x1cSetAgencyJoinEnabledResponse\x12-\n" +
|
||||
"\x06agency\x18\x01 \x01(\v2\x15.hyapp.user.v1.AgencyR\x06agency2\xb9\f\n" +
|
||||
"\x06agency\x18\x01 \x01(\v2\x15.hyapp.user.v1.AgencyR\x06agency2\x89\r\n" +
|
||||
"\x0fUserHostService\x12]\n" +
|
||||
"\x0eSearchAgencies\x12$.hyapp.user.v1.SearchAgenciesRequest\x1a%.hyapp.user.v1.SearchAgenciesResponse\x12Z\n" +
|
||||
"\rApplyToAgency\x12#.hyapp.user.v1.ApplyToAgencyRequest\x1a$.hyapp.user.v1.ApplyToAgencyResponse\x12x\n" +
|
||||
@ -4291,7 +4392,8 @@ const file_proto_user_v1_host_proto_rawDesc = "" +
|
||||
"\fGetBDProfile\x12\".hyapp.user.v1.GetBDProfileRequest\x1a#.hyapp.user.v1.GetBDProfileResponse\x12o\n" +
|
||||
"\x14GetCoinSellerProfile\x12*.hyapp.user.v1.GetCoinSellerProfileRequest\x1a+.hyapp.user.v1.GetCoinSellerProfileResponse\x12\x90\x01\n" +
|
||||
"\x1fListActiveCoinSellersInMyRegion\x125.hyapp.user.v1.ListActiveCoinSellersInMyRegionRequest\x1a6.hyapp.user.v1.ListActiveCoinSellersInMyRegionResponse\x12i\n" +
|
||||
"\x12GetUserRoleSummary\x12(.hyapp.user.v1.GetUserRoleSummaryRequest\x1a).hyapp.user.v1.GetUserRoleSummaryResponse\x12x\n" +
|
||||
"\x12GetUserRoleSummary\x12(.hyapp.user.v1.GetUserRoleSummaryRequest\x1a).hyapp.user.v1.GetUserRoleSummaryResponse\x12N\n" +
|
||||
"\tGetAgency\x12\x1f.hyapp.user.v1.GetAgencyRequest\x1a .hyapp.user.v1.GetAgencyResponse\x12x\n" +
|
||||
"\x17CheckBusinessCapability\x12-.hyapp.user.v1.CheckBusinessCapabilityRequest\x1a..hyapp.user.v1.CheckBusinessCapabilityResponse\x12c\n" +
|
||||
"\x10GetAgencyMembers\x12&.hyapp.user.v1.GetAgencyMembersRequest\x1a'.hyapp.user.v1.GetAgencyMembersResponse\x12r\n" +
|
||||
"\x15GetAgencyApplications\x12+.hyapp.user.v1.GetAgencyApplicationsRequest\x1a,.hyapp.user.v1.GetAgencyApplicationsResponse2\x8b\x06\n" +
|
||||
@ -4317,7 +4419,7 @@ func file_proto_user_v1_host_proto_rawDescGZIP() []byte {
|
||||
return file_proto_user_v1_host_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proto_user_v1_host_proto_msgTypes = make([]protoimpl.MessageInfo, 55)
|
||||
var file_proto_user_v1_host_proto_msgTypes = make([]protoimpl.MessageInfo, 57)
|
||||
var file_proto_user_v1_host_proto_goTypes = []any{
|
||||
(*HostProfile)(nil), // 0: hyapp.user.v1.HostProfile
|
||||
(*Agency)(nil), // 1: hyapp.user.v1.Agency
|
||||
@ -4352,136 +4454,142 @@ var file_proto_user_v1_host_proto_goTypes = []any{
|
||||
(*ListActiveCoinSellersInMyRegionResponse)(nil), // 30: hyapp.user.v1.ListActiveCoinSellersInMyRegionResponse
|
||||
(*GetUserRoleSummaryRequest)(nil), // 31: hyapp.user.v1.GetUserRoleSummaryRequest
|
||||
(*GetUserRoleSummaryResponse)(nil), // 32: hyapp.user.v1.GetUserRoleSummaryResponse
|
||||
(*CheckBusinessCapabilityRequest)(nil), // 33: hyapp.user.v1.CheckBusinessCapabilityRequest
|
||||
(*CheckBusinessCapabilityResponse)(nil), // 34: hyapp.user.v1.CheckBusinessCapabilityResponse
|
||||
(*GetAgencyMembersRequest)(nil), // 35: hyapp.user.v1.GetAgencyMembersRequest
|
||||
(*GetAgencyMembersResponse)(nil), // 36: hyapp.user.v1.GetAgencyMembersResponse
|
||||
(*GetAgencyApplicationsRequest)(nil), // 37: hyapp.user.v1.GetAgencyApplicationsRequest
|
||||
(*GetAgencyApplicationsResponse)(nil), // 38: hyapp.user.v1.GetAgencyApplicationsResponse
|
||||
(*CreateBDLeaderRequest)(nil), // 39: hyapp.user.v1.CreateBDLeaderRequest
|
||||
(*CreateBDLeaderResponse)(nil), // 40: hyapp.user.v1.CreateBDLeaderResponse
|
||||
(*CreateBDRequest)(nil), // 41: hyapp.user.v1.CreateBDRequest
|
||||
(*CreateBDResponse)(nil), // 42: hyapp.user.v1.CreateBDResponse
|
||||
(*SetBDStatusRequest)(nil), // 43: hyapp.user.v1.SetBDStatusRequest
|
||||
(*SetBDStatusResponse)(nil), // 44: hyapp.user.v1.SetBDStatusResponse
|
||||
(*CreateCoinSellerRequest)(nil), // 45: hyapp.user.v1.CreateCoinSellerRequest
|
||||
(*CreateCoinSellerResponse)(nil), // 46: hyapp.user.v1.CreateCoinSellerResponse
|
||||
(*SetCoinSellerStatusRequest)(nil), // 47: hyapp.user.v1.SetCoinSellerStatusRequest
|
||||
(*SetCoinSellerStatusResponse)(nil), // 48: hyapp.user.v1.SetCoinSellerStatusResponse
|
||||
(*CreateAgencyRequest)(nil), // 49: hyapp.user.v1.CreateAgencyRequest
|
||||
(*CreateAgencyResponse)(nil), // 50: hyapp.user.v1.CreateAgencyResponse
|
||||
(*CloseAgencyRequest)(nil), // 51: hyapp.user.v1.CloseAgencyRequest
|
||||
(*CloseAgencyResponse)(nil), // 52: hyapp.user.v1.CloseAgencyResponse
|
||||
(*SetAgencyJoinEnabledRequest)(nil), // 53: hyapp.user.v1.SetAgencyJoinEnabledRequest
|
||||
(*SetAgencyJoinEnabledResponse)(nil), // 54: hyapp.user.v1.SetAgencyJoinEnabledResponse
|
||||
(*RequestMeta)(nil), // 55: hyapp.user.v1.RequestMeta
|
||||
(*GetAgencyRequest)(nil), // 33: hyapp.user.v1.GetAgencyRequest
|
||||
(*GetAgencyResponse)(nil), // 34: hyapp.user.v1.GetAgencyResponse
|
||||
(*CheckBusinessCapabilityRequest)(nil), // 35: hyapp.user.v1.CheckBusinessCapabilityRequest
|
||||
(*CheckBusinessCapabilityResponse)(nil), // 36: hyapp.user.v1.CheckBusinessCapabilityResponse
|
||||
(*GetAgencyMembersRequest)(nil), // 37: hyapp.user.v1.GetAgencyMembersRequest
|
||||
(*GetAgencyMembersResponse)(nil), // 38: hyapp.user.v1.GetAgencyMembersResponse
|
||||
(*GetAgencyApplicationsRequest)(nil), // 39: hyapp.user.v1.GetAgencyApplicationsRequest
|
||||
(*GetAgencyApplicationsResponse)(nil), // 40: hyapp.user.v1.GetAgencyApplicationsResponse
|
||||
(*CreateBDLeaderRequest)(nil), // 41: hyapp.user.v1.CreateBDLeaderRequest
|
||||
(*CreateBDLeaderResponse)(nil), // 42: hyapp.user.v1.CreateBDLeaderResponse
|
||||
(*CreateBDRequest)(nil), // 43: hyapp.user.v1.CreateBDRequest
|
||||
(*CreateBDResponse)(nil), // 44: hyapp.user.v1.CreateBDResponse
|
||||
(*SetBDStatusRequest)(nil), // 45: hyapp.user.v1.SetBDStatusRequest
|
||||
(*SetBDStatusResponse)(nil), // 46: hyapp.user.v1.SetBDStatusResponse
|
||||
(*CreateCoinSellerRequest)(nil), // 47: hyapp.user.v1.CreateCoinSellerRequest
|
||||
(*CreateCoinSellerResponse)(nil), // 48: hyapp.user.v1.CreateCoinSellerResponse
|
||||
(*SetCoinSellerStatusRequest)(nil), // 49: hyapp.user.v1.SetCoinSellerStatusRequest
|
||||
(*SetCoinSellerStatusResponse)(nil), // 50: hyapp.user.v1.SetCoinSellerStatusResponse
|
||||
(*CreateAgencyRequest)(nil), // 51: hyapp.user.v1.CreateAgencyRequest
|
||||
(*CreateAgencyResponse)(nil), // 52: hyapp.user.v1.CreateAgencyResponse
|
||||
(*CloseAgencyRequest)(nil), // 53: hyapp.user.v1.CloseAgencyRequest
|
||||
(*CloseAgencyResponse)(nil), // 54: hyapp.user.v1.CloseAgencyResponse
|
||||
(*SetAgencyJoinEnabledRequest)(nil), // 55: hyapp.user.v1.SetAgencyJoinEnabledRequest
|
||||
(*SetAgencyJoinEnabledResponse)(nil), // 56: hyapp.user.v1.SetAgencyJoinEnabledResponse
|
||||
(*RequestMeta)(nil), // 57: hyapp.user.v1.RequestMeta
|
||||
}
|
||||
var file_proto_user_v1_host_proto_depIdxs = []int32{
|
||||
55, // 0: hyapp.user.v1.SearchAgenciesRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 0: hyapp.user.v1.SearchAgenciesRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
1, // 1: hyapp.user.v1.SearchAgenciesResponse.agencies:type_name -> hyapp.user.v1.Agency
|
||||
55, // 2: hyapp.user.v1.ApplyToAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 2: hyapp.user.v1.ApplyToAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
7, // 3: hyapp.user.v1.ApplyToAgencyResponse.application:type_name -> hyapp.user.v1.AgencyApplication
|
||||
55, // 4: hyapp.user.v1.ReviewAgencyApplicationRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 4: hyapp.user.v1.ReviewAgencyApplicationRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
7, // 5: hyapp.user.v1.ReviewAgencyApplicationResponse.application:type_name -> hyapp.user.v1.AgencyApplication
|
||||
0, // 6: hyapp.user.v1.ReviewAgencyApplicationResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
|
||||
6, // 7: hyapp.user.v1.ReviewAgencyApplicationResponse.membership:type_name -> hyapp.user.v1.AgencyMembership
|
||||
55, // 8: hyapp.user.v1.KickAgencyHostRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 8: hyapp.user.v1.KickAgencyHostRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
6, // 9: hyapp.user.v1.KickAgencyHostResponse.membership:type_name -> hyapp.user.v1.AgencyMembership
|
||||
0, // 10: hyapp.user.v1.KickAgencyHostResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
|
||||
55, // 11: hyapp.user.v1.InviteAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 11: hyapp.user.v1.InviteAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
8, // 12: hyapp.user.v1.InviteAgencyResponse.invitation:type_name -> hyapp.user.v1.RoleInvitation
|
||||
55, // 13: hyapp.user.v1.InviteBDRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 13: hyapp.user.v1.InviteBDRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
8, // 14: hyapp.user.v1.InviteBDResponse.invitation:type_name -> hyapp.user.v1.RoleInvitation
|
||||
55, // 15: hyapp.user.v1.ProcessRoleInvitationRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 15: hyapp.user.v1.ProcessRoleInvitationRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
8, // 16: hyapp.user.v1.ProcessRoleInvitationResponse.invitation:type_name -> hyapp.user.v1.RoleInvitation
|
||||
0, // 17: hyapp.user.v1.ProcessRoleInvitationResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
|
||||
1, // 18: hyapp.user.v1.ProcessRoleInvitationResponse.agency:type_name -> hyapp.user.v1.Agency
|
||||
6, // 19: hyapp.user.v1.ProcessRoleInvitationResponse.membership:type_name -> hyapp.user.v1.AgencyMembership
|
||||
2, // 20: hyapp.user.v1.ProcessRoleInvitationResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
|
||||
55, // 21: hyapp.user.v1.GetHostProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 21: hyapp.user.v1.GetHostProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
0, // 22: hyapp.user.v1.GetHostProfileResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
|
||||
55, // 23: hyapp.user.v1.GetBDProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 23: hyapp.user.v1.GetBDProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
2, // 24: hyapp.user.v1.GetBDProfileResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
|
||||
55, // 25: hyapp.user.v1.GetCoinSellerProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 25: hyapp.user.v1.GetCoinSellerProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
3, // 26: hyapp.user.v1.GetCoinSellerProfileResponse.coin_seller_profile:type_name -> hyapp.user.v1.CoinSellerProfile
|
||||
55, // 27: hyapp.user.v1.ListActiveCoinSellersInMyRegionRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 27: hyapp.user.v1.ListActiveCoinSellersInMyRegionRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
4, // 28: hyapp.user.v1.ListActiveCoinSellersInMyRegionResponse.coin_sellers:type_name -> hyapp.user.v1.CoinSellerListItem
|
||||
55, // 29: hyapp.user.v1.GetUserRoleSummaryRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 29: hyapp.user.v1.GetUserRoleSummaryRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
5, // 30: hyapp.user.v1.GetUserRoleSummaryResponse.summary:type_name -> hyapp.user.v1.UserRoleSummary
|
||||
55, // 31: hyapp.user.v1.CheckBusinessCapabilityRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
55, // 32: hyapp.user.v1.GetAgencyMembersRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
6, // 33: hyapp.user.v1.GetAgencyMembersResponse.memberships:type_name -> hyapp.user.v1.AgencyMembership
|
||||
55, // 34: hyapp.user.v1.GetAgencyApplicationsRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
7, // 35: hyapp.user.v1.GetAgencyApplicationsResponse.applications:type_name -> hyapp.user.v1.AgencyApplication
|
||||
55, // 36: hyapp.user.v1.CreateBDLeaderRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
2, // 37: hyapp.user.v1.CreateBDLeaderResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
|
||||
55, // 38: hyapp.user.v1.CreateBDRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
2, // 39: hyapp.user.v1.CreateBDResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
|
||||
55, // 40: hyapp.user.v1.SetBDStatusRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
2, // 41: hyapp.user.v1.SetBDStatusResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
|
||||
55, // 42: hyapp.user.v1.CreateCoinSellerRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
3, // 43: hyapp.user.v1.CreateCoinSellerResponse.coin_seller_profile:type_name -> hyapp.user.v1.CoinSellerProfile
|
||||
55, // 44: hyapp.user.v1.SetCoinSellerStatusRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
3, // 45: hyapp.user.v1.SetCoinSellerStatusResponse.coin_seller_profile:type_name -> hyapp.user.v1.CoinSellerProfile
|
||||
55, // 46: hyapp.user.v1.CreateAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
1, // 47: hyapp.user.v1.CreateAgencyResponse.agency:type_name -> hyapp.user.v1.Agency
|
||||
0, // 48: hyapp.user.v1.CreateAgencyResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
|
||||
6, // 49: hyapp.user.v1.CreateAgencyResponse.membership:type_name -> hyapp.user.v1.AgencyMembership
|
||||
55, // 50: hyapp.user.v1.CloseAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
1, // 51: hyapp.user.v1.CloseAgencyResponse.agency:type_name -> hyapp.user.v1.Agency
|
||||
55, // 52: hyapp.user.v1.SetAgencyJoinEnabledRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
1, // 53: hyapp.user.v1.SetAgencyJoinEnabledResponse.agency:type_name -> hyapp.user.v1.Agency
|
||||
9, // 54: hyapp.user.v1.UserHostService.SearchAgencies:input_type -> hyapp.user.v1.SearchAgenciesRequest
|
||||
11, // 55: hyapp.user.v1.UserHostService.ApplyToAgency:input_type -> hyapp.user.v1.ApplyToAgencyRequest
|
||||
13, // 56: hyapp.user.v1.UserHostService.ReviewAgencyApplication:input_type -> hyapp.user.v1.ReviewAgencyApplicationRequest
|
||||
15, // 57: hyapp.user.v1.UserHostService.KickAgencyHost:input_type -> hyapp.user.v1.KickAgencyHostRequest
|
||||
17, // 58: hyapp.user.v1.UserHostService.InviteAgency:input_type -> hyapp.user.v1.InviteAgencyRequest
|
||||
19, // 59: hyapp.user.v1.UserHostService.InviteBD:input_type -> hyapp.user.v1.InviteBDRequest
|
||||
21, // 60: hyapp.user.v1.UserHostService.ProcessRoleInvitation:input_type -> hyapp.user.v1.ProcessRoleInvitationRequest
|
||||
23, // 61: hyapp.user.v1.UserHostService.GetHostProfile:input_type -> hyapp.user.v1.GetHostProfileRequest
|
||||
25, // 62: hyapp.user.v1.UserHostService.GetBDProfile:input_type -> hyapp.user.v1.GetBDProfileRequest
|
||||
27, // 63: hyapp.user.v1.UserHostService.GetCoinSellerProfile:input_type -> hyapp.user.v1.GetCoinSellerProfileRequest
|
||||
29, // 64: hyapp.user.v1.UserHostService.ListActiveCoinSellersInMyRegion:input_type -> hyapp.user.v1.ListActiveCoinSellersInMyRegionRequest
|
||||
31, // 65: hyapp.user.v1.UserHostService.GetUserRoleSummary:input_type -> hyapp.user.v1.GetUserRoleSummaryRequest
|
||||
33, // 66: hyapp.user.v1.UserHostService.CheckBusinessCapability:input_type -> hyapp.user.v1.CheckBusinessCapabilityRequest
|
||||
35, // 67: hyapp.user.v1.UserHostService.GetAgencyMembers:input_type -> hyapp.user.v1.GetAgencyMembersRequest
|
||||
37, // 68: hyapp.user.v1.UserHostService.GetAgencyApplications:input_type -> hyapp.user.v1.GetAgencyApplicationsRequest
|
||||
39, // 69: hyapp.user.v1.UserHostAdminService.CreateBDLeader:input_type -> hyapp.user.v1.CreateBDLeaderRequest
|
||||
41, // 70: hyapp.user.v1.UserHostAdminService.CreateBD:input_type -> hyapp.user.v1.CreateBDRequest
|
||||
43, // 71: hyapp.user.v1.UserHostAdminService.SetBDStatus:input_type -> hyapp.user.v1.SetBDStatusRequest
|
||||
45, // 72: hyapp.user.v1.UserHostAdminService.CreateCoinSeller:input_type -> hyapp.user.v1.CreateCoinSellerRequest
|
||||
47, // 73: hyapp.user.v1.UserHostAdminService.SetCoinSellerStatus:input_type -> hyapp.user.v1.SetCoinSellerStatusRequest
|
||||
49, // 74: hyapp.user.v1.UserHostAdminService.CreateAgency:input_type -> hyapp.user.v1.CreateAgencyRequest
|
||||
51, // 75: hyapp.user.v1.UserHostAdminService.CloseAgency:input_type -> hyapp.user.v1.CloseAgencyRequest
|
||||
53, // 76: hyapp.user.v1.UserHostAdminService.SetAgencyJoinEnabled:input_type -> hyapp.user.v1.SetAgencyJoinEnabledRequest
|
||||
10, // 77: hyapp.user.v1.UserHostService.SearchAgencies:output_type -> hyapp.user.v1.SearchAgenciesResponse
|
||||
12, // 78: hyapp.user.v1.UserHostService.ApplyToAgency:output_type -> hyapp.user.v1.ApplyToAgencyResponse
|
||||
14, // 79: hyapp.user.v1.UserHostService.ReviewAgencyApplication:output_type -> hyapp.user.v1.ReviewAgencyApplicationResponse
|
||||
16, // 80: hyapp.user.v1.UserHostService.KickAgencyHost:output_type -> hyapp.user.v1.KickAgencyHostResponse
|
||||
18, // 81: hyapp.user.v1.UserHostService.InviteAgency:output_type -> hyapp.user.v1.InviteAgencyResponse
|
||||
20, // 82: hyapp.user.v1.UserHostService.InviteBD:output_type -> hyapp.user.v1.InviteBDResponse
|
||||
22, // 83: hyapp.user.v1.UserHostService.ProcessRoleInvitation:output_type -> hyapp.user.v1.ProcessRoleInvitationResponse
|
||||
24, // 84: hyapp.user.v1.UserHostService.GetHostProfile:output_type -> hyapp.user.v1.GetHostProfileResponse
|
||||
26, // 85: hyapp.user.v1.UserHostService.GetBDProfile:output_type -> hyapp.user.v1.GetBDProfileResponse
|
||||
28, // 86: hyapp.user.v1.UserHostService.GetCoinSellerProfile:output_type -> hyapp.user.v1.GetCoinSellerProfileResponse
|
||||
30, // 87: hyapp.user.v1.UserHostService.ListActiveCoinSellersInMyRegion:output_type -> hyapp.user.v1.ListActiveCoinSellersInMyRegionResponse
|
||||
32, // 88: hyapp.user.v1.UserHostService.GetUserRoleSummary:output_type -> hyapp.user.v1.GetUserRoleSummaryResponse
|
||||
34, // 89: hyapp.user.v1.UserHostService.CheckBusinessCapability:output_type -> hyapp.user.v1.CheckBusinessCapabilityResponse
|
||||
36, // 90: hyapp.user.v1.UserHostService.GetAgencyMembers:output_type -> hyapp.user.v1.GetAgencyMembersResponse
|
||||
38, // 91: hyapp.user.v1.UserHostService.GetAgencyApplications:output_type -> hyapp.user.v1.GetAgencyApplicationsResponse
|
||||
40, // 92: hyapp.user.v1.UserHostAdminService.CreateBDLeader:output_type -> hyapp.user.v1.CreateBDLeaderResponse
|
||||
42, // 93: hyapp.user.v1.UserHostAdminService.CreateBD:output_type -> hyapp.user.v1.CreateBDResponse
|
||||
44, // 94: hyapp.user.v1.UserHostAdminService.SetBDStatus:output_type -> hyapp.user.v1.SetBDStatusResponse
|
||||
46, // 95: hyapp.user.v1.UserHostAdminService.CreateCoinSeller:output_type -> hyapp.user.v1.CreateCoinSellerResponse
|
||||
48, // 96: hyapp.user.v1.UserHostAdminService.SetCoinSellerStatus:output_type -> hyapp.user.v1.SetCoinSellerStatusResponse
|
||||
50, // 97: hyapp.user.v1.UserHostAdminService.CreateAgency:output_type -> hyapp.user.v1.CreateAgencyResponse
|
||||
52, // 98: hyapp.user.v1.UserHostAdminService.CloseAgency:output_type -> hyapp.user.v1.CloseAgencyResponse
|
||||
54, // 99: hyapp.user.v1.UserHostAdminService.SetAgencyJoinEnabled:output_type -> hyapp.user.v1.SetAgencyJoinEnabledResponse
|
||||
77, // [77:100] is the sub-list for method output_type
|
||||
54, // [54:77] is the sub-list for method input_type
|
||||
54, // [54:54] is the sub-list for extension type_name
|
||||
54, // [54:54] is the sub-list for extension extendee
|
||||
0, // [0:54] is the sub-list for field type_name
|
||||
57, // 31: hyapp.user.v1.GetAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
1, // 32: hyapp.user.v1.GetAgencyResponse.agency:type_name -> hyapp.user.v1.Agency
|
||||
57, // 33: hyapp.user.v1.CheckBusinessCapabilityRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
57, // 34: hyapp.user.v1.GetAgencyMembersRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
6, // 35: hyapp.user.v1.GetAgencyMembersResponse.memberships:type_name -> hyapp.user.v1.AgencyMembership
|
||||
57, // 36: hyapp.user.v1.GetAgencyApplicationsRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
7, // 37: hyapp.user.v1.GetAgencyApplicationsResponse.applications:type_name -> hyapp.user.v1.AgencyApplication
|
||||
57, // 38: hyapp.user.v1.CreateBDLeaderRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
2, // 39: hyapp.user.v1.CreateBDLeaderResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
|
||||
57, // 40: hyapp.user.v1.CreateBDRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
2, // 41: hyapp.user.v1.CreateBDResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
|
||||
57, // 42: hyapp.user.v1.SetBDStatusRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
2, // 43: hyapp.user.v1.SetBDStatusResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
|
||||
57, // 44: hyapp.user.v1.CreateCoinSellerRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
3, // 45: hyapp.user.v1.CreateCoinSellerResponse.coin_seller_profile:type_name -> hyapp.user.v1.CoinSellerProfile
|
||||
57, // 46: hyapp.user.v1.SetCoinSellerStatusRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
3, // 47: hyapp.user.v1.SetCoinSellerStatusResponse.coin_seller_profile:type_name -> hyapp.user.v1.CoinSellerProfile
|
||||
57, // 48: hyapp.user.v1.CreateAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
1, // 49: hyapp.user.v1.CreateAgencyResponse.agency:type_name -> hyapp.user.v1.Agency
|
||||
0, // 50: hyapp.user.v1.CreateAgencyResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
|
||||
6, // 51: hyapp.user.v1.CreateAgencyResponse.membership:type_name -> hyapp.user.v1.AgencyMembership
|
||||
57, // 52: hyapp.user.v1.CloseAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
1, // 53: hyapp.user.v1.CloseAgencyResponse.agency:type_name -> hyapp.user.v1.Agency
|
||||
57, // 54: hyapp.user.v1.SetAgencyJoinEnabledRequest.meta:type_name -> hyapp.user.v1.RequestMeta
|
||||
1, // 55: hyapp.user.v1.SetAgencyJoinEnabledResponse.agency:type_name -> hyapp.user.v1.Agency
|
||||
9, // 56: hyapp.user.v1.UserHostService.SearchAgencies:input_type -> hyapp.user.v1.SearchAgenciesRequest
|
||||
11, // 57: hyapp.user.v1.UserHostService.ApplyToAgency:input_type -> hyapp.user.v1.ApplyToAgencyRequest
|
||||
13, // 58: hyapp.user.v1.UserHostService.ReviewAgencyApplication:input_type -> hyapp.user.v1.ReviewAgencyApplicationRequest
|
||||
15, // 59: hyapp.user.v1.UserHostService.KickAgencyHost:input_type -> hyapp.user.v1.KickAgencyHostRequest
|
||||
17, // 60: hyapp.user.v1.UserHostService.InviteAgency:input_type -> hyapp.user.v1.InviteAgencyRequest
|
||||
19, // 61: hyapp.user.v1.UserHostService.InviteBD:input_type -> hyapp.user.v1.InviteBDRequest
|
||||
21, // 62: hyapp.user.v1.UserHostService.ProcessRoleInvitation:input_type -> hyapp.user.v1.ProcessRoleInvitationRequest
|
||||
23, // 63: hyapp.user.v1.UserHostService.GetHostProfile:input_type -> hyapp.user.v1.GetHostProfileRequest
|
||||
25, // 64: hyapp.user.v1.UserHostService.GetBDProfile:input_type -> hyapp.user.v1.GetBDProfileRequest
|
||||
27, // 65: hyapp.user.v1.UserHostService.GetCoinSellerProfile:input_type -> hyapp.user.v1.GetCoinSellerProfileRequest
|
||||
29, // 66: hyapp.user.v1.UserHostService.ListActiveCoinSellersInMyRegion:input_type -> hyapp.user.v1.ListActiveCoinSellersInMyRegionRequest
|
||||
31, // 67: hyapp.user.v1.UserHostService.GetUserRoleSummary:input_type -> hyapp.user.v1.GetUserRoleSummaryRequest
|
||||
33, // 68: hyapp.user.v1.UserHostService.GetAgency:input_type -> hyapp.user.v1.GetAgencyRequest
|
||||
35, // 69: hyapp.user.v1.UserHostService.CheckBusinessCapability:input_type -> hyapp.user.v1.CheckBusinessCapabilityRequest
|
||||
37, // 70: hyapp.user.v1.UserHostService.GetAgencyMembers:input_type -> hyapp.user.v1.GetAgencyMembersRequest
|
||||
39, // 71: hyapp.user.v1.UserHostService.GetAgencyApplications:input_type -> hyapp.user.v1.GetAgencyApplicationsRequest
|
||||
41, // 72: hyapp.user.v1.UserHostAdminService.CreateBDLeader:input_type -> hyapp.user.v1.CreateBDLeaderRequest
|
||||
43, // 73: hyapp.user.v1.UserHostAdminService.CreateBD:input_type -> hyapp.user.v1.CreateBDRequest
|
||||
45, // 74: hyapp.user.v1.UserHostAdminService.SetBDStatus:input_type -> hyapp.user.v1.SetBDStatusRequest
|
||||
47, // 75: hyapp.user.v1.UserHostAdminService.CreateCoinSeller:input_type -> hyapp.user.v1.CreateCoinSellerRequest
|
||||
49, // 76: hyapp.user.v1.UserHostAdminService.SetCoinSellerStatus:input_type -> hyapp.user.v1.SetCoinSellerStatusRequest
|
||||
51, // 77: hyapp.user.v1.UserHostAdminService.CreateAgency:input_type -> hyapp.user.v1.CreateAgencyRequest
|
||||
53, // 78: hyapp.user.v1.UserHostAdminService.CloseAgency:input_type -> hyapp.user.v1.CloseAgencyRequest
|
||||
55, // 79: hyapp.user.v1.UserHostAdminService.SetAgencyJoinEnabled:input_type -> hyapp.user.v1.SetAgencyJoinEnabledRequest
|
||||
10, // 80: hyapp.user.v1.UserHostService.SearchAgencies:output_type -> hyapp.user.v1.SearchAgenciesResponse
|
||||
12, // 81: hyapp.user.v1.UserHostService.ApplyToAgency:output_type -> hyapp.user.v1.ApplyToAgencyResponse
|
||||
14, // 82: hyapp.user.v1.UserHostService.ReviewAgencyApplication:output_type -> hyapp.user.v1.ReviewAgencyApplicationResponse
|
||||
16, // 83: hyapp.user.v1.UserHostService.KickAgencyHost:output_type -> hyapp.user.v1.KickAgencyHostResponse
|
||||
18, // 84: hyapp.user.v1.UserHostService.InviteAgency:output_type -> hyapp.user.v1.InviteAgencyResponse
|
||||
20, // 85: hyapp.user.v1.UserHostService.InviteBD:output_type -> hyapp.user.v1.InviteBDResponse
|
||||
22, // 86: hyapp.user.v1.UserHostService.ProcessRoleInvitation:output_type -> hyapp.user.v1.ProcessRoleInvitationResponse
|
||||
24, // 87: hyapp.user.v1.UserHostService.GetHostProfile:output_type -> hyapp.user.v1.GetHostProfileResponse
|
||||
26, // 88: hyapp.user.v1.UserHostService.GetBDProfile:output_type -> hyapp.user.v1.GetBDProfileResponse
|
||||
28, // 89: hyapp.user.v1.UserHostService.GetCoinSellerProfile:output_type -> hyapp.user.v1.GetCoinSellerProfileResponse
|
||||
30, // 90: hyapp.user.v1.UserHostService.ListActiveCoinSellersInMyRegion:output_type -> hyapp.user.v1.ListActiveCoinSellersInMyRegionResponse
|
||||
32, // 91: hyapp.user.v1.UserHostService.GetUserRoleSummary:output_type -> hyapp.user.v1.GetUserRoleSummaryResponse
|
||||
34, // 92: hyapp.user.v1.UserHostService.GetAgency:output_type -> hyapp.user.v1.GetAgencyResponse
|
||||
36, // 93: hyapp.user.v1.UserHostService.CheckBusinessCapability:output_type -> hyapp.user.v1.CheckBusinessCapabilityResponse
|
||||
38, // 94: hyapp.user.v1.UserHostService.GetAgencyMembers:output_type -> hyapp.user.v1.GetAgencyMembersResponse
|
||||
40, // 95: hyapp.user.v1.UserHostService.GetAgencyApplications:output_type -> hyapp.user.v1.GetAgencyApplicationsResponse
|
||||
42, // 96: hyapp.user.v1.UserHostAdminService.CreateBDLeader:output_type -> hyapp.user.v1.CreateBDLeaderResponse
|
||||
44, // 97: hyapp.user.v1.UserHostAdminService.CreateBD:output_type -> hyapp.user.v1.CreateBDResponse
|
||||
46, // 98: hyapp.user.v1.UserHostAdminService.SetBDStatus:output_type -> hyapp.user.v1.SetBDStatusResponse
|
||||
48, // 99: hyapp.user.v1.UserHostAdminService.CreateCoinSeller:output_type -> hyapp.user.v1.CreateCoinSellerResponse
|
||||
50, // 100: hyapp.user.v1.UserHostAdminService.SetCoinSellerStatus:output_type -> hyapp.user.v1.SetCoinSellerStatusResponse
|
||||
52, // 101: hyapp.user.v1.UserHostAdminService.CreateAgency:output_type -> hyapp.user.v1.CreateAgencyResponse
|
||||
54, // 102: hyapp.user.v1.UserHostAdminService.CloseAgency:output_type -> hyapp.user.v1.CloseAgencyResponse
|
||||
56, // 103: hyapp.user.v1.UserHostAdminService.SetAgencyJoinEnabled:output_type -> hyapp.user.v1.SetAgencyJoinEnabledResponse
|
||||
80, // [80:104] is the sub-list for method output_type
|
||||
56, // [56:80] is the sub-list for method input_type
|
||||
56, // [56:56] is the sub-list for extension type_name
|
||||
56, // [56:56] is the sub-list for extension extendee
|
||||
0, // [0:56] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proto_user_v1_host_proto_init() }
|
||||
@ -4496,7 +4604,7 @@ func file_proto_user_v1_host_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_user_v1_host_proto_rawDesc), len(file_proto_user_v1_host_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 55,
|
||||
NumMessages: 57,
|
||||
NumExtensions: 0,
|
||||
NumServices: 2,
|
||||
},
|
||||
|
||||
@ -283,6 +283,15 @@ message GetUserRoleSummaryResponse {
|
||||
UserRoleSummary summary = 1;
|
||||
}
|
||||
|
||||
message GetAgencyRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 agency_id = 2;
|
||||
}
|
||||
|
||||
message GetAgencyResponse {
|
||||
Agency agency = 1;
|
||||
}
|
||||
|
||||
message CheckBusinessCapabilityRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 actor_user_id = 2;
|
||||
@ -435,6 +444,7 @@ service UserHostService {
|
||||
rpc GetCoinSellerProfile(GetCoinSellerProfileRequest) returns (GetCoinSellerProfileResponse);
|
||||
rpc ListActiveCoinSellersInMyRegion(ListActiveCoinSellersInMyRegionRequest) returns (ListActiveCoinSellersInMyRegionResponse);
|
||||
rpc GetUserRoleSummary(GetUserRoleSummaryRequest) returns (GetUserRoleSummaryResponse);
|
||||
rpc GetAgency(GetAgencyRequest) returns (GetAgencyResponse);
|
||||
rpc CheckBusinessCapability(CheckBusinessCapabilityRequest) returns (CheckBusinessCapabilityResponse);
|
||||
rpc GetAgencyMembers(GetAgencyMembersRequest) returns (GetAgencyMembersResponse);
|
||||
rpc GetAgencyApplications(GetAgencyApplicationsRequest) returns (GetAgencyApplicationsResponse);
|
||||
|
||||
@ -31,6 +31,7 @@ const (
|
||||
UserHostService_GetCoinSellerProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetCoinSellerProfile"
|
||||
UserHostService_ListActiveCoinSellersInMyRegion_FullMethodName = "/hyapp.user.v1.UserHostService/ListActiveCoinSellersInMyRegion"
|
||||
UserHostService_GetUserRoleSummary_FullMethodName = "/hyapp.user.v1.UserHostService/GetUserRoleSummary"
|
||||
UserHostService_GetAgency_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgency"
|
||||
UserHostService_CheckBusinessCapability_FullMethodName = "/hyapp.user.v1.UserHostService/CheckBusinessCapability"
|
||||
UserHostService_GetAgencyMembers_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyMembers"
|
||||
UserHostService_GetAgencyApplications_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyApplications"
|
||||
@ -54,6 +55,7 @@ type UserHostServiceClient interface {
|
||||
GetCoinSellerProfile(ctx context.Context, in *GetCoinSellerProfileRequest, opts ...grpc.CallOption) (*GetCoinSellerProfileResponse, error)
|
||||
ListActiveCoinSellersInMyRegion(ctx context.Context, in *ListActiveCoinSellersInMyRegionRequest, opts ...grpc.CallOption) (*ListActiveCoinSellersInMyRegionResponse, error)
|
||||
GetUserRoleSummary(ctx context.Context, in *GetUserRoleSummaryRequest, opts ...grpc.CallOption) (*GetUserRoleSummaryResponse, error)
|
||||
GetAgency(ctx context.Context, in *GetAgencyRequest, opts ...grpc.CallOption) (*GetAgencyResponse, error)
|
||||
CheckBusinessCapability(ctx context.Context, in *CheckBusinessCapabilityRequest, opts ...grpc.CallOption) (*CheckBusinessCapabilityResponse, error)
|
||||
GetAgencyMembers(ctx context.Context, in *GetAgencyMembersRequest, opts ...grpc.CallOption) (*GetAgencyMembersResponse, error)
|
||||
GetAgencyApplications(ctx context.Context, in *GetAgencyApplicationsRequest, opts ...grpc.CallOption) (*GetAgencyApplicationsResponse, error)
|
||||
@ -187,6 +189,16 @@ func (c *userHostServiceClient) GetUserRoleSummary(ctx context.Context, in *GetU
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) GetAgency(ctx context.Context, in *GetAgencyRequest, opts ...grpc.CallOption) (*GetAgencyResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetAgencyResponse)
|
||||
err := c.cc.Invoke(ctx, UserHostService_GetAgency_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) CheckBusinessCapability(ctx context.Context, in *CheckBusinessCapabilityRequest, opts ...grpc.CallOption) (*CheckBusinessCapabilityResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CheckBusinessCapabilityResponse)
|
||||
@ -235,6 +247,7 @@ type UserHostServiceServer interface {
|
||||
GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error)
|
||||
ListActiveCoinSellersInMyRegion(context.Context, *ListActiveCoinSellersInMyRegionRequest) (*ListActiveCoinSellersInMyRegionResponse, error)
|
||||
GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error)
|
||||
GetAgency(context.Context, *GetAgencyRequest) (*GetAgencyResponse, error)
|
||||
CheckBusinessCapability(context.Context, *CheckBusinessCapabilityRequest) (*CheckBusinessCapabilityResponse, error)
|
||||
GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error)
|
||||
GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error)
|
||||
@ -284,6 +297,9 @@ func (UnimplementedUserHostServiceServer) ListActiveCoinSellersInMyRegion(contex
|
||||
func (UnimplementedUserHostServiceServer) GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUserRoleSummary not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) GetAgency(context.Context, *GetAgencyRequest) (*GetAgencyResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetAgency not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) CheckBusinessCapability(context.Context, *CheckBusinessCapabilityRequest) (*CheckBusinessCapabilityResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CheckBusinessCapability not implemented")
|
||||
}
|
||||
@ -530,6 +546,24 @@ func _UserHostService_GetUserRoleSummary_Handler(srv interface{}, ctx context.Co
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_GetAgency_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetAgencyRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserHostServiceServer).GetAgency(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserHostService_GetAgency_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserHostServiceServer).GetAgency(ctx, req.(*GetAgencyRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_CheckBusinessCapability_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CheckBusinessCapabilityRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -639,6 +673,10 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetUserRoleSummary",
|
||||
Handler: _UserHostService_GetUserRoleSummary_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetAgency",
|
||||
Handler: _UserHostService_GetAgency_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CheckBusinessCapability",
|
||||
Handler: _UserHostService_CheckBusinessCapability_Handler,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -46,6 +46,43 @@ message DebitGiftResponse {
|
||||
string host_period_cycle_key = 12;
|
||||
}
|
||||
|
||||
// DebitGiftTarget 是一笔批量送礼中的单个接收方账务快照。
|
||||
message DebitGiftTarget {
|
||||
// command_id 是该目标的账务幂等键,由 room-service 从房间 command_id 派生。
|
||||
string command_id = 1;
|
||||
int64 target_user_id = 2;
|
||||
bool target_is_host = 3;
|
||||
int64 target_host_region_id = 4;
|
||||
int64 target_agency_owner_user_id = 5;
|
||||
}
|
||||
|
||||
// BatchDebitGiftRequest 在一个钱包事务内完成同一 sender 对多个 target 的送礼扣费。
|
||||
message BatchDebitGiftRequest {
|
||||
string command_id = 1;
|
||||
string room_id = 2;
|
||||
int64 sender_user_id = 3;
|
||||
string gift_id = 4;
|
||||
int32 gift_count = 5;
|
||||
string price_version = 6;
|
||||
string app_code = 7;
|
||||
int64 region_id = 8;
|
||||
int64 sender_region_id = 9;
|
||||
repeated DebitGiftTarget targets = 10;
|
||||
}
|
||||
|
||||
// BatchDebitGiftReceipt 保留每个目标独立交易的回执,房间事件按它逐目标落事实。
|
||||
message BatchDebitGiftReceipt {
|
||||
int64 target_user_id = 1;
|
||||
string command_id = 2;
|
||||
DebitGiftResponse billing = 3;
|
||||
}
|
||||
|
||||
// BatchDebitGiftResponse 返回批量聚合值和逐目标账务回执。
|
||||
message BatchDebitGiftResponse {
|
||||
DebitGiftResponse aggregate = 1;
|
||||
repeated BatchDebitGiftReceipt receipts = 2;
|
||||
}
|
||||
|
||||
// AssetBalance 是用户某类资产的余额投影。
|
||||
message AssetBalance {
|
||||
string asset_type = 1;
|
||||
@ -609,6 +646,35 @@ message EquipUserResourceResponse {
|
||||
UserResourceEntitlement resource = 1;
|
||||
}
|
||||
|
||||
message UnequipUserResourceRequest {
|
||||
string request_id = 1;
|
||||
string app_code = 2;
|
||||
int64 user_id = 3;
|
||||
string resource_type = 4;
|
||||
}
|
||||
|
||||
message UnequipUserResourceResponse {
|
||||
string resource_type = 1;
|
||||
bool unequipped = 2;
|
||||
int64 updated_at_ms = 3;
|
||||
}
|
||||
|
||||
message BatchGetUserEquippedResourcesRequest {
|
||||
string request_id = 1;
|
||||
string app_code = 2;
|
||||
repeated int64 user_ids = 3;
|
||||
repeated string resource_types = 4;
|
||||
}
|
||||
|
||||
message UserEquippedResources {
|
||||
int64 user_id = 1;
|
||||
repeated UserResourceEntitlement resources = 2;
|
||||
}
|
||||
|
||||
message BatchGetUserEquippedResourcesResponse {
|
||||
repeated UserEquippedResources users = 1;
|
||||
}
|
||||
|
||||
message ListResourceGrantsRequest {
|
||||
string request_id = 1;
|
||||
string app_code = 2;
|
||||
@ -1369,6 +1435,7 @@ service WalletCronService {
|
||||
// WalletService 是内部账务 gRPC 边界,外部 HTTP 入口仍由 gateway-service 承载。
|
||||
service WalletService {
|
||||
rpc DebitGift(DebitGiftRequest) returns (DebitGiftResponse);
|
||||
rpc BatchDebitGift(BatchDebitGiftRequest) returns (BatchDebitGiftResponse);
|
||||
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
|
||||
rpc AdminCreditAsset(AdminCreditAssetRequest) returns (AdminCreditAssetResponse);
|
||||
rpc AdminCreditCoinSellerStock(AdminCreditCoinSellerStockRequest) returns (AdminCreditCoinSellerStockResponse);
|
||||
@ -1393,6 +1460,8 @@ service WalletService {
|
||||
rpc GrantResourceGroup(GrantResourceGroupRequest) returns (ResourceGrantResponse);
|
||||
rpc ListUserResources(ListUserResourcesRequest) returns (ListUserResourcesResponse);
|
||||
rpc EquipUserResource(EquipUserResourceRequest) returns (EquipUserResourceResponse);
|
||||
rpc UnequipUserResource(UnequipUserResourceRequest) returns (UnequipUserResourceResponse);
|
||||
rpc BatchGetUserEquippedResources(BatchGetUserEquippedResourcesRequest) returns (BatchGetUserEquippedResourcesResponse);
|
||||
rpc ListResourceGrants(ListResourceGrantsRequest) returns (ListResourceGrantsResponse);
|
||||
rpc ListResourceShopItems(ListResourceShopItemsRequest) returns (ListResourceShopItemsResponse);
|
||||
rpc UpsertResourceShopItems(UpsertResourceShopItemsRequest) returns (UpsertResourceShopItemsResponse);
|
||||
|
||||
@ -201,65 +201,68 @@ var WalletCronService_ServiceDesc = grpc.ServiceDesc{
|
||||
}
|
||||
|
||||
const (
|
||||
WalletService_DebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitGift"
|
||||
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
|
||||
WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset"
|
||||
WalletService_AdminCreditCoinSellerStock_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditCoinSellerStock"
|
||||
WalletService_TransferCoinFromSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferCoinFromSeller"
|
||||
WalletService_ListResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResources"
|
||||
WalletService_GetResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResource"
|
||||
WalletService_CreateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResource"
|
||||
WalletService_UpdateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResource"
|
||||
WalletService_SetResourceStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceStatus"
|
||||
WalletService_ListResourceGroups_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGroups"
|
||||
WalletService_GetResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResourceGroup"
|
||||
WalletService_CreateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResourceGroup"
|
||||
WalletService_UpdateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResourceGroup"
|
||||
WalletService_SetResourceGroupStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceGroupStatus"
|
||||
WalletService_ListGiftConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftConfigs"
|
||||
WalletService_ListGiftTypeConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftTypeConfigs"
|
||||
WalletService_CreateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateGiftConfig"
|
||||
WalletService_UpdateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateGiftConfig"
|
||||
WalletService_SetGiftConfigStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetGiftConfigStatus"
|
||||
WalletService_UpsertGiftTypeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertGiftTypeConfig"
|
||||
WalletService_GrantResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResource"
|
||||
WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup"
|
||||
WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources"
|
||||
WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource"
|
||||
WalletService_ListResourceGrants_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGrants"
|
||||
WalletService_ListResourceShopItems_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceShopItems"
|
||||
WalletService_UpsertResourceShopItems_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertResourceShopItems"
|
||||
WalletService_SetResourceShopItemStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceShopItemStatus"
|
||||
WalletService_PurchaseResourceShopItem_FullMethodName = "/hyapp.wallet.v1.WalletService/PurchaseResourceShopItem"
|
||||
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_GetUserGiftWall_FullMethodName = "/hyapp.wallet.v1.WalletService/GetUserGiftWall"
|
||||
WalletService_ListRechargeProducts_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeProducts"
|
||||
WalletService_ConfirmGooglePayment_FullMethodName = "/hyapp.wallet.v1.WalletService/ConfirmGooglePayment"
|
||||
WalletService_ListAdminRechargeProducts_FullMethodName = "/hyapp.wallet.v1.WalletService/ListAdminRechargeProducts"
|
||||
WalletService_CreateRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateRechargeProduct"
|
||||
WalletService_UpdateRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateRechargeProduct"
|
||||
WalletService_DeleteRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/DeleteRechargeProduct"
|
||||
WalletService_GetDiamondExchangeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetDiamondExchangeConfig"
|
||||
WalletService_ListWalletTransactions_FullMethodName = "/hyapp.wallet.v1.WalletService/ListWalletTransactions"
|
||||
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_GrantVip_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantVip"
|
||||
WalletService_ListAdminVipLevels_FullMethodName = "/hyapp.wallet.v1.WalletService/ListAdminVipLevels"
|
||||
WalletService_UpdateAdminVipLevels_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateAdminVipLevels"
|
||||
WalletService_CreditTaskReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditTaskReward"
|
||||
WalletService_CreditLuckyGiftReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditLuckyGiftReward"
|
||||
WalletService_ApplyGameCoinChange_FullMethodName = "/hyapp.wallet.v1.WalletService/ApplyGameCoinChange"
|
||||
WalletService_GetRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacketConfig"
|
||||
WalletService_UpdateRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateRedPacketConfig"
|
||||
WalletService_CreateRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateRedPacket"
|
||||
WalletService_ClaimRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/ClaimRedPacket"
|
||||
WalletService_ListRedPackets_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRedPackets"
|
||||
WalletService_GetRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacket"
|
||||
WalletService_ExpireRedPackets_FullMethodName = "/hyapp.wallet.v1.WalletService/ExpireRedPackets"
|
||||
WalletService_RetryRedPacketRefund_FullMethodName = "/hyapp.wallet.v1.WalletService/RetryRedPacketRefund"
|
||||
WalletService_DebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitGift"
|
||||
WalletService_BatchDebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchDebitGift"
|
||||
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
|
||||
WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset"
|
||||
WalletService_AdminCreditCoinSellerStock_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditCoinSellerStock"
|
||||
WalletService_TransferCoinFromSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferCoinFromSeller"
|
||||
WalletService_ListResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResources"
|
||||
WalletService_GetResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResource"
|
||||
WalletService_CreateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResource"
|
||||
WalletService_UpdateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResource"
|
||||
WalletService_SetResourceStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceStatus"
|
||||
WalletService_ListResourceGroups_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGroups"
|
||||
WalletService_GetResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResourceGroup"
|
||||
WalletService_CreateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResourceGroup"
|
||||
WalletService_UpdateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResourceGroup"
|
||||
WalletService_SetResourceGroupStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceGroupStatus"
|
||||
WalletService_ListGiftConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftConfigs"
|
||||
WalletService_ListGiftTypeConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftTypeConfigs"
|
||||
WalletService_CreateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateGiftConfig"
|
||||
WalletService_UpdateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateGiftConfig"
|
||||
WalletService_SetGiftConfigStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetGiftConfigStatus"
|
||||
WalletService_UpsertGiftTypeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertGiftTypeConfig"
|
||||
WalletService_GrantResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResource"
|
||||
WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup"
|
||||
WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources"
|
||||
WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource"
|
||||
WalletService_UnequipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UnequipUserResource"
|
||||
WalletService_BatchGetUserEquippedResources_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchGetUserEquippedResources"
|
||||
WalletService_ListResourceGrants_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGrants"
|
||||
WalletService_ListResourceShopItems_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceShopItems"
|
||||
WalletService_UpsertResourceShopItems_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertResourceShopItems"
|
||||
WalletService_SetResourceShopItemStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceShopItemStatus"
|
||||
WalletService_PurchaseResourceShopItem_FullMethodName = "/hyapp.wallet.v1.WalletService/PurchaseResourceShopItem"
|
||||
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_GetUserGiftWall_FullMethodName = "/hyapp.wallet.v1.WalletService/GetUserGiftWall"
|
||||
WalletService_ListRechargeProducts_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeProducts"
|
||||
WalletService_ConfirmGooglePayment_FullMethodName = "/hyapp.wallet.v1.WalletService/ConfirmGooglePayment"
|
||||
WalletService_ListAdminRechargeProducts_FullMethodName = "/hyapp.wallet.v1.WalletService/ListAdminRechargeProducts"
|
||||
WalletService_CreateRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateRechargeProduct"
|
||||
WalletService_UpdateRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateRechargeProduct"
|
||||
WalletService_DeleteRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/DeleteRechargeProduct"
|
||||
WalletService_GetDiamondExchangeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetDiamondExchangeConfig"
|
||||
WalletService_ListWalletTransactions_FullMethodName = "/hyapp.wallet.v1.WalletService/ListWalletTransactions"
|
||||
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_GrantVip_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantVip"
|
||||
WalletService_ListAdminVipLevels_FullMethodName = "/hyapp.wallet.v1.WalletService/ListAdminVipLevels"
|
||||
WalletService_UpdateAdminVipLevels_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateAdminVipLevels"
|
||||
WalletService_CreditTaskReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditTaskReward"
|
||||
WalletService_CreditLuckyGiftReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditLuckyGiftReward"
|
||||
WalletService_ApplyGameCoinChange_FullMethodName = "/hyapp.wallet.v1.WalletService/ApplyGameCoinChange"
|
||||
WalletService_GetRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacketConfig"
|
||||
WalletService_UpdateRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateRedPacketConfig"
|
||||
WalletService_CreateRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateRedPacket"
|
||||
WalletService_ClaimRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/ClaimRedPacket"
|
||||
WalletService_ListRedPackets_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRedPackets"
|
||||
WalletService_GetRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacket"
|
||||
WalletService_ExpireRedPackets_FullMethodName = "/hyapp.wallet.v1.WalletService/ExpireRedPackets"
|
||||
WalletService_RetryRedPacketRefund_FullMethodName = "/hyapp.wallet.v1.WalletService/RetryRedPacketRefund"
|
||||
)
|
||||
|
||||
// WalletServiceClient is the client API for WalletService service.
|
||||
@ -269,6 +272,7 @@ const (
|
||||
// WalletService 是内部账务 gRPC 边界,外部 HTTP 入口仍由 gateway-service 承载。
|
||||
type WalletServiceClient interface {
|
||||
DebitGift(ctx context.Context, in *DebitGiftRequest, opts ...grpc.CallOption) (*DebitGiftResponse, error)
|
||||
BatchDebitGift(ctx context.Context, in *BatchDebitGiftRequest, opts ...grpc.CallOption) (*BatchDebitGiftResponse, error)
|
||||
GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error)
|
||||
AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error)
|
||||
AdminCreditCoinSellerStock(ctx context.Context, in *AdminCreditCoinSellerStockRequest, opts ...grpc.CallOption) (*AdminCreditCoinSellerStockResponse, error)
|
||||
@ -293,6 +297,8 @@ type WalletServiceClient interface {
|
||||
GrantResourceGroup(ctx context.Context, in *GrantResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
|
||||
ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error)
|
||||
EquipUserResource(ctx context.Context, in *EquipUserResourceRequest, opts ...grpc.CallOption) (*EquipUserResourceResponse, error)
|
||||
UnequipUserResource(ctx context.Context, in *UnequipUserResourceRequest, opts ...grpc.CallOption) (*UnequipUserResourceResponse, error)
|
||||
BatchGetUserEquippedResources(ctx context.Context, in *BatchGetUserEquippedResourcesRequest, opts ...grpc.CallOption) (*BatchGetUserEquippedResourcesResponse, error)
|
||||
ListResourceGrants(ctx context.Context, in *ListResourceGrantsRequest, opts ...grpc.CallOption) (*ListResourceGrantsResponse, error)
|
||||
ListResourceShopItems(ctx context.Context, in *ListResourceShopItemsRequest, opts ...grpc.CallOption) (*ListResourceShopItemsResponse, error)
|
||||
UpsertResourceShopItems(ctx context.Context, in *UpsertResourceShopItemsRequest, opts ...grpc.CallOption) (*UpsertResourceShopItemsResponse, error)
|
||||
@ -347,6 +353,16 @@ func (c *walletServiceClient) DebitGift(ctx context.Context, in *DebitGiftReques
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) BatchDebitGift(ctx context.Context, in *BatchDebitGiftRequest, opts ...grpc.CallOption) (*BatchDebitGiftResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(BatchDebitGiftResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_BatchDebitGift_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetBalancesResponse)
|
||||
@ -587,6 +603,26 @@ func (c *walletServiceClient) EquipUserResource(ctx context.Context, in *EquipUs
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) UnequipUserResource(ctx context.Context, in *UnequipUserResourceRequest, opts ...grpc.CallOption) (*UnequipUserResourceResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(UnequipUserResourceResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_UnequipUserResource_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) BatchGetUserEquippedResources(ctx context.Context, in *BatchGetUserEquippedResourcesRequest, opts ...grpc.CallOption) (*BatchGetUserEquippedResourcesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(BatchGetUserEquippedResourcesResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_BatchGetUserEquippedResources_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) ListResourceGrants(ctx context.Context, in *ListResourceGrantsRequest, opts ...grpc.CallOption) (*ListResourceGrantsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListResourceGrantsResponse)
|
||||
@ -934,6 +970,7 @@ func (c *walletServiceClient) RetryRedPacketRefund(ctx context.Context, in *Retr
|
||||
// WalletService 是内部账务 gRPC 边界,外部 HTTP 入口仍由 gateway-service 承载。
|
||||
type WalletServiceServer interface {
|
||||
DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error)
|
||||
BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error)
|
||||
GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error)
|
||||
AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error)
|
||||
AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error)
|
||||
@ -958,6 +995,8 @@ type WalletServiceServer interface {
|
||||
GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error)
|
||||
ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error)
|
||||
EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error)
|
||||
UnequipUserResource(context.Context, *UnequipUserResourceRequest) (*UnequipUserResourceResponse, error)
|
||||
BatchGetUserEquippedResources(context.Context, *BatchGetUserEquippedResourcesRequest) (*BatchGetUserEquippedResourcesResponse, error)
|
||||
ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, error)
|
||||
ListResourceShopItems(context.Context, *ListResourceShopItemsRequest) (*ListResourceShopItemsResponse, error)
|
||||
UpsertResourceShopItems(context.Context, *UpsertResourceShopItemsRequest) (*UpsertResourceShopItemsResponse, error)
|
||||
@ -1005,6 +1044,9 @@ type UnimplementedWalletServiceServer struct{}
|
||||
func (UnimplementedWalletServiceServer) DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method DebitGift not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchDebitGift not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetBalances not implemented")
|
||||
}
|
||||
@ -1077,6 +1119,12 @@ func (UnimplementedWalletServiceServer) ListUserResources(context.Context, *List
|
||||
func (UnimplementedWalletServiceServer) EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method EquipUserResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UnequipUserResource(context.Context, *UnequipUserResourceRequest) (*UnequipUserResourceResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UnequipUserResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) BatchGetUserEquippedResources(context.Context, *BatchGetUserEquippedResourcesRequest) (*BatchGetUserEquippedResourcesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchGetUserEquippedResources not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListResourceGrants not implemented")
|
||||
}
|
||||
@ -1218,6 +1266,24 @@ func _WalletService_DebitGift_Handler(srv interface{}, ctx context.Context, dec
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_BatchDebitGift_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BatchDebitGiftRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).BatchDebitGift(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_BatchDebitGift_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).BatchDebitGift(ctx, req.(*BatchDebitGiftRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_GetBalances_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetBalancesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -1650,6 +1716,42 @@ func _WalletService_EquipUserResource_Handler(srv interface{}, ctx context.Conte
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_UnequipUserResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UnequipUserResourceRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).UnequipUserResource(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_UnequipUserResource_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).UnequipUserResource(ctx, req.(*UnequipUserResourceRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_BatchGetUserEquippedResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BatchGetUserEquippedResourcesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).BatchGetUserEquippedResources(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_BatchGetUserEquippedResources_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).BatchGetUserEquippedResources(ctx, req.(*BatchGetUserEquippedResourcesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_ListResourceGrants_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListResourceGrantsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -2273,6 +2375,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "DebitGift",
|
||||
Handler: _WalletService_DebitGift_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BatchDebitGift",
|
||||
Handler: _WalletService_BatchDebitGift_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetBalances",
|
||||
Handler: _WalletService_GetBalances_Handler,
|
||||
@ -2369,6 +2475,14 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "EquipUserResource",
|
||||
Handler: _WalletService_EquipUserResource_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UnequipUserResource",
|
||||
Handler: _WalletService_UnequipUserResource_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BatchGetUserEquippedResources",
|
||||
Handler: _WalletService_BatchGetUserEquippedResources_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListResourceGrants",
|
||||
Handler: _WalletService_ListResourceGrants_Handler,
|
||||
|
||||
487
docs/用户装扮Flutter对接文档.md
Normal file
487
docs/用户装扮Flutter对接文档.md
Normal file
@ -0,0 +1,487 @@
|
||||
# 用户装扮 Flutter 对接文档
|
||||
|
||||
## 通用说明
|
||||
|
||||
所有需要登录的接口都需要带:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
统一返回外壳:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "OK",
|
||||
"request_id": "xxx",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
下文只描述 `data` 内部字段。
|
||||
|
||||
## 1. 获取我的装扮聚合
|
||||
|
||||
地址:
|
||||
|
||||
```http
|
||||
GET /api/v1/appearance
|
||||
GET /api/v1/users/me/appearance
|
||||
```
|
||||
|
||||
参数:无。
|
||||
|
||||
返回值:
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "42",
|
||||
"avatar_frame": {
|
||||
"resource_id": 8101,
|
||||
"resource_code": "frame_gold",
|
||||
"resource_type": "avatar_frame",
|
||||
"name": "Gold Frame",
|
||||
"asset_url": "https://cdn.example/frame.png",
|
||||
"preview_url": "",
|
||||
"animation_url": "https://cdn.example/frame.svga",
|
||||
"metadata_json": "{}",
|
||||
"entitlement_id": "ent-frame",
|
||||
"expires_at_ms": 1999999999999
|
||||
},
|
||||
"profile_card": {},
|
||||
"vehicle": {
|
||||
"resource_id": 8102,
|
||||
"resource_code": "vehicle_gold",
|
||||
"resource_type": "vehicle",
|
||||
"name": "Gold Vehicle",
|
||||
"asset_url": "https://cdn.example/vehicle.png",
|
||||
"preview_url": "",
|
||||
"animation_url": "https://cdn.example/vehicle.svga",
|
||||
"metadata_json": "{}",
|
||||
"entitlement_id": "ent-vehicle",
|
||||
"expires_at_ms": 1999999999999
|
||||
},
|
||||
"equipped_badges": [],
|
||||
"badges": [
|
||||
{
|
||||
"slot": "strip",
|
||||
"position": 1,
|
||||
"badge_form": "icon",
|
||||
"resource_id": 8103,
|
||||
"resource": {
|
||||
"resource_id": 8103,
|
||||
"resource_code": "badge_host",
|
||||
"resource_type": "badge",
|
||||
"name": "Host Badge",
|
||||
"asset_url": "https://cdn.example/badge.png",
|
||||
"preview_url": "",
|
||||
"animation_url": "",
|
||||
"metadata_json": "{}",
|
||||
"entitlement_id": "",
|
||||
"expires_at_ms": 0
|
||||
},
|
||||
"entitlement_id": "ent-badge",
|
||||
"source_type": "achievement",
|
||||
"source_id": "ach-host",
|
||||
"pin_mode": "auto",
|
||||
"updated_at_ms": 1778256000000
|
||||
}
|
||||
],
|
||||
"badge_profile": {
|
||||
"strip_badges": [],
|
||||
"profile_tile_badges": [],
|
||||
"honor_badges": [],
|
||||
"server_time_ms": 1778256000000
|
||||
},
|
||||
"resources": [],
|
||||
"server_time_ms": 1778256000000
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| user_id | 用户 ID,字符串 |
|
||||
| avatar_frame | 当前佩戴头像框,未佩戴时为 `{}` |
|
||||
| profile_card | 当前佩戴背景卡,未佩戴时为 `{}` |
|
||||
| vehicle | 当前佩戴入场座驾,未佩戴时为 `{}` |
|
||||
| equipped_badges | wallet 当前佩戴的 badge 权益 |
|
||||
| badges | 房间、公屏等高频展示用徽章,取 `strip_badges` |
|
||||
| badge_profile | 完整徽章展示配置 |
|
||||
| resources | 当前佩戴资源的完整列表 |
|
||||
| server_time_ms | 服务端时间戳 |
|
||||
|
||||
说明:
|
||||
|
||||
- 过期佩戴不会返回。
|
||||
- 头像框、背景卡、入场座驾来自 wallet 当前佩戴。
|
||||
- 徽章展示配置来自 activity。
|
||||
- 客户端渲染动画优先使用 `animation_url`,没有动画时用 `asset_url` 或 `preview_url` 兜底。
|
||||
|
||||
## 2. 获取我的资源背包
|
||||
|
||||
地址:
|
||||
|
||||
```http
|
||||
GET /api/v1/users/me/resources
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| resource_type | string | 否 | `avatar_frame` / `profile_card` / `vehicle` / `badge` / `chat_bubble` |
|
||||
|
||||
示例:
|
||||
|
||||
```http
|
||||
GET /api/v1/users/me/resources?resource_type=vehicle
|
||||
```
|
||||
|
||||
返回值:
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"entitlement_id": "ent-vehicle",
|
||||
"resource_id": 8102,
|
||||
"resource": {
|
||||
"resource_id": 8102,
|
||||
"resource_code": "vehicle_gold",
|
||||
"resource_type": "vehicle",
|
||||
"name": "Gold Vehicle",
|
||||
"status": "active",
|
||||
"usage_scopes": ["room_entry"],
|
||||
"asset_url": "https://cdn.example/vehicle.png",
|
||||
"preview_url": "",
|
||||
"animation_url": "https://cdn.example/vehicle.svga",
|
||||
"metadata_json": "{}"
|
||||
},
|
||||
"status": "active",
|
||||
"quantity": 1,
|
||||
"remaining_quantity": 1,
|
||||
"effective_at_ms": 1778256000000,
|
||||
"expires_at_ms": 1999999999999,
|
||||
"source_grant_id": "grant-1",
|
||||
"created_at_ms": 1778256000000,
|
||||
"updated_at_ms": 1778256000000,
|
||||
"equipped": true
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| entitlement_id | 用户权益 ID,佩戴时建议传这个值 |
|
||||
| resource_id | 资源 ID |
|
||||
| resource | 资源素材和基础信息 |
|
||||
| status | 权益状态 |
|
||||
| quantity | 总数量 |
|
||||
| remaining_quantity | 剩余数量 |
|
||||
| effective_at_ms | 生效时间 |
|
||||
| expires_at_ms | 过期时间,0 表示不过期 |
|
||||
| equipped | 是否当前佩戴 |
|
||||
|
||||
## 3. 佩戴资源
|
||||
|
||||
地址:
|
||||
|
||||
```http
|
||||
POST /api/v1/users/me/resources/{resource_id}/equip
|
||||
```
|
||||
|
||||
路径参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| resource_id | int64 | 是 | 资源 ID |
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"entitlement_id": "ent-vehicle"
|
||||
}
|
||||
```
|
||||
|
||||
Body 参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| entitlement_id | string | 否 | 用户权益 ID,建议传 |
|
||||
|
||||
返回值:
|
||||
|
||||
```json
|
||||
{
|
||||
"entitlement_id": "ent-vehicle",
|
||||
"resource_id": 8102,
|
||||
"resource": {},
|
||||
"status": "active",
|
||||
"quantity": 1,
|
||||
"remaining_quantity": 1,
|
||||
"effective_at_ms": 1778256000000,
|
||||
"expires_at_ms": 1999999999999,
|
||||
"equipped": true
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 可佩戴类型:`avatar_frame`、`profile_card`、`vehicle`、`chat_bubble`、`badge`。
|
||||
- 过期资源不能佩戴。
|
||||
- 不传 `entitlement_id` 时,服务端会选择当前有效权益。
|
||||
|
||||
## 4. 取消佩戴
|
||||
|
||||
地址:
|
||||
|
||||
```http
|
||||
DELETE /api/v1/users/me/resources/{resource_type}/unequip
|
||||
```
|
||||
|
||||
路径参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| resource_type | string | 是 | `avatar_frame` / `profile_card` / `vehicle` / `chat_bubble` / `badge` |
|
||||
|
||||
返回值:
|
||||
|
||||
```json
|
||||
{
|
||||
"resource_type": "vehicle",
|
||||
"unequipped": true,
|
||||
"updated_at_ms": 1778256000000
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 重复取消不会报错。
|
||||
- 用户本来没佩戴该类型时,`unequipped=false`。
|
||||
|
||||
## 5. 批量获取房间展示资料
|
||||
|
||||
地址:
|
||||
|
||||
```http
|
||||
GET /api/v1/users/room-display-profiles:batch
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| user_ids | string | 是 | 多个用户 ID,逗号分隔 |
|
||||
| user_id | string | 否 | 也支持重复传多个 `user_id` |
|
||||
|
||||
示例:
|
||||
|
||||
```http
|
||||
GET /api/v1/users/room-display-profiles:batch?user_ids=42,43
|
||||
```
|
||||
|
||||
返回值:
|
||||
|
||||
```json
|
||||
{
|
||||
"profiles": [
|
||||
{
|
||||
"user_id": "42",
|
||||
"username": "Tom",
|
||||
"avatar": "https://cdn.example/avatar.png",
|
||||
"display_user_id": "100001",
|
||||
"gender": "male",
|
||||
"age": 22,
|
||||
"country": "CN",
|
||||
"country_name": "China",
|
||||
"country_display_name": "China",
|
||||
"country_flag": "CN",
|
||||
"vip": {},
|
||||
"level": {},
|
||||
"badges": [],
|
||||
"avatar_frame": {
|
||||
"resource_id": 8101,
|
||||
"resource_code": "frame_gold",
|
||||
"resource_type": "avatar_frame",
|
||||
"name": "Gold Frame",
|
||||
"asset_url": "https://cdn.example/frame.png",
|
||||
"preview_url": "",
|
||||
"animation_url": "https://cdn.example/frame.svga",
|
||||
"metadata_json": "{}",
|
||||
"entitlement_id": "ent-frame",
|
||||
"expires_at_ms": 1999999999999
|
||||
},
|
||||
"profile_card": {
|
||||
"resource_id": 8104,
|
||||
"resource_code": "profile_card_gold",
|
||||
"resource_type": "profile_card",
|
||||
"name": "Gold Profile Card",
|
||||
"asset_url": "https://cdn.example/profile-card.png",
|
||||
"preview_url": "",
|
||||
"animation_url": "https://cdn.example/profile-card.mp4",
|
||||
"metadata_json": "{\"profile_card_layout\":{\"source_width\":1136,\"source_height\":1680,\"content_top\":143,\"content_height\":1525}}",
|
||||
"entitlement_id": "ent-profile-card",
|
||||
"expires_at_ms": 1999999999999
|
||||
},
|
||||
"vehicle": {
|
||||
"resource_id": 8102,
|
||||
"resource_code": "vehicle_gold",
|
||||
"resource_type": "vehicle",
|
||||
"name": "Gold Vehicle",
|
||||
"asset_url": "https://cdn.example/vehicle.png",
|
||||
"preview_url": "",
|
||||
"animation_url": "https://cdn.example/vehicle.svga",
|
||||
"metadata_json": "{}",
|
||||
"entitlement_id": "ent-vehicle",
|
||||
"expires_at_ms": 1999999999999
|
||||
},
|
||||
"charm": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 房间、公屏、在线列表展示头像框用 `avatar_frame`。
|
||||
- 房间资料卡展示用 `profile_card`,从 `metadata_json.profile_card_layout` 解析 `content_top` 和 `content_height`。
|
||||
- 入场座驾展示用 `vehicle`。
|
||||
- 徽章条展示用 `badges`。
|
||||
- 过期佩戴不会返回。
|
||||
|
||||
## 6. 进房
|
||||
|
||||
地址:
|
||||
|
||||
```http
|
||||
POST /api/v1/rooms/join
|
||||
```
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"room_id": "room_1001",
|
||||
"command_id": "cmd-join-1001",
|
||||
"role": "audience",
|
||||
"password": "1234"
|
||||
}
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| room_id | string | 是 | 房间 ID |
|
||||
| command_id | string | 是 | 幂等 ID,Flutter 每次进房动作生成一个 |
|
||||
| role | string | 否 | 默认 `audience` |
|
||||
| password | string | 否 | 锁房密码 |
|
||||
|
||||
返回值:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"applied": true,
|
||||
"room_version": 9,
|
||||
"server_time_ms": 1778256000000
|
||||
},
|
||||
"room": {},
|
||||
"viewer": {},
|
||||
"seats": [],
|
||||
"contribution_rank": [],
|
||||
"profiles": [
|
||||
{
|
||||
"user_id": "42",
|
||||
"display_user_id": "100001",
|
||||
"username": "Tom",
|
||||
"avatar": "https://cdn.example/avatar.png"
|
||||
}
|
||||
],
|
||||
"im": {
|
||||
"group_id": "room_1001",
|
||||
"need_join_group": true
|
||||
},
|
||||
"rtc": {
|
||||
"need_token": true,
|
||||
"available": true,
|
||||
"token": {}
|
||||
},
|
||||
"server_time_ms": 1778256000000
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- Flutter 不需要传入场座驾。
|
||||
- 服务端会在进房时自动读取当前佩戴的 `vehicle`,写入房间 IM。
|
||||
- `join` 返回里的 `profiles` 是基础资料。
|
||||
- 房间增强资料用 `/users/room-display-profiles:batch`,或使用房间详情、在线用户列表里内嵌的 `profile` 字段。
|
||||
|
||||
## 7. 相关 IM
|
||||
|
||||
### 用户进房
|
||||
|
||||
腾讯 IM 群自定义消息:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "evt_xxx",
|
||||
"room_id": "room_1001",
|
||||
"event_type": "room_user_joined",
|
||||
"actor_user_id": 42,
|
||||
"target_user_id": 42,
|
||||
"room_version": 9,
|
||||
"entry_vehicle": {
|
||||
"resource_id": 8102,
|
||||
"resource_code": "vehicle_gold",
|
||||
"name": "Gold Vehicle",
|
||||
"asset_url": "https://cdn.example/vehicle.png",
|
||||
"preview_url": "",
|
||||
"animation_url": "https://cdn.example/vehicle.svga",
|
||||
"metadata_json": "{}",
|
||||
"entitlement_id": "ent-vehicle",
|
||||
"expires_at_ms": 1999999999999
|
||||
},
|
||||
"attributes": {
|
||||
"role": "audience"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Flutter 处理:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| event_type | `room_user_joined` 表示用户进入房间 |
|
||||
| actor_user_id | 进房用户 ID |
|
||||
| target_user_id | 进房用户 ID |
|
||||
| room_version | 房间版本 |
|
||||
| entry_vehicle | 入场座驾快照 |
|
||||
| attributes.role | 用户进房角色,仅用于展示 |
|
||||
|
||||
处理规则:
|
||||
|
||||
- `entry_vehicle.resource_id > 0`:播放入场座驾。
|
||||
- `entry_vehicle.animation_url`:优先用于动画播放。
|
||||
- `entry_vehicle.asset_url` / `entry_vehicle.preview_url`:动画不可用时兜底展示。
|
||||
- `entry_vehicle` 不存在或为空:普通进房,不播放座驾。
|
||||
- 不要收到进房 IM 后再实时请求 `/appearance` 查询座驾,直接使用 IM 中的 `entry_vehicle` 快照。
|
||||
|
||||
## 8. Flutter 调用建议
|
||||
|
||||
1. 个人装扮页进入时调 `/api/v1/appearance`。
|
||||
2. 背包页调 `/api/v1/users/me/resources?resource_type=xxx`。
|
||||
3. 点击佩戴调 `POST /api/v1/users/me/resources/{resource_id}/equip`,成功后刷新 `/appearance`。
|
||||
4. 点击取消佩戴调 `DELETE /api/v1/users/me/resources/{resource_type}/unequip`,成功后刷新 `/appearance`。
|
||||
5. 房间、公屏、在线用户资料优先使用 `room-display-profiles:batch` 的 `avatar_frame`、`profile_card`、`vehicle`、`badges`。
|
||||
6. 收到 `room_user_joined` IM 时,只按 `entry_vehicle` 播放入场座驾。
|
||||
@ -376,6 +376,11 @@ Response `data`:
|
||||
"level": {},
|
||||
"badges": [],
|
||||
"avatar_frame": {},
|
||||
"profile_card": {
|
||||
"resource_id": 8104,
|
||||
"animation_url": "https://cdn.example/profile-card.mp4",
|
||||
"metadata_json": "{\"profile_card_layout\":{\"source_height\":1680,\"content_top\":143,\"content_height\":1525}}"
|
||||
},
|
||||
"vehicle": {},
|
||||
"charm": 10
|
||||
}
|
||||
@ -386,7 +391,7 @@ Response `data`:
|
||||
Rules:
|
||||
|
||||
- 该接口专门服务房间麦位、公屏和礼物动效展示;不要复用“我的页”或用户主页重接口。
|
||||
- 当前已接入用户基础资料,`vip/level/badges/avatar_frame/vehicle/charm` 保持稳定字段,后续接入对应 read model 后填充。
|
||||
- 当前已接入用户基础资料,`vip/level/badges/avatar_frame/profile_card/vehicle/charm` 保持稳定字段;资料卡高度从 `profile_card.metadata_json.profile_card_layout` 解析。
|
||||
|
||||
### IM UserSig
|
||||
|
||||
|
||||
@ -94,17 +94,33 @@ type AccountProfile struct {
|
||||
|
||||
// RoomEvent 是 room-service 发给腾讯 IM 的房间系统消息负载。
|
||||
type RoomEvent struct {
|
||||
EventID string `json:"event_id"`
|
||||
RoomID string `json:"room_id"`
|
||||
EventType string `json:"event_type"`
|
||||
ActorUserID int64 `json:"actor_user_id,omitempty"`
|
||||
TargetUserID int64 `json:"target_user_id,omitempty"`
|
||||
SeatNo int32 `json:"seat_no,omitempty"`
|
||||
GiftValue int64 `json:"gift_value,omitempty"`
|
||||
RoomHeat int64 `json:"room_heat,omitempty"`
|
||||
RoomVersion int64 `json:"room_version,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
EventID string `json:"event_id"`
|
||||
RoomID string `json:"room_id"`
|
||||
EventType string `json:"event_type"`
|
||||
ActorUserID int64 `json:"actor_user_id,omitempty"`
|
||||
TargetUserID int64 `json:"target_user_id,omitempty"`
|
||||
SeatNo int32 `json:"seat_no,omitempty"`
|
||||
GiftValue int64 `json:"gift_value,omitempty"`
|
||||
RoomHeat int64 `json:"room_heat,omitempty"`
|
||||
RoomVersion int64 `json:"room_version,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
// EntryVehicle 是用户进房瞬间的座驾快照;客户端收到 room_user_joined 后直接播放,不再反查 wallet。
|
||||
EntryVehicle *RoomEntryVehicleSnapshot `json:"entry_vehicle,omitempty"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
}
|
||||
|
||||
// RoomEntryVehicleSnapshot 是房间系统消息里可直接渲染的座驾素材快照。
|
||||
// 它故意不包含 wallet 内部状态,只保留客户端展示和过期兜底需要的字段。
|
||||
type RoomEntryVehicleSnapshot struct {
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
ResourceCode string `json:"resource_code,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
AssetURL string `json:"asset_url,omitempty"`
|
||||
PreviewURL string `json:"preview_url,omitempty"`
|
||||
AnimationURL string `json:"animation_url,omitempty"`
|
||||
MetadataJSON string `json:"metadata_json,omitempty"`
|
||||
EntitlementID string `json:"entitlement_id,omitempty"`
|
||||
ExpiresAtMS int64 `json:"expires_at_ms,omitempty"`
|
||||
}
|
||||
|
||||
// RESTClient 封装腾讯云 IM REST API,把 UserSig、endpoint query 和错误码处理收敛在一个边界。
|
||||
|
||||
95
scripts/mysql/backfill_level_badge_metadata.sql
Normal file
95
scripts/mysql/backfill_level_badge_metadata.sql
Normal file
@ -0,0 +1,95 @@
|
||||
-- Backfill level badge resource metadata and user level display projections.
|
||||
-- Run this on a MySQL account that can read hyapp_activity and update hyapp_wallet.
|
||||
-- If the conflict SELECT returns rows, copy the shared resource into separate resources first.
|
||||
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_level_badge_resource_refs;
|
||||
|
||||
CREATE TEMPORARY TABLE tmp_level_badge_resource_refs AS
|
||||
SELECT
|
||||
app_code,
|
||||
track,
|
||||
level,
|
||||
CAST(JSON_UNQUOTE(JSON_EXTRACT(display_config_json, '$.long_badge_resource_id')) AS UNSIGNED) AS resource_id
|
||||
FROM hyapp_activity.growth_level_rules
|
||||
WHERE status = 'active'
|
||||
AND COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(display_config_json, '$.long_badge_resource_id')) AS UNSIGNED), 0) > 0;
|
||||
|
||||
SELECT
|
||||
resource_id,
|
||||
GROUP_CONCAT(DISTINCT track ORDER BY track) AS tracks,
|
||||
COUNT(DISTINCT track) AS track_count
|
||||
FROM tmp_level_badge_resource_refs
|
||||
GROUP BY resource_id
|
||||
HAVING COUNT(DISTINCT track) > 1;
|
||||
|
||||
UPDATE hyapp_wallet.resources AS r
|
||||
JOIN (
|
||||
SELECT app_code, resource_id, MIN(track) AS track, COUNT(DISTINCT track) AS track_count
|
||||
FROM tmp_level_badge_resource_refs
|
||||
GROUP BY app_code, resource_id
|
||||
) AS refs
|
||||
ON refs.app_code = r.app_code AND refs.resource_id = r.resource_id
|
||||
SET r.metadata_json = JSON_SET(
|
||||
COALESCE(r.metadata_json, JSON_OBJECT()),
|
||||
'$.badge_form', 'strip',
|
||||
'$.badge_kind', 'level',
|
||||
'$.level_track', refs.track
|
||||
),
|
||||
r.updated_at_ms = UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000
|
||||
WHERE refs.track_count = 1
|
||||
AND r.resource_type = 'badge';
|
||||
|
||||
UPDATE hyapp_activity.user_level_display_profiles AS p
|
||||
LEFT JOIN (
|
||||
SELECT a.app_code, a.user_id, refs.level, refs.resource_id
|
||||
FROM hyapp_activity.user_growth_level_accounts AS a
|
||||
JOIN tmp_level_badge_resource_refs AS refs
|
||||
ON refs.app_code = a.app_code AND refs.track = a.track AND refs.level <= a.current_level
|
||||
LEFT JOIN tmp_level_badge_resource_refs AS higher
|
||||
ON higher.app_code = a.app_code
|
||||
AND higher.track = a.track
|
||||
AND higher.level <= a.current_level
|
||||
AND higher.level > refs.level
|
||||
WHERE a.track = 'wealth' AND higher.resource_id IS NULL
|
||||
) AS picked
|
||||
ON picked.app_code = p.app_code AND picked.user_id = p.user_id
|
||||
SET p.wealth_badge_resource_id = COALESCE(picked.resource_id, 0),
|
||||
p.wealth_badge_source_level = COALESCE(picked.level, 0),
|
||||
p.updated_at_ms = UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000;
|
||||
UPDATE hyapp_activity.user_level_display_profiles AS p
|
||||
LEFT JOIN (
|
||||
SELECT a.app_code, a.user_id, refs.level, refs.resource_id
|
||||
FROM hyapp_activity.user_growth_level_accounts AS a
|
||||
JOIN tmp_level_badge_resource_refs AS refs
|
||||
ON refs.app_code = a.app_code AND refs.track = a.track AND refs.level <= a.current_level
|
||||
LEFT JOIN tmp_level_badge_resource_refs AS higher
|
||||
ON higher.app_code = a.app_code
|
||||
AND higher.track = a.track
|
||||
AND higher.level <= a.current_level
|
||||
AND higher.level > refs.level
|
||||
WHERE a.track = 'game' AND higher.resource_id IS NULL
|
||||
) AS picked
|
||||
ON picked.app_code = p.app_code AND picked.user_id = p.user_id
|
||||
SET p.game_badge_resource_id = COALESCE(picked.resource_id, 0),
|
||||
p.game_badge_source_level = COALESCE(picked.level, 0),
|
||||
p.updated_at_ms = UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000;
|
||||
|
||||
UPDATE hyapp_activity.user_level_display_profiles AS p
|
||||
LEFT JOIN (
|
||||
SELECT a.app_code, a.user_id, refs.level, refs.resource_id
|
||||
FROM hyapp_activity.user_growth_level_accounts AS a
|
||||
JOIN tmp_level_badge_resource_refs AS refs
|
||||
ON refs.app_code = a.app_code AND refs.track = a.track AND refs.level <= a.current_level
|
||||
LEFT JOIN tmp_level_badge_resource_refs AS higher
|
||||
ON higher.app_code = a.app_code
|
||||
AND higher.track = a.track
|
||||
AND higher.level <= a.current_level
|
||||
AND higher.level > refs.level
|
||||
WHERE a.track = 'charm' AND higher.resource_id IS NULL
|
||||
) AS picked
|
||||
ON picked.app_code = p.app_code AND picked.user_id = p.user_id
|
||||
SET p.charm_badge_resource_id = COALESCE(picked.resource_id, 0),
|
||||
p.charm_badge_source_level = COALESCE(picked.level, 0),
|
||||
p.updated_at_ms = UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000;
|
||||
@ -236,7 +236,7 @@ func main() {
|
||||
HostOrg: hostorgmodule.New(userclient.NewGRPC(userConn), walletclient.NewGRPC(walletConn), userDB, walletDB, sqlDB, auditHandler),
|
||||
HostSalarySettlement: hostsalarysettlementmodule.New(walletDB, userDB),
|
||||
Job: jobmodule.New(store, cfg, auditHandler),
|
||||
LevelConfig: levelconfigmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
LevelConfig: levelconfigmodule.New(activityclient.NewGRPC(activityConn), walletclient.NewGRPC(walletConn), auditHandler),
|
||||
LuckyGift: luckygiftmodule.New(activityclient.NewGRPC(activityConn), cfg.ActivityService.RequestTimeout, auditHandler),
|
||||
Menu: menumodule.New(store, auditHandler),
|
||||
Payment: paymentmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
|
||||
|
||||
@ -10,10 +10,12 @@ import (
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
"hyapp-admin-server/internal/integration/walletclient"
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
"hyapp-admin-server/internal/modules/shared"
|
||||
"hyapp-admin-server/internal/response"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@ -27,11 +29,12 @@ const (
|
||||
|
||||
type Handler struct {
|
||||
activity activityclient.Client
|
||||
wallet walletclient.Client
|
||||
audit shared.OperationLogger
|
||||
}
|
||||
|
||||
func New(activity activityclient.Client, audit shared.OperationLogger) *Handler {
|
||||
return &Handler{activity: activity, audit: audit}
|
||||
func New(activity activityclient.Client, wallet walletclient.Client, audit shared.OperationLogger) *Handler {
|
||||
return &Handler{activity: activity, wallet: wallet, audit: audit}
|
||||
}
|
||||
|
||||
type levelConfigDTO struct {
|
||||
@ -208,7 +211,7 @@ func (h *Handler) UpdateTrack(c *gin.Context) {
|
||||
// UpsertRule 配置某条轨道某一级的升级阈值、等级奖励资源组和每级长徽章素材。
|
||||
func (h *Handler) UpsertRule(c *gin.Context) {
|
||||
track := normalizeTrack(c.Param("track"))
|
||||
level, ok := parsePositiveInt32(c.Param("level"))
|
||||
level, ok := parseNonNegativeInt32(c.Param("level"))
|
||||
if !ok || !validTrack(track) {
|
||||
response.BadRequest(c, "等级参数不正确")
|
||||
return
|
||||
@ -233,8 +236,8 @@ func (h *Handler) UpsertRule(c *gin.Context) {
|
||||
if req.RequiredValue != nil {
|
||||
requiredValue = *req.RequiredValue
|
||||
}
|
||||
if requiredValue <= 0 {
|
||||
response.BadRequest(c, "升级所需数值必须大于 0")
|
||||
if requiredValue < 0 {
|
||||
response.BadRequest(c, "升级所需数值不能小于 0")
|
||||
return
|
||||
}
|
||||
name := current.GetName()
|
||||
@ -279,6 +282,12 @@ func (h *Handler) UpsertRule(c *gin.Context) {
|
||||
response.BadRequest(c, "展示配置 JSON 不正确")
|
||||
return
|
||||
}
|
||||
if badgeID := longBadgeResourceID(displayConfig); badgeID > 0 {
|
||||
if err := h.validateLevelLongBadgeResource(c, track, badgeID); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
resp, err := h.activity.UpsertLevelRule(c.Request.Context(), &activityv1.UpsertLevelRuleRequest{
|
||||
Meta: h.meta(c),
|
||||
Track: track,
|
||||
@ -359,7 +368,7 @@ func (h *Handler) upsertTier(c *gin.Context, tierID int64) {
|
||||
if req.MaxLevel != nil {
|
||||
maxLevel = *req.MaxLevel
|
||||
}
|
||||
if minLevel <= 0 || maxLevel < minLevel {
|
||||
if minLevel < 0 || maxLevel < minLevel {
|
||||
response.BadRequest(c, "等级段范围不正确")
|
||||
return
|
||||
}
|
||||
@ -453,6 +462,38 @@ func (h *Handler) loadConfig(c *gin.Context, track string, status string) (*acti
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) validateLevelLongBadgeResource(c *gin.Context, track string, resourceID int64) error {
|
||||
if resourceID <= 0 {
|
||||
return nil
|
||||
}
|
||||
if h.wallet == nil {
|
||||
return fmt.Errorf("资源服务不可用")
|
||||
}
|
||||
resp, err := h.wallet.GetResource(c.Request.Context(), &walletv1.GetResourceRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
ResourceId: resourceID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("等级徽章资源不存在")
|
||||
}
|
||||
resource := resp.GetResource()
|
||||
if resource.GetResourceType() != "badge" {
|
||||
return fmt.Errorf("等级长徽章必须选择徽章资源")
|
||||
}
|
||||
metadata := levelBadgeMetadataFromJSON(resource.GetMetadataJson())
|
||||
if metadata.BadgeForm != "strip" {
|
||||
return fmt.Errorf("等级长徽章必须选择长徽章素材")
|
||||
}
|
||||
if metadata.BadgeKind != "level" {
|
||||
return fmt.Errorf("等级长徽章必须选择等级徽章素材")
|
||||
}
|
||||
if metadata.LevelTrack != track {
|
||||
return fmt.Errorf("等级长徽章所属等级不匹配")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) currentTrack(c *gin.Context, track string) (trackDTO, bool, error) {
|
||||
config, err := h.loadConfig(c, track, "")
|
||||
if err != nil {
|
||||
@ -669,9 +710,26 @@ func longBadgeResourceID(raw string) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func parsePositiveInt32(raw string) (int32, bool) {
|
||||
type levelBadgeMetadata struct {
|
||||
BadgeForm string `json:"badge_form"`
|
||||
BadgeKind string `json:"badge_kind"`
|
||||
LevelTrack string `json:"level_track"`
|
||||
}
|
||||
|
||||
func levelBadgeMetadataFromJSON(raw string) levelBadgeMetadata {
|
||||
payload := levelBadgeMetadata{}
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &payload); err != nil {
|
||||
return levelBadgeMetadata{}
|
||||
}
|
||||
payload.BadgeForm = strings.ToLower(strings.TrimSpace(payload.BadgeForm))
|
||||
payload.BadgeKind = strings.ToLower(strings.TrimSpace(payload.BadgeKind))
|
||||
payload.LevelTrack = normalizeTrack(payload.LevelTrack)
|
||||
return payload
|
||||
}
|
||||
|
||||
func parseNonNegativeInt32(raw string) (int32, bool) {
|
||||
value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 32)
|
||||
if err != nil || value <= 0 {
|
||||
if err != nil || value < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return int32(value), true
|
||||
|
||||
@ -18,6 +18,8 @@ type resourceDTO struct {
|
||||
CoinPrice int64 `json:"coinPrice"`
|
||||
GiftPointAmount int64 `json:"giftPointAmount"`
|
||||
BadgeForm string `json:"badgeForm,omitempty"`
|
||||
BadgeKind string `json:"badgeKind,omitempty"`
|
||||
LevelTrack string `json:"levelTrack,omitempty"`
|
||||
UsageScopes []string `json:"usageScopes"`
|
||||
AssetURL string `json:"assetUrl"`
|
||||
PreviewURL string `json:"previewUrl"`
|
||||
@ -198,6 +200,8 @@ func resourceFromProto(item *walletv1.Resource) resourceDTO {
|
||||
CoinPrice: item.GetCoinPrice(),
|
||||
GiftPointAmount: item.GetGiftPointAmount(),
|
||||
BadgeForm: badgeFormForResource(item.GetResourceType(), item.GetMetadataJson()),
|
||||
BadgeKind: badgeKindForResource(item.GetResourceType(), item.GetMetadataJson()),
|
||||
LevelTrack: badgeLevelTrackForResource(item.GetResourceType(), item.GetMetadataJson()),
|
||||
UsageScopes: item.GetUsageScopes(),
|
||||
AssetURL: item.GetAssetUrl(),
|
||||
PreviewURL: item.GetPreviewUrl(),
|
||||
|
||||
@ -433,10 +433,15 @@ func (h *Handler) GrantResource(c *gin.Context) {
|
||||
response.BadRequest(c, "资源赠送参数不正确")
|
||||
return
|
||||
}
|
||||
targetUserID, err := parseGrantTargetUserID(req.TargetUserID)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := h.wallet.GrantResource(c.Request.Context(), &walletv1.GrantResourceRequest{
|
||||
CommandId: strings.TrimSpace(req.CommandID),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
TargetUserId: req.TargetUserID,
|
||||
TargetUserId: targetUserID,
|
||||
ResourceId: req.ResourceID,
|
||||
Quantity: req.Quantity,
|
||||
DurationMs: req.DurationMS,
|
||||
@ -459,10 +464,15 @@ func (h *Handler) GrantResourceGroup(c *gin.Context) {
|
||||
response.BadRequest(c, "资源组赠送参数不正确")
|
||||
return
|
||||
}
|
||||
targetUserID, err := parseGrantTargetUserID(req.TargetUserID)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := h.wallet.GrantResourceGroup(c.Request.Context(), &walletv1.GrantResourceGroupRequest{
|
||||
CommandId: strings.TrimSpace(req.CommandID),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
TargetUserId: req.TargetUserID,
|
||||
TargetUserId: targetUserID,
|
||||
GroupId: req.GroupID,
|
||||
Reason: strings.TrimSpace(req.Reason),
|
||||
OperatorUserId: actorID(c),
|
||||
@ -477,6 +487,45 @@ func (h *Handler) GrantResourceGroup(c *gin.Context) {
|
||||
response.Created(c, grant)
|
||||
}
|
||||
|
||||
func (h *Handler) LookupResourceGrantTarget(c *gin.Context) {
|
||||
keyword := strings.TrimSpace(firstQuery(c, "user_id", "userId", "keyword"))
|
||||
if keyword == "" {
|
||||
response.BadRequest(c, "请输入用户 ID 或短号")
|
||||
return
|
||||
}
|
||||
if h == nil || h.userDB == nil {
|
||||
response.ServerError(c, "用户数据库未配置")
|
||||
return
|
||||
}
|
||||
// 资源赠送页允许输入短号,但 wallet 只认内部 user_id;这里在 admin 侧先解析,
|
||||
// 避免前端把短号直接发给 wallet,也避免 JS number 对长 user_id 造成精度损失。
|
||||
row := h.userDB.QueryRowContext(c.Request.Context(), `
|
||||
SELECT user_id, current_display_user_id, COALESCE(username, ''), COALESCE(avatar, '')
|
||||
FROM users
|
||||
WHERE app_code = ?
|
||||
AND (CAST(user_id AS CHAR) = ? OR current_display_user_id = ?)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN CAST(user_id AS CHAR) = ? THEN 0
|
||||
WHEN current_display_user_id = ? THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
user_id DESC
|
||||
LIMIT 1`,
|
||||
appctx.FromContext(c.Request.Context()), keyword, keyword, keyword, keyword,
|
||||
)
|
||||
user := grantUserDTO{}
|
||||
if err := row.Scan(&user.UserID, &user.DisplayUserID, &user.Username, &user.Avatar); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
response.BadRequest(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
response.ServerError(c, "查询用户失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, user)
|
||||
}
|
||||
|
||||
func (h *Handler) ListResourceGrants(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
targetUserID, ok := optionalInt64Query(c, "target_user_id", "targetUserId")
|
||||
@ -685,3 +734,13 @@ func optionalInt64QueryWithPresence(c *gin.Context, primary string, fallback str
|
||||
}
|
||||
return value, true, true
|
||||
}
|
||||
|
||||
func firstQuery(c *gin.Context, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
value := strings.TrimSpace(c.Query(key))
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -2,9 +2,30 @@ package resource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseGrantTargetUserIDAcceptsStringLongID(t *testing.T) {
|
||||
userID, err := parseGrantTargetUserID("318705991371722752")
|
||||
if err != nil {
|
||||
t.Fatalf("string long user id should be accepted: %v", err)
|
||||
}
|
||||
if userID != 318705991371722752 {
|
||||
t.Fatalf("user id mismatch: %d", userID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGrantTargetUserIDKeepsLegacyNumber(t *testing.T) {
|
||||
userID, err := parseGrantTargetUserID(float64(163000))
|
||||
if err != nil {
|
||||
t.Fatalf("legacy numeric user id should be accepted: %v", err)
|
||||
}
|
||||
if userID != 163000 {
|
||||
t.Fatalf("user id mismatch: %d", userID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyGrantOperatorsUsesSourceSpecificProfiles(t *testing.T) {
|
||||
grants := []grantDTO{
|
||||
{GrantID: "grant-admin", GrantSource: "admin", OperatorUserID: 7},
|
||||
@ -72,19 +93,78 @@ func TestEmojiPackMetadataKeepsLimitedRegions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBadgeResourceMetadataUsesSelectedForm(t *testing.T) {
|
||||
metadata := resourceMetadataJSON(resourceTypeBadge, "long")
|
||||
metadata := resourceMetadataJSON(resourceTypeBadge, "long", "", "", "")
|
||||
payload := badgeMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(metadata), &payload); err != nil {
|
||||
t.Fatalf("badge metadata json mismatch: %v", err)
|
||||
}
|
||||
if payload.BadgeForm != badgeFormStrip || payload.DefaultSlot != "profile_strip" {
|
||||
if payload.BadgeForm != badgeFormStrip || payload.BadgeKind != badgeKindNormal || payload.DefaultSlot != "profile_strip" {
|
||||
t.Fatalf("long badge metadata mismatch: %+v", payload)
|
||||
}
|
||||
if got := badgeFormFromMetadata(resourceMetadataJSON(resourceTypeBadge, "short")); got != badgeFormTile {
|
||||
if got := badgeFormFromMetadata(resourceMetadataJSON(resourceTypeBadge, "short", "", "", "")); got != badgeFormTile {
|
||||
t.Fatalf("short badge form mismatch: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadgeResourceMetadataMarksLevelBadge(t *testing.T) {
|
||||
metadata := resourceMetadataJSON(resourceTypeBadge, badgeFormStrip, "", badgeKindLevel, "wealth")
|
||||
payload := badgeMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(metadata), &payload); err != nil {
|
||||
t.Fatalf("badge metadata json mismatch: %v", err)
|
||||
}
|
||||
if payload.BadgeKind != badgeKindLevel || payload.LevelTrack != "wealth" || payload.BadgeForm != badgeFormStrip {
|
||||
t.Fatalf("level badge metadata mismatch: %+v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileCardResourceMetadataKeepsLayoutOnly(t *testing.T) {
|
||||
metadata := resourceMetadataJSON(resourceTypeProfileCard, "", `{
|
||||
"profile_card_layout": {
|
||||
"source_width": 1136,
|
||||
"source_height": 1680,
|
||||
"color_content_width": 750,
|
||||
"content_top": 117,
|
||||
"content_bottom": 1666,
|
||||
"content_height": 1550,
|
||||
"content_top_ratio": 0,
|
||||
"content_height_ratio": 0,
|
||||
"detect_version": 99
|
||||
},
|
||||
"debug": true
|
||||
}`, "", "")
|
||||
payload := profileCardMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(metadata), &payload); err != nil {
|
||||
t.Fatalf("profile card metadata json mismatch: %v", err)
|
||||
}
|
||||
if payload.ProfileCardLayout == nil {
|
||||
t.Fatalf("profile card layout should be kept")
|
||||
}
|
||||
if payload.ProfileCardLayout.ContentTop != 117 || payload.ProfileCardLayout.ContentHeight != 1550 {
|
||||
t.Fatalf("profile card layout mismatch: %+v", payload.ProfileCardLayout)
|
||||
}
|
||||
if payload.ProfileCardLayout.DetectVersion != 1 {
|
||||
t.Fatalf("profile card detect version should be normalized: %+v", payload.ProfileCardLayout)
|
||||
}
|
||||
if metadata == "{}" || strings.Contains(metadata, "debug") {
|
||||
t.Fatalf("profile card metadata should keep only layout: %s", metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileCardResourceMetadataRejectsInvalidLayout(t *testing.T) {
|
||||
metadata := resourceMetadataJSON(resourceTypeProfileCard, "", `{
|
||||
"profile_card_layout": {
|
||||
"source_width": 1136,
|
||||
"source_height": 1680,
|
||||
"content_top": 117,
|
||||
"content_bottom": 1666,
|
||||
"content_height": 1
|
||||
}
|
||||
}`, "", "")
|
||||
if metadata != "{}" {
|
||||
t.Fatalf("invalid profile card layout should be dropped: %s", metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBadgeResourceRequiresBadgeForm(t *testing.T) {
|
||||
if err := validateResourceBadgeForm(resourceRequest{ResourceType: resourceTypeBadge}); err == nil {
|
||||
t.Fatalf("badge form should be required")
|
||||
@ -92,6 +172,12 @@ func TestValidateBadgeResourceRequiresBadgeForm(t *testing.T) {
|
||||
if err := validateResourceBadgeForm(resourceRequest{ResourceType: resourceTypeBadge, BadgeForm: badgeFormTile}); err != nil {
|
||||
t.Fatalf("valid badge form rejected: %v", err)
|
||||
}
|
||||
if err := validateResourceBadgeForm(resourceRequest{ResourceType: resourceTypeBadge, BadgeForm: badgeFormStrip, BadgeKind: badgeKindLevel}); err == nil {
|
||||
t.Fatalf("level badge should require level track")
|
||||
}
|
||||
if err := validateResourceBadgeForm(resourceRequest{ResourceType: resourceTypeBadge, BadgeForm: badgeFormStrip, BadgeKind: badgeKindLevel, LevelTrack: "wealth"}); err != nil {
|
||||
t.Fatalf("valid level badge rejected: %v", err)
|
||||
}
|
||||
if err := validateResourceBadgeForm(resourceRequest{ResourceType: "gift", BadgeForm: ""}); err != nil {
|
||||
t.Fatalf("non-badge resource should not require badge form: %v", err)
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package resource
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@ -17,9 +18,10 @@ import (
|
||||
const dayMillis int64 = 24 * 60 * 60 * 1000
|
||||
|
||||
const (
|
||||
resourceTypeCoin = "coin"
|
||||
resourceTypeBadge = "badge"
|
||||
resourceTypeEmojiPack = "emoji_pack"
|
||||
resourceTypeCoin = "coin"
|
||||
resourceTypeBadge = "badge"
|
||||
resourceTypeEmojiPack = "emoji_pack"
|
||||
resourceTypeProfileCard = "profile_card"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -32,6 +34,11 @@ const (
|
||||
badgeFormTile = "tile"
|
||||
)
|
||||
|
||||
const (
|
||||
badgeKindNormal = "normal"
|
||||
badgeKindLevel = "level"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultEmojiPackCategory = "默认"
|
||||
emojiPackPricingFree = "free"
|
||||
@ -48,6 +55,9 @@ type resourceRequest struct {
|
||||
CoinPrice int64 `json:"coinPrice"`
|
||||
GiftPointAmount int64 `json:"giftPointAmount"`
|
||||
BadgeForm string `json:"badgeForm"`
|
||||
BadgeKind string `json:"badgeKind"`
|
||||
LevelTrack string `json:"levelTrack"`
|
||||
MetadataJSON string `json:"metadataJson"`
|
||||
ManagerGrantEnabled *bool `json:"managerGrantEnabled"`
|
||||
AssetURL string `json:"assetUrl"`
|
||||
PreviewURL string `json:"previewUrl"`
|
||||
@ -74,9 +84,27 @@ type emojiPackMetadataPayload struct {
|
||||
|
||||
type badgeMetadataPayload struct {
|
||||
BadgeForm string `json:"badge_form"`
|
||||
BadgeKind string `json:"badge_kind,omitempty"`
|
||||
LevelTrack string `json:"level_track,omitempty"`
|
||||
DefaultSlot string `json:"default_slot,omitempty"`
|
||||
}
|
||||
|
||||
type profileCardMetadataPayload struct {
|
||||
ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"`
|
||||
}
|
||||
|
||||
type profileCardLayoutMetadataPayload struct {
|
||||
SourceWidth int64 `json:"source_width"`
|
||||
SourceHeight int64 `json:"source_height"`
|
||||
ColorContentWidth int64 `json:"color_content_width"`
|
||||
ContentTop int64 `json:"content_top"`
|
||||
ContentBottom int64 `json:"content_bottom"`
|
||||
ContentHeight int64 `json:"content_height"`
|
||||
ContentTopRatio float64 `json:"content_top_ratio"`
|
||||
ContentHeightRatio float64 `json:"content_height_ratio"`
|
||||
DetectVersion int64 `json:"detect_version"`
|
||||
}
|
||||
|
||||
type resourceGroupRequest struct {
|
||||
GroupCode string `json:"groupCode"`
|
||||
Name string `json:"name"`
|
||||
@ -136,7 +164,7 @@ type giftTypeItemRequest struct {
|
||||
|
||||
type grantResourceRequest struct {
|
||||
CommandID string `json:"commandId"`
|
||||
TargetUserID int64 `json:"targetUserId"`
|
||||
TargetUserID any `json:"targetUserId"`
|
||||
ResourceID int64 `json:"resourceId"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
DurationMS int64 `json:"durationMs"`
|
||||
@ -145,7 +173,7 @@ type grantResourceRequest struct {
|
||||
|
||||
type grantGroupRequest struct {
|
||||
CommandID string `json:"commandId"`
|
||||
TargetUserID int64 `json:"targetUserId"`
|
||||
TargetUserID any `json:"targetUserId"`
|
||||
GroupID int64 `json:"groupId"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
@ -168,7 +196,7 @@ func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceReq
|
||||
resourceType := normalizeResourceType(r.ResourceType)
|
||||
walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount)
|
||||
priceType, coinPrice, giftPointAmount := resourcePricing(r.PriceType, r.CoinPrice)
|
||||
metadataJSON := resourceMetadataJSON(resourceType, r.BadgeForm)
|
||||
metadataJSON := resourceMetadataJSON(resourceType, r.BadgeForm, r.MetadataJSON, r.BadgeKind, r.LevelTrack)
|
||||
return &walletv1.CreateResourceRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
@ -194,11 +222,50 @@ func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceReq
|
||||
}
|
||||
}
|
||||
|
||||
func parseGrantTargetUserID(value any) (int64, error) {
|
||||
// Admin 前端会先把短号解析成内部 user_id;内部 user_id 已超过 JS 安全整数,
|
||||
// 所以新请求用字符串传递,避免浏览器把 318705991371722752 这类 ID 四舍五入。
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
userID, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||
if err != nil || userID <= 0 {
|
||||
return 0, fmt.Errorf("target_user_id is invalid")
|
||||
}
|
||||
return userID, nil
|
||||
case json.Number:
|
||||
userID, err := typed.Int64()
|
||||
if err != nil || userID <= 0 {
|
||||
return 0, fmt.Errorf("target_user_id is invalid")
|
||||
}
|
||||
return userID, nil
|
||||
case float64:
|
||||
// 兼容旧 admin-platform 对短号或小整数 user_id 传 JSON number 的请求;
|
||||
// 长 ID 必须走字符串,否则到这里前精度已经不可恢复。
|
||||
userID := int64(typed)
|
||||
if float64(userID) != typed || userID <= 0 {
|
||||
return 0, fmt.Errorf("target_user_id is invalid")
|
||||
}
|
||||
return userID, nil
|
||||
case int64:
|
||||
if typed <= 0 {
|
||||
return 0, fmt.Errorf("target_user_id is invalid")
|
||||
}
|
||||
return typed, nil
|
||||
case int:
|
||||
if typed <= 0 {
|
||||
return 0, fmt.Errorf("target_user_id is invalid")
|
||||
}
|
||||
return int64(typed), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("target_user_id is required")
|
||||
}
|
||||
}
|
||||
|
||||
func (r resourceRequest) updateProto(c *gin.Context, resourceID int64) *walletv1.UpdateResourceRequest {
|
||||
resourceType := normalizeResourceType(r.ResourceType)
|
||||
walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount)
|
||||
priceType, coinPrice, giftPointAmount := resourcePricing(r.PriceType, r.CoinPrice)
|
||||
metadataJSON := resourceMetadataJSON(resourceType, r.BadgeForm)
|
||||
metadataJSON := resourceMetadataJSON(resourceType, r.BadgeForm, r.MetadataJSON, r.BadgeKind, r.LevelTrack)
|
||||
return &walletv1.UpdateResourceRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
@ -460,6 +527,13 @@ func validateResourceBadgeForm(req resourceRequest) error {
|
||||
if normalizeBadgeForm(req.BadgeForm) == "" {
|
||||
return fmt.Errorf("请选择徽章属性")
|
||||
}
|
||||
badgeKind := normalizeBadgeKind(req.BadgeKind)
|
||||
if badgeKind == "" {
|
||||
return fmt.Errorf("请选择徽章类型")
|
||||
}
|
||||
if badgeKind == badgeKindLevel && !validLevelTrack(req.LevelTrack) {
|
||||
return fmt.Errorf("请选择等级徽章所属等级")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -476,11 +550,19 @@ func resourceGrantStrategy(resourceType string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func resourceMetadataJSON(resourceType string, badgeForm string) string {
|
||||
if resourceType != resourceTypeBadge {
|
||||
func resourceMetadataJSON(resourceType string, badgeForm string, metadataJSON string, badgeKind string, levelTrack string) string {
|
||||
switch resourceType {
|
||||
case resourceTypeBadge:
|
||||
return badgeMetadataJSON(badgeForm, badgeKind, levelTrack)
|
||||
case resourceTypeProfileCard:
|
||||
return profileCardMetadataJSON(metadataJSON)
|
||||
default:
|
||||
return "{}"
|
||||
}
|
||||
payload := badgeMetadataPayload{BadgeForm: normalizeBadgeForm(badgeForm)}
|
||||
}
|
||||
|
||||
func badgeMetadataJSON(badgeForm string, badgeKind string, levelTrack string) string {
|
||||
payload := badgeMetadataPayload{BadgeForm: normalizeBadgeForm(badgeForm), BadgeKind: normalizeBadgeKind(badgeKind)}
|
||||
switch payload.BadgeForm {
|
||||
case badgeFormStrip:
|
||||
payload.DefaultSlot = "profile_strip"
|
||||
@ -489,6 +571,12 @@ func resourceMetadataJSON(resourceType string, badgeForm string) string {
|
||||
default:
|
||||
return "{}"
|
||||
}
|
||||
if payload.BadgeKind == badgeKindLevel {
|
||||
payload.LevelTrack = normalizeLevelTrack(levelTrack)
|
||||
if payload.LevelTrack == "" {
|
||||
return "{}"
|
||||
}
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
@ -496,6 +584,66 @@ func resourceMetadataJSON(resourceType string, badgeForm string) string {
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func profileCardMetadataJSON(metadataJSON string) string {
|
||||
raw := strings.TrimSpace(metadataJSON)
|
||||
if raw == "" {
|
||||
return "{}"
|
||||
}
|
||||
payload := profileCardMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||
return "{}"
|
||||
}
|
||||
layout := sanitizeProfileCardLayoutMetadata(payload.ProfileCardLayout)
|
||||
if layout == nil {
|
||||
return "{}"
|
||||
}
|
||||
// 资料卡上传解析只允许写入布局字段;不要把后台临时调试数据透给 app。
|
||||
body, err := json.Marshal(profileCardMetadataPayload{ProfileCardLayout: layout})
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func sanitizeProfileCardLayoutMetadata(layout *profileCardLayoutMetadataPayload) *profileCardLayoutMetadataPayload {
|
||||
if layout == nil {
|
||||
return nil
|
||||
}
|
||||
if layout.SourceWidth <= 0 || layout.SourceHeight <= 0 {
|
||||
return nil
|
||||
}
|
||||
if layout.ContentTop < 0 || layout.ContentBottom < layout.ContentTop || layout.ContentBottom >= layout.SourceHeight {
|
||||
return nil
|
||||
}
|
||||
contentHeight := layout.ContentBottom - layout.ContentTop + 1
|
||||
if layout.ContentHeight != contentHeight {
|
||||
return nil
|
||||
}
|
||||
colorContentWidth := layout.ColorContentWidth
|
||||
if colorContentWidth <= 0 || colorContentWidth > layout.SourceWidth {
|
||||
colorContentWidth = layout.SourceWidth
|
||||
}
|
||||
// 比例字段由整数边界重新计算,避免前端浮点误差或脏数据进入客户端展示协议。
|
||||
return &profileCardLayoutMetadataPayload{
|
||||
SourceWidth: layout.SourceWidth,
|
||||
SourceHeight: layout.SourceHeight,
|
||||
ColorContentWidth: colorContentWidth,
|
||||
ContentTop: layout.ContentTop,
|
||||
ContentBottom: layout.ContentBottom,
|
||||
ContentHeight: contentHeight,
|
||||
ContentTopRatio: profileCardRatio(layout.ContentTop, layout.SourceHeight),
|
||||
ContentHeightRatio: profileCardRatio(contentHeight, layout.SourceHeight),
|
||||
DetectVersion: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func profileCardRatio(value int64, total int64) float64 {
|
||||
if total <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(value) / float64(total)
|
||||
}
|
||||
|
||||
func normalizeBadgeForm(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case badgeFormStrip, "long":
|
||||
@ -507,6 +655,34 @@ func normalizeBadgeForm(value string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeBadgeKind(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", badgeKindNormal:
|
||||
return badgeKindNormal
|
||||
case badgeKindLevel:
|
||||
return badgeKindLevel
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeLevelTrack(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
if validLevelTrack(value) {
|
||||
return value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validLevelTrack(value string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "wealth", "game", "charm":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func badgeFormFromMetadata(metadataJSON string) string {
|
||||
payload := badgeMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
|
||||
@ -515,6 +691,22 @@ func badgeFormFromMetadata(metadataJSON string) string {
|
||||
return normalizeBadgeForm(payload.BadgeForm)
|
||||
}
|
||||
|
||||
func badgeKindFromMetadata(metadataJSON string) string {
|
||||
payload := badgeMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
|
||||
return badgeKindNormal
|
||||
}
|
||||
return normalizeBadgeKind(payload.BadgeKind)
|
||||
}
|
||||
|
||||
func badgeLevelTrackFromMetadata(metadataJSON string) string {
|
||||
payload := badgeMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
return normalizeLevelTrack(payload.LevelTrack)
|
||||
}
|
||||
|
||||
func badgeFormForResource(resourceType string, metadataJSON string) string {
|
||||
if normalizeResourceType(resourceType) != resourceTypeBadge {
|
||||
return ""
|
||||
@ -522,6 +714,24 @@ func badgeFormForResource(resourceType string, metadataJSON string) string {
|
||||
return badgeFormFromMetadata(metadataJSON)
|
||||
}
|
||||
|
||||
func badgeKindForResource(resourceType string, metadataJSON string) string {
|
||||
if normalizeResourceType(resourceType) != resourceTypeBadge {
|
||||
return ""
|
||||
}
|
||||
kind := badgeKindFromMetadata(metadataJSON)
|
||||
if kind == "" {
|
||||
return badgeKindNormal
|
||||
}
|
||||
return kind
|
||||
}
|
||||
|
||||
func badgeLevelTrackForResource(resourceType string, metadataJSON string) string {
|
||||
if normalizeResourceType(resourceType) != resourceTypeBadge {
|
||||
return ""
|
||||
}
|
||||
return badgeLevelTrackFromMetadata(metadataJSON)
|
||||
}
|
||||
|
||||
func emojiPackMetadataJSON(regionIDs []int64, category string, pricingType string) (string, error) {
|
||||
normalizedRegionIDs := normalizeRegionIDs(regionIDs)
|
||||
metadata := emojiPackMetadataPayload{
|
||||
|
||||
@ -41,6 +41,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
|
||||
protected.POST("/admin/resource-grants/resource", middleware.RequirePermission("resource-grant:create"), h.GrantResource)
|
||||
protected.POST("/admin/resource-grants/group", middleware.RequirePermission("resource-grant:create"), h.GrantResourceGroup)
|
||||
protected.GET("/admin/resource-grants/target", middleware.RequirePermission("resource-grant:create"), h.LookupResourceGrantTarget)
|
||||
protected.GET("/admin/resource-grants", middleware.RequirePermission("resource-grant:view"), h.ListResourceGrants)
|
||||
|
||||
protected.GET("/admin/resource-shop/items", middleware.RequirePermission("resource-shop:view"), h.ListResourceShopItems)
|
||||
|
||||
@ -62,6 +62,7 @@ rocketmq:
|
||||
wallet_outbox:
|
||||
enabled: true
|
||||
topic: "hyapp_wallet_outbox"
|
||||
realtime_topic: "hyapp_wallet_realtime_outbox"
|
||||
first_recharge_consumer_group: "hyapp-activity-first-recharge-wallet-outbox"
|
||||
red_packet_broadcast_consumer_group: "hyapp-activity-red-packet-wallet-outbox"
|
||||
consumer_max_reconsume_times: 16
|
||||
|
||||
@ -61,6 +61,7 @@ rocketmq:
|
||||
wallet_outbox:
|
||||
enabled: true
|
||||
topic: "hyapp_wallet_outbox"
|
||||
realtime_topic: "hyapp_wallet_realtime_outbox"
|
||||
first_recharge_consumer_group: "hyapp-activity-first-recharge-wallet-outbox"
|
||||
red_packet_broadcast_consumer_group: "hyapp-activity-red-packet-wallet-outbox"
|
||||
consumer_max_reconsume_times: 16
|
||||
|
||||
@ -61,6 +61,7 @@ rocketmq:
|
||||
wallet_outbox:
|
||||
enabled: false
|
||||
topic: "hyapp_wallet_outbox"
|
||||
realtime_topic: ""
|
||||
first_recharge_consumer_group: "hyapp-activity-first-recharge-wallet-outbox"
|
||||
red_packet_broadcast_consumer_group: "hyapp-activity-red-packet-wallet-outbox"
|
||||
consumer_max_reconsume_times: 16
|
||||
|
||||
@ -729,18 +729,48 @@ CREATE TABLE IF NOT EXISTS user_level_display_profiles (
|
||||
wealth_tier_id BIGINT NOT NULL DEFAULT 0 COMMENT '财富档位 ID',
|
||||
wealth_avatar_frame_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '财富头像框资源 ID',
|
||||
wealth_badge_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '财富徽章资源 ID',
|
||||
wealth_badge_source_level INT NOT NULL DEFAULT 0 COMMENT '财富徽章来源等级',
|
||||
game_level INT NOT NULL DEFAULT 0 COMMENT '游戏等级',
|
||||
game_tier_id BIGINT NOT NULL DEFAULT 0 COMMENT '游戏档位 ID',
|
||||
game_avatar_frame_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '游戏头像框资源 ID',
|
||||
game_badge_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '游戏徽章资源 ID',
|
||||
game_badge_source_level INT NOT NULL DEFAULT 0 COMMENT '游戏徽章来源等级',
|
||||
charm_level INT NOT NULL DEFAULT 0 COMMENT '魅力等级',
|
||||
charm_tier_id BIGINT NOT NULL DEFAULT 0 COMMENT '魅力档位 ID',
|
||||
charm_avatar_frame_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '魅力头像框资源 ID',
|
||||
charm_badge_resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '魅力徽章资源 ID',
|
||||
charm_badge_source_level INT NOT NULL DEFAULT 0 COMMENT '魅力徽章来源等级',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户等级展示资料表';
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_level_display_profiles' AND COLUMN_NAME = 'wealth_badge_source_level') = 0,
|
||||
'ALTER TABLE user_level_display_profiles ADD COLUMN wealth_badge_source_level INT NOT NULL DEFAULT 0 COMMENT ''财富徽章来源等级'' AFTER wealth_badge_resource_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_level_display_profiles' AND COLUMN_NAME = 'game_badge_source_level') = 0,
|
||||
'ALTER TABLE user_level_display_profiles ADD COLUMN game_badge_source_level INT NOT NULL DEFAULT 0 COMMENT ''游戏徽章来源等级'' AFTER game_badge_resource_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_level_display_profiles' AND COLUMN_NAME = 'charm_badge_source_level') = 0,
|
||||
'ALTER TABLE user_level_display_profiles ADD COLUMN charm_badge_source_level INT NOT NULL DEFAULT 0 COMMENT ''魅力徽章来源等级'' AFTER charm_badge_resource_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
INSERT INTO growth_level_tracks (app_code, track, name, status, sort_order, display_config_json, created_at_ms, updated_at_ms)
|
||||
VALUES
|
||||
('lalu', 'wealth', '财富等级', 'active', 10, JSON_OBJECT(), 0, 0),
|
||||
|
||||
@ -232,7 +232,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
_ = repository.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := consumer.Subscribe(cfg.RocketMQ.WalletOutbox.Topic, walletmq.TagWalletOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
|
||||
if err := consumer.Subscribe(redPacketWalletOutboxTopic(cfg.RocketMQ.WalletOutbox), walletmq.TagWalletOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
|
||||
event, ok, err := redPacketEventFromWalletMessage(message.Body)
|
||||
if err != nil || !ok {
|
||||
return err
|
||||
@ -389,6 +389,14 @@ func walletOutboxConsumerConfig(cfg config.RocketMQConfig, group string) rocketm
|
||||
return rocketMQConsumerConfig(cfg, group, cfg.WalletOutbox.ConsumerMaxReconsumeTimes)
|
||||
}
|
||||
|
||||
func redPacketWalletOutboxTopic(cfg config.WalletOutboxMQConfig) string {
|
||||
// 钱包实时 topic 显式配置后,红包 worker 只消费实时通道,避免继续被普通账务 topic 的高频消息拖慢。
|
||||
if cfg.RealtimeTopic != "" {
|
||||
return cfg.RealtimeTopic
|
||||
}
|
||||
return cfg.Topic
|
||||
}
|
||||
|
||||
func rocketMQConsumerConfig(cfg config.RocketMQConfig, group string, maxReconsume int32) rocketmqx.ConsumerConfig {
|
||||
return rocketmqx.ConsumerConfig{
|
||||
EndpointConfig: rocketmqx.EndpointConfig{
|
||||
|
||||
@ -147,6 +147,7 @@ type RoomOutboxMQConfig struct {
|
||||
type WalletOutboxMQConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Topic string `yaml:"topic"`
|
||||
RealtimeTopic string `yaml:"realtime_topic"`
|
||||
FirstRechargeConsumerGroup string `yaml:"first_recharge_consumer_group"`
|
||||
RedPacketBroadcastConsumerGroup string `yaml:"red_packet_broadcast_consumer_group"`
|
||||
ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"`
|
||||
@ -226,6 +227,7 @@ func defaultRocketMQConfig() RocketMQConfig {
|
||||
WalletOutbox: WalletOutboxMQConfig{
|
||||
Enabled: false,
|
||||
Topic: "hyapp_wallet_outbox",
|
||||
RealtimeTopic: "",
|
||||
FirstRechargeConsumerGroup: "hyapp-activity-first-recharge-wallet-outbox",
|
||||
RedPacketBroadcastConsumerGroup: "hyapp-activity-red-packet-wallet-outbox",
|
||||
ConsumerMaxReconsumeTimes: 16,
|
||||
@ -355,6 +357,7 @@ func normalizeRocketMQConfig(cfg RocketMQConfig) (RocketMQConfig, error) {
|
||||
if cfg.WalletOutbox.Topic = strings.TrimSpace(cfg.WalletOutbox.Topic); cfg.WalletOutbox.Topic == "" {
|
||||
cfg.WalletOutbox.Topic = defaults.WalletOutbox.Topic
|
||||
}
|
||||
cfg.WalletOutbox.RealtimeTopic = strings.TrimSpace(cfg.WalletOutbox.RealtimeTopic)
|
||||
if cfg.WalletOutbox.FirstRechargeConsumerGroup = strings.TrimSpace(cfg.WalletOutbox.FirstRechargeConsumerGroup); cfg.WalletOutbox.FirstRechargeConsumerGroup == "" {
|
||||
cfg.WalletOutbox.FirstRechargeConsumerGroup = defaults.WalletOutbox.FirstRechargeConsumerGroup
|
||||
}
|
||||
|
||||
@ -20,4 +20,7 @@ func TestLoadTencentExampleEnablesRoomOutboxMQ(t *testing.T) {
|
||||
if !cfg.RocketMQ.Enabled || !cfg.RocketMQ.RoomOutbox.Enabled || cfg.RocketMQ.RoomOutbox.Topic == "" || cfg.RocketMQ.RoomOutbox.ConsumerGroup == "" {
|
||||
t.Fatalf("tencent example must configure room outbox MQ consumer: %+v", cfg.RocketMQ)
|
||||
}
|
||||
if cfg.RocketMQ.WalletOutbox.RealtimeTopic != "hyapp_wallet_realtime_outbox" {
|
||||
t.Fatalf("red packet worker should consume realtime wallet topic in tencent example: %+v", cfg.RocketMQ.WalletOutbox)
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,10 +100,49 @@ type TrackOverview struct {
|
||||
NextLevelRequiredValue int64
|
||||
DisplayAvatarFrameResourceID int64
|
||||
DisplayBadgeResourceID int64
|
||||
DisplayBadgeSourceLevel int32
|
||||
RewardPendingCount int64
|
||||
SortOrder int32
|
||||
}
|
||||
|
||||
// LevelDisplayTrackProfile 是房间资料、资料卡这类轻量展示面读取的单轨道投影。
|
||||
type LevelDisplayTrackProfile struct {
|
||||
Track string
|
||||
Level int32
|
||||
TierID int64
|
||||
AvatarFrameResourceID int64
|
||||
BadgeResourceID int64
|
||||
BadgeSourceLevel int32
|
||||
UpdatedAtMS int64
|
||||
}
|
||||
|
||||
// UserLevelDisplayProfile 固定携带 wealth/game/charm 三条轨道,避免展示层按数组顺序猜含义。
|
||||
type UserLevelDisplayProfile struct {
|
||||
UserID int64
|
||||
Wealth LevelDisplayTrackProfile
|
||||
Game LevelDisplayTrackProfile
|
||||
Charm LevelDisplayTrackProfile
|
||||
}
|
||||
|
||||
// LevelDisplayProfiles 是批量用户展示投影的只读快照。
|
||||
type LevelDisplayProfiles struct {
|
||||
Profiles []UserLevelDisplayProfile
|
||||
ServerTimeMS int64
|
||||
}
|
||||
|
||||
// RegistrationLevelBadgeGrant 是注册后按等级规则补发的 1 级等级徽章发放结果。
|
||||
type RegistrationLevelBadgeGrant struct {
|
||||
Track string
|
||||
Level int32
|
||||
ResourceID int64
|
||||
GrantID string
|
||||
}
|
||||
|
||||
type RegistrationLevelBadgeGrants struct {
|
||||
Grants []RegistrationLevelBadgeGrant
|
||||
ServerTimeMS int64
|
||||
}
|
||||
|
||||
// TrackDetail 返回一个轨道的完整阈值、等级段和当前用户进度。
|
||||
type TrackDetail struct {
|
||||
Overview TrackOverview
|
||||
|
||||
@ -3,6 +3,8 @@ package growth
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -17,10 +19,16 @@ import (
|
||||
|
||||
const levelRewardReason = "growth_level_reward"
|
||||
|
||||
const (
|
||||
registrationLevelBadgeReason = "growth_level_registration_badge"
|
||||
registrationLevelBadgeSourceLevel = int32(1)
|
||||
)
|
||||
|
||||
// Repository 是等级体系的 activity MySQL 持久化边界。
|
||||
type Repository interface {
|
||||
ListLevelOverview(ctx context.Context, userID int64, nowMS int64) (domain.Overview, error)
|
||||
GetLevelTrack(ctx context.Context, userID int64, track string, nowMS int64) (domain.TrackDetail, error)
|
||||
BatchGetUserLevelDisplayProfiles(ctx context.Context, userIDs []int64, nowMS int64) (domain.LevelDisplayProfiles, error)
|
||||
ListLevelConfig(ctx context.Context, query domain.ConfigQuery, nowMS int64) (domain.Config, error)
|
||||
ListLevelRewards(ctx context.Context, query domain.RewardQuery) ([]domain.RewardJob, int64, error)
|
||||
ConsumeLevelEvent(ctx context.Context, event domain.ValueEvent, nowMS int64) (domain.EventResult, error)
|
||||
@ -34,6 +42,7 @@ type Repository interface {
|
||||
|
||||
// WalletClient 是等级奖励发放的唯一外部副作用。
|
||||
type WalletClient interface {
|
||||
GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest, opts ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error)
|
||||
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest, opts ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error)
|
||||
}
|
||||
|
||||
@ -75,6 +84,76 @@ func (s *Service) GetLevelTrack(ctx context.Context, userID int64, track string)
|
||||
return s.repository.GetLevelTrack(ctx, userID, track, s.now().UnixMilli())
|
||||
}
|
||||
|
||||
func (s *Service) BatchGetUserLevelDisplayProfiles(ctx context.Context, userIDs []int64) (domain.LevelDisplayProfiles, error) {
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return domain.LevelDisplayProfiles{}, err
|
||||
}
|
||||
normalized := make([]int64, 0, len(userIDs))
|
||||
seen := make(map[int64]struct{}, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
if userID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[userID]; exists {
|
||||
continue
|
||||
}
|
||||
seen[userID] = struct{}{}
|
||||
normalized = append(normalized, userID)
|
||||
}
|
||||
if len(normalized) == 0 {
|
||||
return domain.LevelDisplayProfiles{Profiles: []domain.UserLevelDisplayProfile{}, ServerTimeMS: s.now().UnixMilli()}, nil
|
||||
}
|
||||
return s.repository.BatchGetUserLevelDisplayProfiles(ctx, normalized, s.now().UnixMilli())
|
||||
}
|
||||
|
||||
func (s *Service) IssueRegistrationLevelBadges(ctx context.Context, userID int64, commandID string) (domain.RegistrationLevelBadgeGrants, error) {
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return domain.RegistrationLevelBadgeGrants{}, err
|
||||
}
|
||||
if s.wallet == nil {
|
||||
return domain.RegistrationLevelBadgeGrants{}, xerr.New(xerr.Unavailable, "wallet client is not configured")
|
||||
}
|
||||
commandID = strings.TrimSpace(commandID)
|
||||
if userID <= 0 || commandID == "" {
|
||||
return domain.RegistrationLevelBadgeGrants{}, xerr.New(xerr.InvalidArgument, "registration level badge command is incomplete")
|
||||
}
|
||||
nowMS := s.now().UnixMilli()
|
||||
config, err := s.repository.ListLevelConfig(ctx, domain.ConfigQuery{Status: domain.StatusActive}, nowMS)
|
||||
if err != nil {
|
||||
return domain.RegistrationLevelBadgeGrants{}, err
|
||||
}
|
||||
grants := make([]domain.RegistrationLevelBadgeGrant, 0, 3)
|
||||
for _, track := range []string{domain.TrackWealth, domain.TrackGame, domain.TrackCharm} {
|
||||
resourceID := levelRuleBadgeForExactLevel(config.Rules, track, registrationLevelBadgeSourceLevel)
|
||||
if resourceID <= 0 {
|
||||
// 没有配置 1 级等级徽章时跳过该轨道;注册主流程和其它轨道不受影响。
|
||||
continue
|
||||
}
|
||||
// wallet 的 command_id 是实际发放幂等边界;按轨道拆分后,部分成功的重试只会补齐失败轨道。
|
||||
resp, err := s.wallet.GrantResource(ctx, &walletv1.GrantResourceRequest{
|
||||
CommandId: fmt.Sprintf("%s:%s", commandID, track),
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
TargetUserId: userID,
|
||||
ResourceId: resourceID,
|
||||
Quantity: 1,
|
||||
DurationMs: 0,
|
||||
Reason: registrationLevelBadgeReason,
|
||||
OperatorUserId: userID,
|
||||
GrantSource: domain.GrantSourceGrowthLevel,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.RegistrationLevelBadgeGrants{}, err
|
||||
}
|
||||
grants = append(grants, domain.RegistrationLevelBadgeGrant{
|
||||
Track: track,
|
||||
Level: registrationLevelBadgeSourceLevel,
|
||||
ResourceID: resourceID,
|
||||
GrantID: resp.GetGrant().GetGrantId(),
|
||||
})
|
||||
}
|
||||
return domain.RegistrationLevelBadgeGrants{Grants: grants, ServerTimeMS: nowMS}, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListLevelConfig(ctx context.Context, query domain.ConfigQuery) (domain.Config, error) {
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return domain.Config{}, err
|
||||
@ -93,6 +172,16 @@ func (s *Service) ListLevelConfig(ctx context.Context, query domain.ConfigQuery)
|
||||
return s.repository.ListLevelConfig(ctx, query, s.now().UnixMilli())
|
||||
}
|
||||
|
||||
func levelRuleBadgeForExactLevel(rules []domain.Rule, track string, level int32) int64 {
|
||||
for _, rule := range rules {
|
||||
if rule.Track != track || rule.Level != level || rule.Status != domain.StatusActive {
|
||||
continue
|
||||
}
|
||||
return longBadgeResourceIDFromDisplayConfig(rule.DisplayConfigJSON)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *Service) ListLevelRewards(ctx context.Context, query domain.RewardQuery) ([]domain.RewardJob, int64, error) {
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return nil, 0, err
|
||||
@ -212,7 +301,7 @@ func (s *Service) UpsertLevelRule(ctx context.Context, command domain.RuleComman
|
||||
command.Name = strings.TrimSpace(command.Name)
|
||||
command.Status = normalizeStatus(command.Status)
|
||||
command.DisplayConfigJSON = normalizeJSON(command.DisplayConfigJSON)
|
||||
if !validTrack(command.Track) || command.Level <= 0 || command.RequiredValue <= 0 || command.Name == "" || !validStatus(command.Status) || command.OperatorAdminID <= 0 {
|
||||
if !validTrack(command.Track) || command.Level < 0 || command.RequiredValue < 0 || command.Name == "" || !validStatus(command.Status) || command.OperatorAdminID <= 0 {
|
||||
return domain.Rule{}, false, xerr.New(xerr.InvalidArgument, "level rule command is invalid")
|
||||
}
|
||||
if command.RewardResourceGroupID < 0 {
|
||||
@ -229,7 +318,7 @@ func (s *Service) UpsertLevelTier(ctx context.Context, command domain.TierComman
|
||||
command.Name = strings.TrimSpace(command.Name)
|
||||
command.Status = normalizeStatus(command.Status)
|
||||
command.DisplayConfigJSON = normalizeJSON(command.DisplayConfigJSON)
|
||||
if !validTrack(command.Track) || command.MinLevel <= 0 || command.MaxLevel < command.MinLevel || command.Name == "" || !validStatus(command.Status) || command.OperatorAdminID <= 0 {
|
||||
if !validTrack(command.Track) || command.MinLevel < 0 || command.MaxLevel < command.MinLevel || command.Name == "" || !validStatus(command.Status) || command.OperatorAdminID <= 0 {
|
||||
return domain.Tier{}, false, xerr.New(xerr.InvalidArgument, "level tier command is invalid")
|
||||
}
|
||||
if command.DisplayAvatarFrameResourceID < 0 || command.DisplayBadgeResourceID < 0 || command.RewardResourceGroupID < 0 {
|
||||
@ -356,6 +445,29 @@ func normalizeJSON(value string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
func longBadgeResourceIDFromDisplayConfig(raw string) int64 {
|
||||
payload := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &payload); err != nil {
|
||||
return 0
|
||||
}
|
||||
value, ok := payload["long_badge_resource_id"]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
if typed > 0 {
|
||||
return int64(typed)
|
||||
}
|
||||
case string:
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||
if err == nil && parsed > 0 {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func validTrack(value string) bool {
|
||||
switch value {
|
||||
case domain.TrackWealth, domain.TrackGame, domain.TrackCharm:
|
||||
|
||||
@ -61,9 +61,16 @@ func TestGrowthLevelEventRewardFlow(t *testing.T) {
|
||||
t.Fatalf("GetMyLevelOverview failed: %v", err)
|
||||
}
|
||||
wealth := findTrackOverview(overview.Tracks, domain.TrackWealth)
|
||||
if wealth.Level != 2 || wealth.TierID == 0 || wealth.TotalValue != 250 || wealth.DisplayAvatarFrameResourceID != 502 || wealth.DisplayBadgeResourceID != 602 || wealth.RewardPendingCount != 2 {
|
||||
if wealth.Level != 2 || wealth.TierID == 0 || wealth.TotalValue != 250 || wealth.DisplayAvatarFrameResourceID != 502 || wealth.DisplayBadgeResourceID != 702 || wealth.DisplayBadgeSourceLevel != 2 || wealth.RewardPendingCount != 2 {
|
||||
t.Fatalf("wealth overview mismatch: %+v", wealth)
|
||||
}
|
||||
profiles, err := svc.BatchGetUserLevelDisplayProfiles(ctx, []int64{10001})
|
||||
if err != nil {
|
||||
t.Fatalf("BatchGetUserLevelDisplayProfiles failed: %v", err)
|
||||
}
|
||||
if len(profiles.Profiles) != 1 || profiles.Profiles[0].Wealth.BadgeResourceID != 702 || profiles.Profiles[0].Wealth.BadgeSourceLevel != 2 {
|
||||
t.Fatalf("display profile should use highest configured rule badge: %+v", profiles.Profiles)
|
||||
}
|
||||
|
||||
claimed, processed, success, failure, hasMore, err := svc.ProcessLevelRewardBatch(ctx, "run-1", "worker-1", 10, time.Second)
|
||||
if err != nil {
|
||||
@ -154,6 +161,61 @@ func TestListLevelConfigReturnsAdminRulesAndTiers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroLevelRuleAndTierAreValid(t *testing.T) {
|
||||
svc, _ := newGrowthService(t)
|
||||
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||
if _, created, err := svc.UpsertLevelRule(ctx, domain.RuleCommand{
|
||||
Track: domain.TrackWealth,
|
||||
Level: 0,
|
||||
RequiredValue: 0,
|
||||
Name: "wealth 0",
|
||||
Status: domain.StatusActive,
|
||||
SortOrder: 0,
|
||||
DisplayConfigJSON: `{"long_badge_resource_id":700}`,
|
||||
OperatorAdminID: 90001,
|
||||
}); err != nil || !created {
|
||||
t.Fatalf("seed level rule 0 failed: created=%v err=%v", created, err)
|
||||
}
|
||||
if _, created, err := svc.UpsertLevelTier(ctx, domain.TierCommand{
|
||||
Track: domain.TrackWealth,
|
||||
MinLevel: 0,
|
||||
MaxLevel: 9,
|
||||
Name: "wealth 0-9",
|
||||
Status: domain.StatusActive,
|
||||
OperatorAdminID: 90001,
|
||||
}); err != nil || !created {
|
||||
t.Fatalf("seed tier 0 failed: created=%v err=%v", created, err)
|
||||
}
|
||||
detail, err := svc.GetLevelTrack(ctx, 30001, domain.TrackWealth)
|
||||
if err != nil {
|
||||
t.Fatalf("GetLevelTrack failed: %v", err)
|
||||
}
|
||||
if detail.Overview.Level != 0 || detail.Overview.CurrentLevelRequiredValue != 0 || detail.Overview.DisplayBadgeResourceID != 700 || detail.Overview.DisplayBadgeSourceLevel != 0 {
|
||||
t.Fatalf("zero level overview mismatch: %+v", detail.Overview)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueRegistrationLevelBadgesGrantsLevelOneBadges(t *testing.T) {
|
||||
svc, wallet := newGrowthService(t)
|
||||
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||
seedGrowthRules(t, ctx, svc, domain.TrackWealth)
|
||||
seedGrowthRules(t, ctx, svc, domain.TrackGame)
|
||||
seedGrowthRules(t, ctx, svc, domain.TrackCharm)
|
||||
|
||||
grants, err := svc.IssueRegistrationLevelBadges(ctx, 30001, "registration_level_badges:lalu:30001")
|
||||
if err != nil {
|
||||
t.Fatalf("IssueRegistrationLevelBadges failed: %v", err)
|
||||
}
|
||||
if len(grants.Grants) != 3 || len(wallet.resourceGrants) != 3 {
|
||||
t.Fatalf("registration level badge grants mismatch: grants=%+v wallet=%d", grants.Grants, len(wallet.resourceGrants))
|
||||
}
|
||||
for _, req := range wallet.resourceGrants {
|
||||
if req.GetTargetUserId() != 30001 || req.GetResourceId() != 701 || req.GetQuantity() != 1 || req.GetGrantSource() != domain.GrantSourceGrowthLevel {
|
||||
t.Fatalf("wallet grant request mismatch: %+v", req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newGrowthService(t *testing.T) (*growthservice.Service, *fakeGrowthWallet) {
|
||||
t.Helper()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
@ -173,6 +235,7 @@ func seedGrowthRules(t *testing.T, ctx context.Context, svc *growthservice.Servi
|
||||
Status: domain.StatusActive,
|
||||
RewardResourceGroupID: 0,
|
||||
SortOrder: 1,
|
||||
DisplayConfigJSON: `{"long_badge_resource_id":701}`,
|
||||
OperatorAdminID: 90001,
|
||||
}); err != nil || !created {
|
||||
t.Fatalf("seed level rule 1 failed: created=%v err=%v", created, err)
|
||||
@ -185,6 +248,7 @@ func seedGrowthRules(t *testing.T, ctx context.Context, svc *growthservice.Servi
|
||||
Status: domain.StatusActive,
|
||||
RewardResourceGroupID: 9002,
|
||||
SortOrder: 2,
|
||||
DisplayConfigJSON: `{"long_badge_resource_id":702}`,
|
||||
OperatorAdminID: 90001,
|
||||
}); err != nil || !created {
|
||||
t.Fatalf("seed level rule 2 failed: created=%v err=%v", created, err)
|
||||
@ -231,7 +295,18 @@ func findTrackOverview(items []domain.TrackOverview, track string) domain.TrackO
|
||||
}
|
||||
|
||||
type fakeGrowthWallet struct {
|
||||
grants []*walletv1.GrantResourceGroupRequest
|
||||
grants []*walletv1.GrantResourceGroupRequest
|
||||
resourceGrants []*walletv1.GrantResourceRequest
|
||||
}
|
||||
|
||||
func (f *fakeGrowthWallet) GrantResource(_ context.Context, req *walletv1.GrantResourceRequest, _ ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
|
||||
f.resourceGrants = append(f.resourceGrants, req)
|
||||
return &walletv1.ResourceGrantResponse{
|
||||
Grant: &walletv1.ResourceGrant{
|
||||
GrantId: fmt.Sprintf("resource-grant-%d", len(f.resourceGrants)),
|
||||
Status: "succeeded",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fakeGrowthWallet) GrantResourceGroup(_ context.Context, req *walletv1.GrantResourceGroupRequest, _ ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
|
||||
|
||||
@ -3,8 +3,10 @@ package mysql
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -63,6 +65,66 @@ func (r *Repository) GetLevelTrack(ctx context.Context, userID int64, track stri
|
||||
return domain.TrackDetail{Overview: overview, Rules: rules, Tiers: tiers, ServerTimeMS: nowMS}, nil
|
||||
}
|
||||
|
||||
// BatchGetUserLevelDisplayProfiles 返回资料卡和房间列表需要的等级展示投影。
|
||||
func (r *Repository) BatchGetUserLevelDisplayProfiles(ctx context.Context, userIDs []int64, nowMS int64) (domain.LevelDisplayProfiles, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return domain.LevelDisplayProfiles{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
userIDs = uniquePositiveInt64s(userIDs)
|
||||
if len(userIDs) == 0 {
|
||||
return domain.LevelDisplayProfiles{Profiles: []domain.UserLevelDisplayProfile{}, ServerTimeMS: nowMS}, nil
|
||||
}
|
||||
profiles := make(map[int64]domain.UserLevelDisplayProfile, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
profiles[userID] = defaultLevelDisplayProfile(userID)
|
||||
}
|
||||
args := make([]any, 0, len(userIDs)+1)
|
||||
args = append(args, appcode.FromContext(ctx))
|
||||
for _, userID := range userIDs {
|
||||
args = append(args, userID)
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT user_id,
|
||||
wealth_level, wealth_tier_id, wealth_avatar_frame_resource_id, wealth_badge_resource_id, wealth_badge_source_level,
|
||||
game_level, game_tier_id, game_avatar_frame_resource_id, game_badge_resource_id, game_badge_source_level,
|
||||
charm_level, charm_tier_id, charm_avatar_frame_resource_id, charm_badge_resource_id, charm_badge_source_level,
|
||||
updated_at_ms
|
||||
FROM user_level_display_profiles
|
||||
WHERE app_code = ? AND user_id IN (`+placeholders(len(userIDs))+`)`, args...)
|
||||
if err != nil {
|
||||
return domain.LevelDisplayProfiles{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var item domain.UserLevelDisplayProfile
|
||||
var updatedAtMS int64
|
||||
if err := rows.Scan(
|
||||
&item.UserID,
|
||||
&item.Wealth.Level, &item.Wealth.TierID, &item.Wealth.AvatarFrameResourceID, &item.Wealth.BadgeResourceID, &item.Wealth.BadgeSourceLevel,
|
||||
&item.Game.Level, &item.Game.TierID, &item.Game.AvatarFrameResourceID, &item.Game.BadgeResourceID, &item.Game.BadgeSourceLevel,
|
||||
&item.Charm.Level, &item.Charm.TierID, &item.Charm.AvatarFrameResourceID, &item.Charm.BadgeResourceID, &item.Charm.BadgeSourceLevel,
|
||||
&updatedAtMS,
|
||||
); err != nil {
|
||||
return domain.LevelDisplayProfiles{}, err
|
||||
}
|
||||
item.Wealth.Track = domain.TrackWealth
|
||||
item.Game.Track = domain.TrackGame
|
||||
item.Charm.Track = domain.TrackCharm
|
||||
item.Wealth.UpdatedAtMS = updatedAtMS
|
||||
item.Game.UpdatedAtMS = updatedAtMS
|
||||
item.Charm.UpdatedAtMS = updatedAtMS
|
||||
profiles[item.UserID] = item
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return domain.LevelDisplayProfiles{}, err
|
||||
}
|
||||
result := make([]domain.UserLevelDisplayProfile, 0, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
result = append(result, profiles[userID])
|
||||
}
|
||||
return domain.LevelDisplayProfiles{Profiles: result, ServerTimeMS: nowMS}, nil
|
||||
}
|
||||
|
||||
// ListLevelConfig 返回后台等级配置页需要的轨道、单级阈值和等级段展示配置。
|
||||
func (r *Repository) ListLevelConfig(ctx context.Context, query domain.ConfigQuery, nowMS int64) (domain.Config, error) {
|
||||
if r == nil || r.db == nil {
|
||||
@ -186,7 +248,8 @@ func (r *Repository) ConsumeLevelEvent(ctx context.Context, event domain.ValueEv
|
||||
}
|
||||
rewardJobCount = count
|
||||
}
|
||||
if err := r.upsertLevelDisplayProfile(ctx, tx, event.UserID, event.Track, nextLevel, nextTier, nowMS); err != nil {
|
||||
badgeResourceID, badgeSourceLevel := levelRuleBadgeForLevel(rules, nextLevel)
|
||||
if err := r.upsertLevelDisplayProfile(ctx, tx, event.UserID, event.Track, nextLevel, nextTier, badgeResourceID, badgeSourceLevel, nowMS); err != nil {
|
||||
return domain.EventResult{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
@ -405,6 +468,7 @@ func (r *Repository) levelTrackOverview(ctx context.Context, userID int64, track
|
||||
return domain.TrackOverview{}, err
|
||||
}
|
||||
tier := tierByID(tiers, account.CurrentTierID)
|
||||
badgeResourceID, badgeSourceLevel := levelRuleBadgeForLevel(rules, account.CurrentLevel)
|
||||
currentRequired, nextLevel, nextRequired := levelProgress(rules, account.CurrentLevel)
|
||||
pendingCount, err := r.pendingLevelRewardCount(ctx, userID, track.Track)
|
||||
if err != nil {
|
||||
@ -420,7 +484,8 @@ func (r *Repository) levelTrackOverview(ctx context.Context, userID int64, track
|
||||
NextLevel: nextLevel,
|
||||
NextLevelRequiredValue: nextRequired,
|
||||
DisplayAvatarFrameResourceID: tier.DisplayAvatarFrameResourceID,
|
||||
DisplayBadgeResourceID: tier.DisplayBadgeResourceID,
|
||||
DisplayBadgeResourceID: badgeResourceID,
|
||||
DisplayBadgeSourceLevel: badgeSourceLevel,
|
||||
RewardPendingCount: pendingCount,
|
||||
SortOrder: track.SortOrder,
|
||||
}, nil
|
||||
@ -532,7 +597,7 @@ func (r *Repository) insertLevelRewardJob(ctx context.Context, tx *sql.Tx, userI
|
||||
return affected > 0, err
|
||||
}
|
||||
|
||||
func (r *Repository) upsertLevelDisplayProfile(ctx context.Context, tx *sql.Tx, userID int64, track string, level int32, tier domain.Tier, nowMS int64) error {
|
||||
func (r *Repository) upsertLevelDisplayProfile(ctx context.Context, tx *sql.Tx, userID int64, track string, level int32, tier domain.Tier, badgeResourceID int64, badgeSourceLevel int32, nowMS int64) error {
|
||||
columnPrefix := ""
|
||||
switch track {
|
||||
case domain.TrackWealth:
|
||||
@ -546,18 +611,19 @@ func (r *Repository) upsertLevelDisplayProfile(ctx context.Context, tx *sql.Tx,
|
||||
}
|
||||
query := fmt.Sprintf(`
|
||||
INSERT INTO user_level_display_profiles (
|
||||
app_code, user_id, %s_level, %s_tier_id, %s_avatar_frame_resource_id, %s_badge_resource_id, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
app_code, user_id, %s_level, %s_tier_id, %s_avatar_frame_resource_id, %s_badge_resource_id, %s_badge_source_level, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
%s_level = VALUES(%s_level),
|
||||
%s_tier_id = VALUES(%s_tier_id),
|
||||
%s_avatar_frame_resource_id = VALUES(%s_avatar_frame_resource_id),
|
||||
%s_badge_resource_id = VALUES(%s_badge_resource_id),
|
||||
%s_badge_source_level = VALUES(%s_badge_source_level),
|
||||
updated_at_ms = VALUES(updated_at_ms)`,
|
||||
columnPrefix, columnPrefix, columnPrefix, columnPrefix,
|
||||
columnPrefix, columnPrefix, columnPrefix, columnPrefix, columnPrefix, columnPrefix, columnPrefix, columnPrefix,
|
||||
columnPrefix, columnPrefix, columnPrefix, columnPrefix, columnPrefix,
|
||||
columnPrefix, columnPrefix, columnPrefix, columnPrefix, columnPrefix, columnPrefix, columnPrefix, columnPrefix, columnPrefix, columnPrefix,
|
||||
)
|
||||
_, err := tx.ExecContext(ctx, query, appcode.FromContext(ctx), userID, level, tier.TierID, tier.DisplayAvatarFrameResourceID, tier.DisplayBadgeResourceID, nowMS)
|
||||
_, err := tx.ExecContext(ctx, query, appcode.FromContext(ctx), userID, level, tier.TierID, tier.DisplayAvatarFrameResourceID, badgeResourceID, badgeSourceLevel, nowMS)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -779,7 +845,7 @@ func levelForValue(rules []domain.Rule, totalValue int64) int32 {
|
||||
}
|
||||
|
||||
func tierForLevel(tiers []domain.Tier, level int32) domain.Tier {
|
||||
if level <= 0 {
|
||||
if level < 0 {
|
||||
return domain.Tier{}
|
||||
}
|
||||
for _, tier := range tiers {
|
||||
@ -819,6 +885,92 @@ func levelProgress(rules []domain.Rule, currentLevel int32) (int64, int32, int64
|
||||
return currentRequired, nextLevel, nextRequired
|
||||
}
|
||||
|
||||
func levelRuleBadgeForLevel(rules []domain.Rule, currentLevel int32) (int64, int32) {
|
||||
if currentLevel < 0 {
|
||||
return 0, 0
|
||||
}
|
||||
badgeResourceID := int64(0)
|
||||
badgeSourceLevel := int32(0)
|
||||
for _, rule := range rules {
|
||||
if rule.Level > currentLevel || rule.Level < badgeSourceLevel {
|
||||
continue
|
||||
}
|
||||
candidate := longBadgeResourceIDFromDisplayConfig(rule.DisplayConfigJSON)
|
||||
if candidate <= 0 {
|
||||
continue
|
||||
}
|
||||
// 等级徽章按当前等级以内最高配置生效;后续低等级配置不再覆盖更高等级展示。
|
||||
badgeResourceID = candidate
|
||||
badgeSourceLevel = rule.Level
|
||||
}
|
||||
return badgeResourceID, badgeSourceLevel
|
||||
}
|
||||
|
||||
func longBadgeResourceIDFromDisplayConfig(raw string) int64 {
|
||||
payload := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &payload); err != nil {
|
||||
return 0
|
||||
}
|
||||
value, ok := payload["long_badge_resource_id"]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
if typed > 0 {
|
||||
return int64(typed)
|
||||
}
|
||||
case string:
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||
if err == nil && parsed > 0 {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func defaultLevelDisplayProfile(userID int64) domain.UserLevelDisplayProfile {
|
||||
return domain.UserLevelDisplayProfile{
|
||||
UserID: userID,
|
||||
Wealth: domain.LevelDisplayTrackProfile{
|
||||
Track: domain.TrackWealth,
|
||||
},
|
||||
Game: domain.LevelDisplayTrackProfile{
|
||||
Track: domain.TrackGame,
|
||||
},
|
||||
Charm: domain.LevelDisplayTrackProfile{
|
||||
Track: domain.TrackCharm,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func uniquePositiveInt64s(values []int64) []int64 {
|
||||
seen := make(map[int64]struct{}, len(values))
|
||||
result := make([]int64, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[value]; exists {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func placeholders(count int) string {
|
||||
if count <= 0 {
|
||||
return ""
|
||||
}
|
||||
items := make([]string, count)
|
||||
for index := range items {
|
||||
items[index] = "?"
|
||||
}
|
||||
return strings.Join(items, ",")
|
||||
}
|
||||
|
||||
func scanLevelTracks(rows *sql.Rows) ([]domain.Track, error) {
|
||||
items := make([]domain.Track, 0)
|
||||
for rows.Next() {
|
||||
|
||||
@ -58,6 +58,22 @@ func (s *GrowthLevelServer) GetLevelTrack(ctx context.Context, req *activityv1.G
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *GrowthLevelServer) BatchGetUserLevelDisplayProfiles(ctx context.Context, req *activityv1.BatchGetUserLevelDisplayProfilesRequest) (*activityv1.BatchGetUserLevelDisplayProfilesResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
profiles, err := s.svc.BatchGetUserLevelDisplayProfiles(ctx, req.GetUserIds())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &activityv1.BatchGetUserLevelDisplayProfilesResponse{
|
||||
Profiles: make([]*activityv1.UserLevelDisplayProfile, 0, len(profiles.Profiles)),
|
||||
ServerTimeMs: profiles.ServerTimeMS,
|
||||
}
|
||||
for _, item := range profiles.Profiles {
|
||||
resp.Profiles = append(resp.Profiles, userLevelDisplayProfileToProto(item))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *GrowthLevelServer) ListLevelRewards(ctx context.Context, req *activityv1.ListLevelRewardsRequest) (*activityv1.ListLevelRewardsResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
rewards, total, err := s.svc.ListLevelRewards(ctx, domain.RewardQuery{
|
||||
@ -104,6 +120,22 @@ func (s *GrowthLevelServer) ConsumeLevelEvent(ctx context.Context, req *activity
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *GrowthLevelServer) IssueRegistrationLevelBadges(ctx context.Context, req *activityv1.IssueRegistrationLevelBadgesRequest) (*activityv1.IssueRegistrationLevelBadgesResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
grants, err := s.svc.IssueRegistrationLevelBadges(ctx, req.GetUserId(), req.GetCommandId())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &activityv1.IssueRegistrationLevelBadgesResponse{
|
||||
Grants: make([]*activityv1.RegistrationLevelBadgeGrant, 0, len(grants.Grants)),
|
||||
ServerTimeMs: grants.ServerTimeMS,
|
||||
}
|
||||
for _, item := range grants.Grants {
|
||||
resp.Grants = append(resp.Grants, registrationLevelBadgeGrantToProto(item))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AdminGrowthLevelServer 暴露后台等级配置入口,业务约束仍由 service/repository 校验。
|
||||
type AdminGrowthLevelServer struct {
|
||||
activityv1.UnimplementedAdminGrowthLevelServiceServer
|
||||
@ -257,11 +289,33 @@ func levelTrackOverviewToProto(item domain.TrackOverview) *activityv1.LevelTrack
|
||||
NextLevelRequiredValue: item.NextLevelRequiredValue,
|
||||
DisplayAvatarFrameResourceId: item.DisplayAvatarFrameResourceID,
|
||||
DisplayBadgeResourceId: item.DisplayBadgeResourceID,
|
||||
DisplayBadgeSourceLevel: item.DisplayBadgeSourceLevel,
|
||||
RewardPendingCount: item.RewardPendingCount,
|
||||
SortOrder: item.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
func userLevelDisplayProfileToProto(item domain.UserLevelDisplayProfile) *activityv1.UserLevelDisplayProfile {
|
||||
return &activityv1.UserLevelDisplayProfile{
|
||||
UserId: item.UserID,
|
||||
Wealth: levelDisplayTrackProfileToProto(item.Wealth),
|
||||
Game: levelDisplayTrackProfileToProto(item.Game),
|
||||
Charm: levelDisplayTrackProfileToProto(item.Charm),
|
||||
}
|
||||
}
|
||||
|
||||
func levelDisplayTrackProfileToProto(item domain.LevelDisplayTrackProfile) *activityv1.LevelDisplayTrackProfile {
|
||||
return &activityv1.LevelDisplayTrackProfile{
|
||||
Track: item.Track,
|
||||
Level: item.Level,
|
||||
TierId: item.TierID,
|
||||
AvatarFrameResourceId: item.AvatarFrameResourceID,
|
||||
BadgeResourceId: item.BadgeResourceID,
|
||||
BadgeSourceLevel: item.BadgeSourceLevel,
|
||||
UpdatedAtMs: item.UpdatedAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
func levelRewardToProto(item domain.RewardJob) *activityv1.LevelRewardJob {
|
||||
return &activityv1.LevelRewardJob{
|
||||
RewardJobId: item.RewardJobID,
|
||||
@ -279,3 +333,12 @@ func levelRewardToProto(item domain.RewardJob) *activityv1.LevelRewardJob {
|
||||
UpdatedAtMs: item.UpdatedAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
func registrationLevelBadgeGrantToProto(item domain.RegistrationLevelBadgeGrant) *activityv1.RegistrationLevelBadgeGrant {
|
||||
return &activityv1.RegistrationLevelBadgeGrant{
|
||||
Track: item.Track,
|
||||
Level: item.Level,
|
||||
ResourceId: item.ResourceID,
|
||||
GrantId: item.GrantID,
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
type GrowthLevelClient interface {
|
||||
GetMyLevelOverview(ctx context.Context, req *activityv1.GetMyLevelOverviewRequest) (*activityv1.GetMyLevelOverviewResponse, error)
|
||||
GetLevelTrack(ctx context.Context, req *activityv1.GetLevelTrackRequest) (*activityv1.GetLevelTrackResponse, error)
|
||||
BatchGetUserLevelDisplayProfiles(ctx context.Context, req *activityv1.BatchGetUserLevelDisplayProfilesRequest) (*activityv1.BatchGetUserLevelDisplayProfilesResponse, error)
|
||||
ListLevelRewards(ctx context.Context, req *activityv1.ListLevelRewardsRequest) (*activityv1.ListLevelRewardsResponse, error)
|
||||
}
|
||||
|
||||
@ -30,6 +31,10 @@ func (c *grpcGrowthLevelClient) GetLevelTrack(ctx context.Context, req *activity
|
||||
return c.client.GetLevelTrack(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcGrowthLevelClient) BatchGetUserLevelDisplayProfiles(ctx context.Context, req *activityv1.BatchGetUserLevelDisplayProfilesRequest) (*activityv1.BatchGetUserLevelDisplayProfilesResponse, error) {
|
||||
return c.client.BatchGetUserLevelDisplayProfiles(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcGrowthLevelClient) ListLevelRewards(ctx context.Context, req *activityv1.ListLevelRewardsRequest) (*activityv1.ListLevelRewardsResponse, error) {
|
||||
return c.client.ListLevelRewards(ctx, req)
|
||||
}
|
||||
|
||||
@ -74,7 +74,12 @@ type UserHostClient interface {
|
||||
GetCoinSellerProfile(ctx context.Context, req *userv1.GetCoinSellerProfileRequest) (*userv1.GetCoinSellerProfileResponse, error)
|
||||
ListActiveCoinSellersInMyRegion(ctx context.Context, req *userv1.ListActiveCoinSellersInMyRegionRequest) (*userv1.ListActiveCoinSellersInMyRegionResponse, error)
|
||||
GetUserRoleSummary(ctx context.Context, req *userv1.GetUserRoleSummaryRequest) (*userv1.GetUserRoleSummaryResponse, error)
|
||||
GetAgency(ctx context.Context, req *userv1.GetAgencyRequest) (*userv1.GetAgencyResponse, error)
|
||||
CheckBusinessCapability(ctx context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error)
|
||||
GetAgencyMembers(ctx context.Context, req *userv1.GetAgencyMembersRequest) (*userv1.GetAgencyMembersResponse, error)
|
||||
GetAgencyApplications(ctx context.Context, req *userv1.GetAgencyApplicationsRequest) (*userv1.GetAgencyApplicationsResponse, error)
|
||||
ReviewAgencyApplication(ctx context.Context, req *userv1.ReviewAgencyApplicationRequest) (*userv1.ReviewAgencyApplicationResponse, error)
|
||||
KickAgencyHost(ctx context.Context, req *userv1.KickAgencyHostRequest) (*userv1.KickAgencyHostResponse, error)
|
||||
}
|
||||
|
||||
// AppRegistryClient 抽象 gateway 对 user-service App 注册表的解析依赖。
|
||||
@ -330,10 +335,30 @@ func (c *grpcUserHostClient) GetUserRoleSummary(ctx context.Context, req *userv1
|
||||
return c.client.GetUserRoleSummary(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) GetAgency(ctx context.Context, req *userv1.GetAgencyRequest) (*userv1.GetAgencyResponse, error) {
|
||||
return c.client.GetAgency(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) CheckBusinessCapability(ctx context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error) {
|
||||
return c.client.CheckBusinessCapability(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) GetAgencyMembers(ctx context.Context, req *userv1.GetAgencyMembersRequest) (*userv1.GetAgencyMembersResponse, error) {
|
||||
return c.client.GetAgencyMembers(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) GetAgencyApplications(ctx context.Context, req *userv1.GetAgencyApplicationsRequest) (*userv1.GetAgencyApplicationsResponse, error) {
|
||||
return c.client.GetAgencyApplications(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) ReviewAgencyApplication(ctx context.Context, req *userv1.ReviewAgencyApplicationRequest) (*userv1.ReviewAgencyApplicationResponse, error) {
|
||||
return c.client.ReviewAgencyApplication(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcUserHostClient) KickAgencyHost(ctx context.Context, req *userv1.KickAgencyHostRequest) (*userv1.KickAgencyHostResponse, error) {
|
||||
return c.client.KickAgencyHost(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcAppRegistryClient) ResolveApp(ctx context.Context, req *userv1.ResolveAppRequest) (*userv1.ResolveAppResponse, error) {
|
||||
return c.client.ResolveApp(ctx, req)
|
||||
}
|
||||
|
||||
@ -32,6 +32,8 @@ type WalletClient interface {
|
||||
GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error)
|
||||
ListUserResources(ctx context.Context, req *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error)
|
||||
EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error)
|
||||
UnequipUserResource(ctx context.Context, req *walletv1.UnequipUserResourceRequest) (*walletv1.UnequipUserResourceResponse, error)
|
||||
BatchGetUserEquippedResources(ctx context.Context, req *walletv1.BatchGetUserEquippedResourcesRequest) (*walletv1.BatchGetUserEquippedResourcesResponse, error)
|
||||
GetRedPacketConfig(ctx context.Context, req *walletv1.GetRedPacketConfigRequest) (*walletv1.GetRedPacketConfigResponse, error)
|
||||
CreateRedPacket(ctx context.Context, req *walletv1.CreateRedPacketRequest) (*walletv1.CreateRedPacketResponse, error)
|
||||
ClaimRedPacket(ctx context.Context, req *walletv1.ClaimRedPacketRequest) (*walletv1.ClaimRedPacketResponse, error)
|
||||
@ -136,6 +138,14 @@ func (c *grpcWalletClient) EquipUserResource(ctx context.Context, req *walletv1.
|
||||
return c.client.EquipUserResource(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcWalletClient) UnequipUserResource(ctx context.Context, req *walletv1.UnequipUserResourceRequest) (*walletv1.UnequipUserResourceResponse, error) {
|
||||
return c.client.UnequipUserResource(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcWalletClient) BatchGetUserEquippedResources(ctx context.Context, req *walletv1.BatchGetUserEquippedResourcesRequest) (*walletv1.BatchGetUserEquippedResourcesResponse, error) {
|
||||
return c.client.BatchGetUserEquippedResources(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcWalletClient) GetRedPacketConfig(ctx context.Context, req *walletv1.GetRedPacketConfigRequest) (*walletv1.GetRedPacketConfigResponse, error) {
|
||||
return c.client.GetRedPacketConfig(ctx, req)
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ type levelTrackOverviewData struct {
|
||||
NextLevelRequiredValue int64 `json:"next_level_required_value"`
|
||||
DisplayAvatarFrameResourceID int64 `json:"display_avatar_frame_resource_id"`
|
||||
DisplayBadgeResourceID int64 `json:"display_badge_resource_id"`
|
||||
DisplayBadgeSourceLevel int32 `json:"display_badge_source_level"`
|
||||
RewardPendingCount int64 `json:"reward_pending_count"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
}
|
||||
@ -190,6 +191,7 @@ func levelTrackOverviewFromProto(item *activityv1.LevelTrackOverview) levelTrack
|
||||
NextLevelRequiredValue: item.GetNextLevelRequiredValue(),
|
||||
DisplayAvatarFrameResourceID: item.GetDisplayAvatarFrameResourceId(),
|
||||
DisplayBadgeResourceID: item.GetDisplayBadgeResourceId(),
|
||||
DisplayBadgeSourceLevel: item.GetDisplayBadgeSourceLevel(),
|
||||
RewardPendingCount: item.GetRewardPendingCount(),
|
||||
SortOrder: item.GetSortOrder(),
|
||||
}
|
||||
|
||||
@ -62,30 +62,38 @@ type AppHandlers struct {
|
||||
}
|
||||
|
||||
type UserHandlers struct {
|
||||
ResolveDisplayUserID http.HandlerFunc
|
||||
BatchUserProfiles http.HandlerFunc
|
||||
BatchRoomDisplayProfiles http.HandlerFunc
|
||||
GetMyOverview http.HandlerFunc
|
||||
GetMyGiftWall http.HandlerFunc
|
||||
GetMyIdentity http.HandlerFunc
|
||||
GetMyHostIdentity http.HandlerFunc
|
||||
GetMyRoleSummary http.HandlerFunc
|
||||
SearchHostAgencies http.HandlerFunc
|
||||
ApplyToHostAgency http.HandlerFunc
|
||||
CompleteMyOnboarding http.HandlerFunc
|
||||
UpdateMyProfile http.HandlerFunc
|
||||
ChangeMyCountry http.HandlerFunc
|
||||
ChangeMyDisplayUserID http.HandlerFunc
|
||||
ApplyMyPrettyDisplayUserID http.HandlerFunc
|
||||
ListMyProfileVisitors http.HandlerFunc
|
||||
ListMyFollowing http.HandlerFunc
|
||||
ListMyFriends http.HandlerFunc
|
||||
ListMyFriendApplications http.HandlerFunc
|
||||
GetMyProfile http.HandlerFunc
|
||||
ListMyResources http.HandlerFunc
|
||||
EquipMyResource http.HandlerFunc
|
||||
UserSocialAction http.HandlerFunc
|
||||
SubmitReport http.HandlerFunc
|
||||
ResolveDisplayUserID http.HandlerFunc
|
||||
BatchUserProfiles http.HandlerFunc
|
||||
BatchRoomDisplayProfiles http.HandlerFunc
|
||||
GetMyOverview http.HandlerFunc
|
||||
GetMyGiftWall http.HandlerFunc
|
||||
GetMyIdentity http.HandlerFunc
|
||||
GetMyHostIdentity http.HandlerFunc
|
||||
GetMyRoleSummary http.HandlerFunc
|
||||
SearchHostAgencies http.HandlerFunc
|
||||
ApplyToHostAgency http.HandlerFunc
|
||||
GetAgencyCenterOverview http.HandlerFunc
|
||||
ListAgencyCenterHosts http.HandlerFunc
|
||||
ListAgencyCenterApplications http.HandlerFunc
|
||||
ReviewAgencyCenterApplication http.HandlerFunc
|
||||
RemoveAgencyCenterHost http.HandlerFunc
|
||||
CompleteMyOnboarding http.HandlerFunc
|
||||
UpdateMyProfile http.HandlerFunc
|
||||
ChangeMyCountry http.HandlerFunc
|
||||
ChangeMyDisplayUserID http.HandlerFunc
|
||||
ApplyMyPrettyDisplayUserID http.HandlerFunc
|
||||
ListMyProfileVisitors http.HandlerFunc
|
||||
ListMyFollowing http.HandlerFunc
|
||||
ListMyFriends http.HandlerFunc
|
||||
ListMyFriendApplications http.HandlerFunc
|
||||
GetMyProfile http.HandlerFunc
|
||||
GetMyAppearance http.HandlerFunc
|
||||
GetUserAppearance http.HandlerFunc
|
||||
ListMyResources http.HandlerFunc
|
||||
EquipMyResource http.HandlerFunc
|
||||
UnequipMyResource http.HandlerFunc
|
||||
UserSocialAction http.HandlerFunc
|
||||
SubmitReport http.HandlerFunc
|
||||
}
|
||||
|
||||
type ManagerHandlers struct {
|
||||
@ -292,6 +300,11 @@ func (r routes) registerUserRoutes() {
|
||||
r.profile("/users/me/role-summary", "", h.GetMyRoleSummary)
|
||||
r.profile("/host/agencies/search", http.MethodGet, h.SearchHostAgencies)
|
||||
r.profile("/host/agency-applications", http.MethodPost, h.ApplyToHostAgency)
|
||||
r.profile("/agency-center/overview", http.MethodGet, h.GetAgencyCenterOverview)
|
||||
r.profile("/agency-center/hosts", http.MethodGet, h.ListAgencyCenterHosts)
|
||||
r.profile("/agency-center/applications", http.MethodGet, h.ListAgencyCenterApplications)
|
||||
r.profile("/agency-center/applications/{application_id}/review", http.MethodPost, h.ReviewAgencyCenterApplication)
|
||||
r.profile("/agency-center/hosts/{host_user_id}/remove", http.MethodPost, h.RemoveAgencyCenterHost)
|
||||
r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding)
|
||||
r.profile("/users/me/profile/update", "", h.UpdateMyProfile)
|
||||
r.profile("/users/me/country/change", "", h.ChangeMyCountry)
|
||||
@ -301,9 +314,12 @@ func (r routes) registerUserRoutes() {
|
||||
r.profile("/users/me/following", http.MethodGet, h.ListMyFollowing)
|
||||
r.profile("/users/me/friends", http.MethodGet, h.ListMyFriends)
|
||||
r.profile("/users/me/friend-requests", http.MethodGet, h.ListMyFriendApplications)
|
||||
r.profile("/appearance", http.MethodGet, h.GetMyAppearance)
|
||||
r.profile("/users/me/appearance", http.MethodGet, h.GetMyAppearance)
|
||||
r.profile("/users/me", "", h.GetMyProfile)
|
||||
r.profile("/users/me/resources", "", h.ListMyResources)
|
||||
r.profile("/users/me/resources/{resource_id}/equip", "", h.EquipMyResource)
|
||||
r.profile("/users/me/resources/{resource_type}/unequip", http.MethodDelete, h.UnequipMyResource)
|
||||
r.profile("/reports", http.MethodPost, h.SubmitReport)
|
||||
r.profile("/users/{user_id}/{action...}", "", h.UserSocialAction)
|
||||
}
|
||||
|
||||
@ -55,3 +55,7 @@ func (h *Handler) ListMyResources(writer http.ResponseWriter, request *http.Requ
|
||||
func (h *Handler) EquipMyResource(writer http.ResponseWriter, request *http.Request) {
|
||||
h.equipMyResource(writer, request)
|
||||
}
|
||||
|
||||
func (h *Handler) UnequipMyResource(writer http.ResponseWriter, request *http.Request) {
|
||||
h.unequipMyResource(writer, request)
|
||||
}
|
||||
|
||||
@ -498,6 +498,37 @@ func (h *Handler) equipMyResource(writer http.ResponseWriter, request *http.Requ
|
||||
httpkit.WriteOK(writer, request, userResourceFromProto(resp.GetResource()))
|
||||
}
|
||||
|
||||
func (h *Handler) unequipMyResource(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Method != http.MethodDelete {
|
||||
httpkit.WriteError(writer, request, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
if h.walletClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
resourceType := strings.TrimSpace(request.PathValue("resource_type"))
|
||||
if resourceType == "" {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
resp, err := h.walletClient.UnequipUserResource(request.Context(), &walletv1.UnequipUserResourceRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
UserId: auth.UserIDFromContext(request.Context()),
|
||||
ResourceType: resourceType,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
httpkit.WriteOK(writer, request, map[string]any{
|
||||
"resource_type": resp.GetResourceType(),
|
||||
"unequipped": resp.GetUnequipped(),
|
||||
"updated_at_ms": resp.GetUpdatedAtMs(),
|
||||
})
|
||||
}
|
||||
|
||||
func resourceFromProto(item *walletv1.Resource) resourceData {
|
||||
if item == nil {
|
||||
return resourceData{}
|
||||
|
||||
@ -404,64 +404,84 @@ type fakeUserHostClient struct {
|
||||
lastRoleSummary *userv1.GetUserRoleSummaryRequest
|
||||
roleSummary *userv1.UserRoleSummary
|
||||
roleSummaryErr error
|
||||
lastGetAgency *userv1.GetAgencyRequest
|
||||
getAgencyResp *userv1.GetAgencyResponse
|
||||
getAgencyErr error
|
||||
lastMembers *userv1.GetAgencyMembersRequest
|
||||
membersResp *userv1.GetAgencyMembersResponse
|
||||
membersErr error
|
||||
lastApplications *userv1.GetAgencyApplicationsRequest
|
||||
applicationsResp *userv1.GetAgencyApplicationsResponse
|
||||
applicationsErr error
|
||||
lastReview *userv1.ReviewAgencyApplicationRequest
|
||||
reviewResp *userv1.ReviewAgencyApplicationResponse
|
||||
reviewErr error
|
||||
lastKick *userv1.KickAgencyHostRequest
|
||||
kickResp *userv1.KickAgencyHostResponse
|
||||
kickErr error
|
||||
lastCapability *userv1.CheckBusinessCapabilityRequest
|
||||
capabilityResp *userv1.CheckBusinessCapabilityResponse
|
||||
capabilityErr error
|
||||
}
|
||||
|
||||
type fakeWalletClient struct {
|
||||
last *walletv1.GetBalancesRequest
|
||||
resp *walletv1.GetBalancesResponse
|
||||
err error
|
||||
lastOverview *walletv1.GetWalletOverviewRequest
|
||||
overviewResp *walletv1.GetWalletOverviewResponse
|
||||
overviewErr error
|
||||
lastValueSummary *walletv1.GetWalletValueSummaryRequest
|
||||
valueSummaryResp *walletv1.GetWalletValueSummaryResponse
|
||||
valueSummaryErr error
|
||||
lastGiftWall *walletv1.GetUserGiftWallRequest
|
||||
giftWallResp *walletv1.GetUserGiftWallResponse
|
||||
lastRechargeProducts *walletv1.ListRechargeProductsRequest
|
||||
rechargeProductsResp *walletv1.ListRechargeProductsResponse
|
||||
lastGoogleConfirm *walletv1.ConfirmGooglePaymentRequest
|
||||
googleConfirmResp *walletv1.ConfirmGooglePaymentResponse
|
||||
lastDiamondExchange *walletv1.GetDiamondExchangeConfigRequest
|
||||
diamondExchangeResp *walletv1.GetDiamondExchangeConfigResponse
|
||||
lastTransactions *walletv1.ListWalletTransactionsRequest
|
||||
transactionsResp *walletv1.ListWalletTransactionsResponse
|
||||
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
|
||||
lastListResources *walletv1.ListResourcesRequest
|
||||
listResourcesResp *walletv1.ListResourcesResponse
|
||||
lastGetResource *walletv1.GetResourceRequest
|
||||
resourcesByID map[int64]*walletv1.Resource
|
||||
lastGetResourceGroup *walletv1.GetResourceGroupRequest
|
||||
resourceGroupsByID map[int64]*walletv1.ResourceGroup
|
||||
lastResourceShop *walletv1.ListResourceShopItemsRequest
|
||||
resourceShopResp *walletv1.ListResourceShopItemsResponse
|
||||
lastPurchaseShop *walletv1.PurchaseResourceShopItemRequest
|
||||
purchaseShopResp *walletv1.PurchaseResourceShopItemResponse
|
||||
lastListGiftConfigs *walletv1.ListGiftConfigsRequest
|
||||
listGiftConfigsResp *walletv1.ListGiftConfigsResponse
|
||||
lastListGiftTypes *walletv1.ListGiftTypeConfigsRequest
|
||||
listGiftTypesResp *walletv1.ListGiftTypeConfigsResponse
|
||||
lastGrantResource *walletv1.GrantResourceRequest
|
||||
grantResourceResp *walletv1.ResourceGrantResponse
|
||||
grantResourceErr error
|
||||
lastListRedPackets *walletv1.ListRedPacketsRequest
|
||||
listRedPacketsResp *walletv1.ListRedPacketsResponse
|
||||
lastGetRedPacket *walletv1.GetRedPacketRequest
|
||||
getRedPacketResp *walletv1.GetRedPacketResponse
|
||||
lastClaimRedPacket *walletv1.ClaimRedPacketRequest
|
||||
claimRedPacketResp *walletv1.ClaimRedPacketResponse
|
||||
last *walletv1.GetBalancesRequest
|
||||
balanceRequests []*walletv1.GetBalancesRequest
|
||||
resp *walletv1.GetBalancesResponse
|
||||
balancesByUserID map[int64]*walletv1.GetBalancesResponse
|
||||
err error
|
||||
lastOverview *walletv1.GetWalletOverviewRequest
|
||||
overviewResp *walletv1.GetWalletOverviewResponse
|
||||
overviewErr error
|
||||
lastValueSummary *walletv1.GetWalletValueSummaryRequest
|
||||
valueSummaryResp *walletv1.GetWalletValueSummaryResponse
|
||||
valueSummaryErr error
|
||||
lastGiftWall *walletv1.GetUserGiftWallRequest
|
||||
giftWallResp *walletv1.GetUserGiftWallResponse
|
||||
lastRechargeProducts *walletv1.ListRechargeProductsRequest
|
||||
rechargeProductsResp *walletv1.ListRechargeProductsResponse
|
||||
lastGoogleConfirm *walletv1.ConfirmGooglePaymentRequest
|
||||
googleConfirmResp *walletv1.ConfirmGooglePaymentResponse
|
||||
lastDiamondExchange *walletv1.GetDiamondExchangeConfigRequest
|
||||
diamondExchangeResp *walletv1.GetDiamondExchangeConfigResponse
|
||||
lastTransactions *walletv1.ListWalletTransactionsRequest
|
||||
transactionsResp *walletv1.ListWalletTransactionsResponse
|
||||
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
|
||||
lastListResources *walletv1.ListResourcesRequest
|
||||
listResourcesResp *walletv1.ListResourcesResponse
|
||||
lastGetResource *walletv1.GetResourceRequest
|
||||
resourcesByID map[int64]*walletv1.Resource
|
||||
lastGetResourceGroup *walletv1.GetResourceGroupRequest
|
||||
resourceGroupsByID map[int64]*walletv1.ResourceGroup
|
||||
lastResourceShop *walletv1.ListResourceShopItemsRequest
|
||||
resourceShopResp *walletv1.ListResourceShopItemsResponse
|
||||
lastPurchaseShop *walletv1.PurchaseResourceShopItemRequest
|
||||
purchaseShopResp *walletv1.PurchaseResourceShopItemResponse
|
||||
lastListGiftConfigs *walletv1.ListGiftConfigsRequest
|
||||
listGiftConfigsResp *walletv1.ListGiftConfigsResponse
|
||||
lastListGiftTypes *walletv1.ListGiftTypeConfigsRequest
|
||||
listGiftTypesResp *walletv1.ListGiftTypeConfigsResponse
|
||||
lastGrantResource *walletv1.GrantResourceRequest
|
||||
grantResourceResp *walletv1.ResourceGrantResponse
|
||||
grantResourceErr error
|
||||
lastBatchEquipped *walletv1.BatchGetUserEquippedResourcesRequest
|
||||
batchEquippedRequests []*walletv1.BatchGetUserEquippedResourcesRequest
|
||||
batchEquippedResp *walletv1.BatchGetUserEquippedResourcesResponse
|
||||
lastListRedPackets *walletv1.ListRedPacketsRequest
|
||||
listRedPacketsResp *walletv1.ListRedPacketsResponse
|
||||
lastGetRedPacket *walletv1.GetRedPacketRequest
|
||||
getRedPacketResp *walletv1.GetRedPacketResponse
|
||||
lastClaimRedPacket *walletv1.ClaimRedPacketRequest
|
||||
claimRedPacketResp *walletv1.ClaimRedPacketResponse
|
||||
}
|
||||
|
||||
type fakeMessageInboxClient struct {
|
||||
@ -638,6 +658,12 @@ func (f *fakeUserProfileClient) BatchGetUsers(_ context.Context, req *userv1.Bat
|
||||
f.lastBatch = req
|
||||
users := make(map[int64]*userv1.User, len(req.GetUserIds()))
|
||||
for _, userID := range req.GetUserIds() {
|
||||
if f.usersByID != nil {
|
||||
if user, ok := f.usersByID[userID]; ok {
|
||||
users[userID] = user
|
||||
continue
|
||||
}
|
||||
}
|
||||
users[userID] = &userv1.User{
|
||||
UserId: userID,
|
||||
DisplayUserId: strconv.FormatInt(100000+userID, 10),
|
||||
@ -1102,6 +1128,17 @@ func (f *fakeUserHostClient) GetUserRoleSummary(_ context.Context, req *userv1.G
|
||||
return &userv1.GetUserRoleSummaryResponse{Summary: &userv1.UserRoleSummary{UserId: req.GetUserId()}}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) GetAgency(_ context.Context, req *userv1.GetAgencyRequest) (*userv1.GetAgencyResponse, error) {
|
||||
f.lastGetAgency = req
|
||||
if f.getAgencyErr != nil {
|
||||
return nil, f.getAgencyErr
|
||||
}
|
||||
if f.getAgencyResp != nil {
|
||||
return f.getAgencyResp, nil
|
||||
}
|
||||
return &userv1.GetAgencyResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) CheckBusinessCapability(_ context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error) {
|
||||
f.lastCapability = req
|
||||
if f.capabilityErr != nil {
|
||||
@ -1113,11 +1150,61 @@ func (f *fakeUserHostClient) CheckBusinessCapability(_ context.Context, req *use
|
||||
return &userv1.CheckBusinessCapabilityResponse{Allowed: true}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) GetAgencyMembers(_ context.Context, req *userv1.GetAgencyMembersRequest) (*userv1.GetAgencyMembersResponse, error) {
|
||||
f.lastMembers = req
|
||||
if f.membersErr != nil {
|
||||
return nil, f.membersErr
|
||||
}
|
||||
if f.membersResp != nil {
|
||||
return f.membersResp, nil
|
||||
}
|
||||
return &userv1.GetAgencyMembersResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) GetAgencyApplications(_ context.Context, req *userv1.GetAgencyApplicationsRequest) (*userv1.GetAgencyApplicationsResponse, error) {
|
||||
f.lastApplications = req
|
||||
if f.applicationsErr != nil {
|
||||
return nil, f.applicationsErr
|
||||
}
|
||||
if f.applicationsResp != nil {
|
||||
return f.applicationsResp, nil
|
||||
}
|
||||
return &userv1.GetAgencyApplicationsResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) ReviewAgencyApplication(_ context.Context, req *userv1.ReviewAgencyApplicationRequest) (*userv1.ReviewAgencyApplicationResponse, error) {
|
||||
f.lastReview = req
|
||||
if f.reviewErr != nil {
|
||||
return nil, f.reviewErr
|
||||
}
|
||||
if f.reviewResp != nil {
|
||||
return f.reviewResp, nil
|
||||
}
|
||||
return &userv1.ReviewAgencyApplicationResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserHostClient) KickAgencyHost(_ context.Context, req *userv1.KickAgencyHostRequest) (*userv1.KickAgencyHostResponse, error) {
|
||||
f.lastKick = req
|
||||
if f.kickErr != nil {
|
||||
return nil, f.kickErr
|
||||
}
|
||||
if f.kickResp != nil {
|
||||
return f.kickResp, nil
|
||||
}
|
||||
return &userv1.KickAgencyHostResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeWalletClient) GetBalances(_ context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
|
||||
f.last = req
|
||||
f.balanceRequests = append(f.balanceRequests, req)
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
if f.balancesByUserID != nil {
|
||||
if resp, ok := f.balancesByUserID[req.GetUserId()]; ok {
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
if f.resp != nil {
|
||||
return f.resp, nil
|
||||
}
|
||||
@ -1368,6 +1455,22 @@ func (f *fakeWalletClient) EquipUserResource(context.Context, *walletv1.EquipUse
|
||||
return &walletv1.EquipUserResourceResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeWalletClient) UnequipUserResource(context.Context, *walletv1.UnequipUserResourceRequest) (*walletv1.UnequipUserResourceResponse, error) {
|
||||
return &walletv1.UnequipUserResourceResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeWalletClient) BatchGetUserEquippedResources(_ context.Context, req *walletv1.BatchGetUserEquippedResourcesRequest) (*walletv1.BatchGetUserEquippedResourcesResponse, error) {
|
||||
f.lastBatchEquipped = req
|
||||
f.batchEquippedRequests = append(f.batchEquippedRequests, req)
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
if f.batchEquippedResp != nil {
|
||||
return f.batchEquippedResp, nil
|
||||
}
|
||||
return &walletv1.BatchGetUserEquippedResourcesResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeWalletClient) GetRedPacketConfig(context.Context, *walletv1.GetRedPacketConfigRequest) (*walletv1.GetRedPacketConfigResponse, error) {
|
||||
return &walletv1.GetRedPacketConfigResponse{}, nil
|
||||
}
|
||||
@ -1724,14 +1827,17 @@ func TestSendGiftResponseIncludesLuckyGiftDraw(t *testing.T) {
|
||||
GiftId: "rose",
|
||||
RuleVersion: 12,
|
||||
ExperiencePool: "novice",
|
||||
SelectedTierId: "novice_2x",
|
||||
MultiplierPpm: 2_000_000,
|
||||
EffectiveRewardCoins: 200,
|
||||
SelectedTierId: "batch",
|
||||
MultiplierPpm: 5_000_000,
|
||||
EffectiveRewardCoins: 500,
|
||||
RewardStatus: "granted",
|
||||
WalletTransactionId: "wallet_tx_lucky",
|
||||
CoinBalanceAfter: 8800,
|
||||
CreatedAtMs: 1_779_258_000_000,
|
||||
},
|
||||
LuckyGifts: []*roomv1.LuckyGiftDrawResult{
|
||||
{Enabled: true, DrawId: "lucky_draw_1", CommandId: "cmd-gift-lucky:target:43", PoolId: "super_lucky", GiftId: "rose", MultiplierPpm: 2_000_000, EffectiveRewardCoins: 200, RewardStatus: "granted", WalletTransactionId: "wallet_tx_lucky_1", TargetUserId: 43},
|
||||
{Enabled: true, DrawId: "lucky_draw_2", CommandId: "cmd-gift-lucky:target:44", PoolId: "super_lucky", GiftId: "rose", MultiplierPpm: 3_000_000, EffectiveRewardCoins: 300, RewardStatus: "granted", WalletTransactionId: "wallet_tx_lucky_2", TargetUserId: 44},
|
||||
},
|
||||
}}
|
||||
router := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}).Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-gift-lucky","target_user_id":43,"gift_id":"rose","gift_count":1,"pool_id":"super_lucky"}`)))
|
||||
@ -1755,14 +1861,22 @@ func TestSendGiftResponseIncludesLuckyGiftDraw(t *testing.T) {
|
||||
WalletTransactionID string `json:"wallet_transaction_id"`
|
||||
CoinBalanceAfter int64 `json:"coin_balance_after"`
|
||||
} `json:"lucky_gift"`
|
||||
LuckyGifts []struct {
|
||||
TargetUserID int64 `json:"target_user_id"`
|
||||
MultiplierPPM int64 `json:"multiplier_ppm"`
|
||||
EffectiveRewardCoins int64 `json:"effective_reward_coins"`
|
||||
} `json:"lucky_gifts"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("decode response failed: %v body=%s", err, recorder.Body.String())
|
||||
}
|
||||
if envelope.Code != "OK" || envelope.Data.LuckyGift.DrawID != "lucky_draw_test" || envelope.Data.LuckyGift.PoolID != "super_lucky" || envelope.Data.LuckyGift.MultiplierPPM != 2_000_000 || envelope.Data.LuckyGift.EffectiveRewardCoins != 200 || envelope.Data.LuckyGift.RewardStatus != "granted" || envelope.Data.LuckyGift.WalletTransactionID != "wallet_tx_lucky" || envelope.Data.LuckyGift.CoinBalanceAfter != 8800 {
|
||||
if envelope.Code != "OK" || envelope.Data.LuckyGift.DrawID != "lucky_draw_test" || envelope.Data.LuckyGift.PoolID != "super_lucky" || envelope.Data.LuckyGift.MultiplierPPM != 5_000_000 || envelope.Data.LuckyGift.EffectiveRewardCoins != 500 || envelope.Data.LuckyGift.RewardStatus != "granted" || envelope.Data.LuckyGift.WalletTransactionID != "" || envelope.Data.LuckyGift.CoinBalanceAfter != 8800 {
|
||||
t.Fatalf("lucky_gift response mismatch: %+v", envelope.Data.LuckyGift)
|
||||
}
|
||||
if len(envelope.Data.LuckyGifts) != 2 || envelope.Data.LuckyGifts[0].TargetUserID != 43 || envelope.Data.LuckyGifts[0].MultiplierPPM != 2_000_000 || envelope.Data.LuckyGifts[1].TargetUserID != 44 || envelope.Data.LuckyGifts[1].MultiplierPPM != 3_000_000 {
|
||||
t.Fatalf("lucky_gifts response mismatch: %+v", envelope.Data.LuckyGifts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendGiftInjectsActiveHostPeriodScope(t *testing.T) {
|
||||
@ -1808,6 +1922,23 @@ func TestSendGiftDoesNotInjectHostPeriodScopeForNonHost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendGiftForwardsMultipleTargetUserIDs(t *testing.T) {
|
||||
roomClient := &fakeRoomClient{}
|
||||
router := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}).Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-gift-multi","target_user_ids":[43,44],"gift_id":"rose","gift_count":2}`)))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
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 roomClient.lastGift == nil || roomClient.lastGift.GetTargetUserId() != 43 || len(roomClient.lastGift.GetTargetUserIds()) != 2 || roomClient.lastGift.GetTargetUserIds()[0] != 43 || roomClient.lastGift.GetTargetUserIds()[1] != 44 {
|
||||
t.Fatalf("gateway must forward all target_user_ids: %+v", roomClient.lastGift)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T) {
|
||||
previousNow := roomapi.TimeNow
|
||||
roomapi.TimeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
||||
@ -1832,8 +1963,55 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
||||
RoomShortId: "100101",
|
||||
},
|
||||
}}
|
||||
profileCardResource := &walletv1.Resource{
|
||||
ResourceId: 8104,
|
||||
ResourceCode: "profile_card_gold",
|
||||
ResourceType: "profile_card",
|
||||
Name: "Gold Profile Card",
|
||||
AssetUrl: "https://cdn.example/profile-card.png",
|
||||
AnimationUrl: "https://cdn.example/profile-card.mp4",
|
||||
MetadataJson: `{"profile_card_layout":{"source_width":1136,"source_height":1680,"content_top":143,"content_bottom":1667,"content_height":1525}}`,
|
||||
GrantStrategy: "extend_expiry",
|
||||
Status: "active",
|
||||
}
|
||||
shortBadgeResource := &walletv1.Resource{
|
||||
ResourceId: 8105,
|
||||
ResourceCode: "badge_tile_gold",
|
||||
ResourceType: "badge",
|
||||
Name: "Gold Tile Badge",
|
||||
PreviewUrl: "https://cdn.example/badge-tile.png",
|
||||
MetadataJson: `{"badge_form":"tile","badge_kind":"normal"}`,
|
||||
GrantStrategy: "set_active_flag",
|
||||
Status: "active",
|
||||
}
|
||||
walletClient := &fakeWalletClient{
|
||||
batchEquippedResp: &walletv1.BatchGetUserEquippedResourcesResponse{
|
||||
Users: []*walletv1.UserEquippedResources{{
|
||||
UserId: 42,
|
||||
Resources: []*walletv1.UserResourceEntitlement{
|
||||
{
|
||||
UserId: 42,
|
||||
EntitlementId: "ent-profile-card",
|
||||
ResourceId: 8104,
|
||||
Resource: profileCardResource,
|
||||
Equipped: true,
|
||||
ExpiresAtMs: 1999999999999,
|
||||
},
|
||||
{
|
||||
UserId: 42,
|
||||
EntitlementId: "ent-badge-tile",
|
||||
ResourceId: 8105,
|
||||
Resource: shortBadgeResource,
|
||||
Equipped: true,
|
||||
ExpiresAtMs: 1999999999999,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
profileClient := &fakeUserProfileClient{}
|
||||
handler := NewHandlerWithConfig(roomClient, nil, nil, nil, TencentIMConfig{}, profileClient)
|
||||
handler.SetWalletClient(walletClient)
|
||||
handler.SetTencentRTC(TencentRTCConfig{
|
||||
Enabled: true,
|
||||
SDKAppID: 1400000000,
|
||||
@ -1885,6 +2063,43 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
||||
if profileClient.lastBatch == nil || strings.Join(int64SliceToStrings(profileClient.lastBatch.GetUserIds()), ",") != "101,42,102,301" {
|
||||
t.Fatalf("join response must batch only first-screen profile users: %+v", profileClient.lastBatch)
|
||||
}
|
||||
hasRoomDisplayResourceQuery := false
|
||||
for _, req := range walletClient.batchEquippedRequests {
|
||||
if strings.Join(req.GetResourceTypes(), ",") == "avatar_frame,profile_card,vehicle,badge" {
|
||||
hasRoomDisplayResourceQuery = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRoomDisplayResourceQuery {
|
||||
t.Fatalf("join response must batch room wearable resources including profile_card: %+v", walletClient.batchEquippedRequests)
|
||||
}
|
||||
profiles, profilesOK := data["profiles"].([]any)
|
||||
if !profilesOK || len(profiles) == 0 {
|
||||
t.Fatalf("join response must include room display profiles: %+v", data["profiles"])
|
||||
}
|
||||
var currentProfile map[string]any
|
||||
for _, item := range profiles {
|
||||
profile, ok := item.(map[string]any)
|
||||
if !ok || profile["user_id"] != "42" {
|
||||
continue
|
||||
}
|
||||
currentProfile = profile
|
||||
break
|
||||
}
|
||||
profileCard, profileCardOK := currentProfile["profile_card"].(map[string]any)
|
||||
metadataJSON, _ := profileCard["metadata_json"].(string)
|
||||
if !profileCardOK || profileCard["resource_id"] != float64(8104) || !strings.Contains(metadataJSON, `"content_top":143`) {
|
||||
t.Fatalf("join profile must include profile_card metadata_json: %+v", currentProfile)
|
||||
}
|
||||
badges, badgesOK := currentProfile["badges"].([]any)
|
||||
if !badgesOK || len(badges) != 1 {
|
||||
t.Fatalf("join profile must include equipped badge list: %+v", currentProfile["badges"])
|
||||
}
|
||||
badge := badges[0].(map[string]any)
|
||||
badgeResource := badge["resource"].(map[string]any)
|
||||
if badge["badge_form"] != "tile" || badgeResource["preview_url"] != "https://cdn.example/badge-tile.png" {
|
||||
t.Fatalf("join profile must include equipped tile badge resource: %+v", badge)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
|
||||
@ -3048,6 +3263,182 @@ func TestApplyToHostAgencyPropagatesClientCommandID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgencyCenterOverviewUsesOwnerAgencySalaryAndPendingApplications(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{
|
||||
roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: true, AgencyId: 7001},
|
||||
getAgencyResp: &userv1.GetAgencyResponse{Agency: &userv1.Agency{
|
||||
AgencyId: 7001,
|
||||
OwnerUserId: 42,
|
||||
RegionId: 30,
|
||||
ParentBdUserId: 77,
|
||||
Name: "Yumi Star Agency",
|
||||
Status: "active",
|
||||
HostCount: 3,
|
||||
}},
|
||||
applicationsResp: &userv1.GetAgencyApplicationsResponse{Applications: []*userv1.AgencyApplication{
|
||||
{ApplicationId: 9001, ApplicantUserId: 100, AgencyId: 7001, Status: "pending"},
|
||||
{ApplicationId: 9002, ApplicantUserId: 101, AgencyId: 7001, Status: "pending"},
|
||||
}},
|
||||
}
|
||||
walletClient := &fakeWalletClient{balancesByUserID: map[int64]*walletv1.GetBalancesResponse{
|
||||
42: {Balances: []*walletv1.AssetBalance{{AssetType: "AGENCY_SALARY_USD", AvailableAmount: 1288075}}},
|
||||
}}
|
||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
||||
77: {UserId: 77, DisplayUserId: "880001", Username: "Mila BD", Avatar: "https://cdn.example/bd.png"},
|
||||
}}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||
handler.SetUserHostClient(hostClient)
|
||||
handler.SetWalletClient(walletClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/agency-center/overview", nil)
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
request.Header.Set("X-Request-ID", "req-agency-center-overview")
|
||||
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 hostClient.lastRoleSummary == nil || hostClient.lastRoleSummary.GetUserId() != 42 || hostClient.lastGetAgency == nil || hostClient.lastGetAgency.GetAgencyId() != 7001 {
|
||||
t.Fatalf("agency owner resolution mismatch: role=%+v agency=%+v", hostClient.lastRoleSummary, hostClient.lastGetAgency)
|
||||
}
|
||||
if walletClient.last == nil || walletClient.last.GetUserId() != 42 || len(walletClient.last.GetAssetTypes()) != 1 || walletClient.last.GetAssetTypes()[0] != "AGENCY_SALARY_USD" {
|
||||
t.Fatalf("agency salary request mismatch: %+v", walletClient.last)
|
||||
}
|
||||
if hostClient.lastApplications == nil || hostClient.lastApplications.GetAgencyId() != 7001 || hostClient.lastApplications.GetStatus() != "pending" {
|
||||
t.Fatalf("pending applications request mismatch: %+v", hostClient.lastApplications)
|
||||
}
|
||||
var response httpkit.ResponseEnvelope
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
data := response.Data.(map[string]any)
|
||||
salary := data["salary"].(map[string]any)
|
||||
bd := data["bd"].(map[string]any)
|
||||
if salary["asset_type"] != "AGENCY_SALARY_USD" || salary["available_amount"] != float64(1288075) || salary["display_amount"] != 12880.75 || data["pending_application_count"] != float64(2) || bd["display_user_id"] != "880001" {
|
||||
t.Fatalf("overview response mismatch: %+v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgencyCenterOverviewRejectsNonOwner(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: false, AgencyId: 7001}}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetUserHostClient(hostClient)
|
||||
handler.SetWalletClient(&fakeWalletClient{})
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/agency-center/overview", nil)
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusForbidden {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var response httpkit.ResponseEnvelope
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
if response.Code != httpkit.CodePermissionDenied || hostClient.lastGetAgency != nil {
|
||||
t.Fatalf("non-owner response mismatch: response=%+v getAgency=%+v", response, hostClient.lastGetAgency)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgencyCenterHostsReturnsActiveHostSalary(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{
|
||||
roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: true, AgencyId: 7001},
|
||||
getAgencyResp: &userv1.GetAgencyResponse{Agency: &userv1.Agency{AgencyId: 7001, OwnerUserId: 42, Status: "active"}},
|
||||
membersResp: &userv1.GetAgencyMembersResponse{Memberships: []*userv1.AgencyMembership{
|
||||
{MembershipId: 1, AgencyId: 7001, HostUserId: 42, MembershipType: "owner", Status: "active", JoinedAtMs: 10},
|
||||
{MembershipId: 2, AgencyId: 7001, HostUserId: 100, MembershipType: "host", Status: "active", JoinedAtMs: 20},
|
||||
{MembershipId: 3, AgencyId: 7001, HostUserId: 101, MembershipType: "host", Status: "active", JoinedAtMs: 30},
|
||||
}},
|
||||
}
|
||||
walletClient := &fakeWalletClient{balancesByUserID: map[int64]*walletv1.GetBalancesResponse{
|
||||
100: {Balances: []*walletv1.AssetBalance{{AssetType: "HOST_SALARY_USD", AvailableAmount: 305025}}},
|
||||
101: {Balances: []*walletv1.AssetBalance{{AssetType: "HOST_SALARY_USD", AvailableAmount: 428050}}},
|
||||
}}
|
||||
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
||||
100: {UserId: 100, DisplayUserId: "163100", Username: "Lina_voice", Avatar: "https://cdn.example/lina.png"},
|
||||
101: {UserId: 101, DisplayUserId: "163101", Username: "Nora_live", Avatar: "https://cdn.example/nora.png"},
|
||||
}}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||
handler.SetUserHostClient(hostClient)
|
||||
handler.SetWalletClient(walletClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/agency-center/hosts", nil)
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
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 hostClient.lastMembers == nil || hostClient.lastMembers.GetAgencyId() != 7001 || hostClient.lastMembers.GetStatus() != "active" {
|
||||
t.Fatalf("members request mismatch: %+v", hostClient.lastMembers)
|
||||
}
|
||||
if len(walletClient.balanceRequests) != 2 || walletClient.balanceRequests[0].GetUserId() != 100 || walletClient.balanceRequests[1].GetUserId() != 101 {
|
||||
t.Fatalf("host salary requests mismatch: %+v", walletClient.balanceRequests)
|
||||
}
|
||||
var response httpkit.ResponseEnvelope
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
data := response.Data.(map[string]any)
|
||||
items := data["items"].([]any)
|
||||
first := items[0].(map[string]any)
|
||||
salary := first["salary"].(map[string]any)
|
||||
if data["total"] != float64(2) || first["host_user_id"] != "100" || first["display_user_id"] != "163100" || first["username"] != "Lina_voice" || salary["display_amount"] != 3050.25 {
|
||||
t.Fatalf("hosts response mismatch: %+v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgencyCenterReviewAndRemoveForwardOwnerCommands(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{
|
||||
roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: true, AgencyId: 7001},
|
||||
getAgencyResp: &userv1.GetAgencyResponse{Agency: &userv1.Agency{AgencyId: 7001, OwnerUserId: 42, Status: "active"}},
|
||||
reviewResp: &userv1.ReviewAgencyApplicationResponse{Application: &userv1.AgencyApplication{
|
||||
ApplicationId: 9001,
|
||||
AgencyId: 7001,
|
||||
Status: "approved",
|
||||
}},
|
||||
kickResp: &userv1.KickAgencyHostResponse{Membership: &userv1.AgencyMembership{
|
||||
MembershipId: 2,
|
||||
AgencyId: 7001,
|
||||
HostUserId: 100,
|
||||
Status: "ended",
|
||||
}},
|
||||
}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetUserHostClient(hostClient)
|
||||
handler.SetWalletClient(&fakeWalletClient{})
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
|
||||
reviewRequest := httptest.NewRequest(http.MethodPost, "/api/v1/agency-center/applications/9001/review", bytes.NewReader([]byte(`{"command_id":"cmd-review","decision":"approve","reason":"ok"}`)))
|
||||
reviewRequest.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
reviewRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(reviewRecorder, reviewRequest)
|
||||
if reviewRecorder.Code != http.StatusOK {
|
||||
t.Fatalf("review status mismatch: got %d body=%s", reviewRecorder.Code, reviewRecorder.Body.String())
|
||||
}
|
||||
if hostClient.lastReview == nil || hostClient.lastReview.GetReviewerUserId() != 42 || hostClient.lastReview.GetApplicationId() != 9001 || hostClient.lastReview.GetCommandId() != "cmd-review" || hostClient.lastReview.GetDecision() != "approve" {
|
||||
t.Fatalf("review request mismatch: %+v", hostClient.lastReview)
|
||||
}
|
||||
|
||||
removeRequest := httptest.NewRequest(http.MethodPost, "/api/v1/agency-center/hosts/100/remove", bytes.NewReader([]byte(`{"command_id":"cmd-remove","reason":"agency_center"}`)))
|
||||
removeRequest.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
removeRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(removeRecorder, removeRequest)
|
||||
if removeRecorder.Code != http.StatusOK {
|
||||
t.Fatalf("remove status mismatch: got %d body=%s", removeRecorder.Code, removeRecorder.Body.String())
|
||||
}
|
||||
if hostClient.lastKick == nil || hostClient.lastKick.GetOperatorUserId() != 42 || hostClient.lastKick.GetAgencyId() != 7001 || hostClient.lastKick.GetHostUserId() != 100 || hostClient.lastKick.GetCommandId() != "cmd-remove" {
|
||||
t.Fatalf("remove request mismatch: %+v", hostClient.lastKick)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmMicPublishingForwardsSessionVersionAndEventTime(t *testing.T) {
|
||||
client := &fakeRoomClient{}
|
||||
router := NewHandler(client).Routes(auth.NewVerifier("secret"))
|
||||
@ -3708,6 +4099,131 @@ func TestAchievementAndBadgeRoutesEmbedResourceMaterials(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppearanceAggregatesEquippedResourcesAndBadges(t *testing.T) {
|
||||
frameResource := &walletv1.Resource{
|
||||
ResourceId: 8101,
|
||||
ResourceCode: "frame_gold",
|
||||
ResourceType: "avatar_frame",
|
||||
Name: "Gold Frame",
|
||||
AssetUrl: "https://cdn.example/frame.png",
|
||||
AnimationUrl: "https://cdn.example/frame.svga",
|
||||
MetadataJson: `{"level":1}`,
|
||||
GrantStrategy: "set_active_flag",
|
||||
Status: "active",
|
||||
}
|
||||
vehicleResource := &walletv1.Resource{
|
||||
ResourceId: 8102,
|
||||
ResourceCode: "vehicle_gold",
|
||||
ResourceType: "vehicle",
|
||||
Name: "Gold Vehicle",
|
||||
AssetUrl: "https://cdn.example/vehicle.png",
|
||||
AnimationUrl: "https://cdn.example/vehicle.svga",
|
||||
GrantStrategy: "set_active_flag",
|
||||
Status: "active",
|
||||
}
|
||||
profileCardResource := &walletv1.Resource{
|
||||
ResourceId: 8104,
|
||||
ResourceCode: "profile_card_gold",
|
||||
ResourceType: "profile_card",
|
||||
Name: "Gold Profile Card",
|
||||
AssetUrl: "https://cdn.example/profile-card.png",
|
||||
AnimationUrl: "https://cdn.example/profile-card.mp4",
|
||||
MetadataJson: `{"profile_card_layout":{"source_width":1136,"source_height":1680,"content_top":117,"content_bottom":1666,"content_height":1550}}`,
|
||||
GrantStrategy: "extend_expiry",
|
||||
Status: "active",
|
||||
}
|
||||
badgeResource := &walletv1.Resource{
|
||||
ResourceId: 8103,
|
||||
ResourceCode: "badge_host",
|
||||
ResourceType: "badge",
|
||||
Name: "Host Badge",
|
||||
AssetUrl: "https://cdn.example/badge.png",
|
||||
GrantStrategy: "set_active_flag",
|
||||
Status: "active",
|
||||
}
|
||||
walletClient := &fakeWalletClient{
|
||||
batchEquippedResp: &walletv1.BatchGetUserEquippedResourcesResponse{
|
||||
Users: []*walletv1.UserEquippedResources{{
|
||||
UserId: 42,
|
||||
Resources: []*walletv1.UserResourceEntitlement{
|
||||
{UserId: 42, EntitlementId: "ent-frame", ResourceId: 8101, Resource: frameResource, Equipped: true, ExpiresAtMs: 1999999999999},
|
||||
{UserId: 42, EntitlementId: "ent-vehicle", ResourceId: 8102, Resource: vehicleResource, Equipped: true, ExpiresAtMs: 1999999999999},
|
||||
{UserId: 42, EntitlementId: "ent-profile-card", ResourceId: 8104, Resource: profileCardResource, Equipped: true, ExpiresAtMs: 1999999999999},
|
||||
{UserId: 42, EntitlementId: "ent-badge", ResourceId: 8103, Resource: badgeResource, Equipped: true, ExpiresAtMs: 1999999999999},
|
||||
},
|
||||
}},
|
||||
},
|
||||
resourcesByID: map[int64]*walletv1.Resource{8103: badgeResource},
|
||||
}
|
||||
achievementClient := &fakeAchievementClient{
|
||||
badgesResp: &activityv1.ListMyBadgesResponse{
|
||||
StripBadges: []*activityv1.BadgeDisplayItem{{
|
||||
Slot: "strip",
|
||||
Position: 1,
|
||||
BadgeForm: "icon",
|
||||
ResourceId: 8103,
|
||||
EntitlementId: "ent-badge",
|
||||
SourceType: "achievement",
|
||||
SourceId: "ach-host",
|
||||
PinMode: "auto",
|
||||
UpdatedAtMs: 1778256000000,
|
||||
}},
|
||||
ServerTimeMs: 1778256000000,
|
||||
},
|
||||
}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetWalletClient(walletClient)
|
||||
handler.SetAchievementClient(achievementClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/appearance", nil)
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("appearance status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var envelope struct {
|
||||
Code string `json:"code"`
|
||||
Data struct {
|
||||
UserID string `json:"user_id"`
|
||||
AvatarFrame struct {
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
AnimationURL string `json:"animation_url"`
|
||||
} `json:"avatar_frame"`
|
||||
Vehicle struct {
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
AnimationURL string `json:"animation_url"`
|
||||
} `json:"vehicle"`
|
||||
ProfileCard struct {
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
MetadataJSON string `json:"metadata_json"`
|
||||
} `json:"profile_card"`
|
||||
EquippedBadges []struct {
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
} `json:"equipped_badges"`
|
||||
Badges []struct {
|
||||
ResourceID int64 `json:"resource_id"`
|
||||
Resource struct {
|
||||
AssetURL string `json:"asset_url"`
|
||||
} `json:"resource"`
|
||||
} `json:"badges"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
|
||||
t.Fatalf("decode appearance response failed: %v", err)
|
||||
}
|
||||
if walletClient.lastBatchEquipped == nil || walletClient.lastBatchEquipped.GetUserIds()[0] != 42 {
|
||||
t.Fatalf("appearance should batch query equipped resources: %+v", walletClient.lastBatchEquipped)
|
||||
}
|
||||
if achievementClient.lastBadges == nil || achievementClient.lastBadges.GetUserId() != 42 {
|
||||
t.Fatalf("appearance should query displayed badges: %+v", achievementClient.lastBadges)
|
||||
}
|
||||
if envelope.Code != httpkit.CodeOK || envelope.Data.UserID != "42" || envelope.Data.AvatarFrame.AnimationURL != "https://cdn.example/frame.svga" || envelope.Data.Vehicle.ResourceID != 8102 || envelope.Data.ProfileCard.ResourceID != 8104 || !strings.Contains(envelope.Data.ProfileCard.MetadataJSON, `"content_top":117`) || len(envelope.Data.EquippedBadges) != 1 || len(envelope.Data.Badges) != 1 || envelope.Data.Badges[0].Resource.AssetURL != "https://cdn.example/badge.png" {
|
||||
t.Fatalf("appearance response mismatch: %+v", envelope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListEmojiPacksFiltersRegionAndReturnsAssets(t *testing.T) {
|
||||
walletClient := &fakeWalletClient{
|
||||
listResourcesResp: &walletv1.ListResourcesResponse{
|
||||
|
||||
@ -18,6 +18,8 @@ type Handler struct {
|
||||
userSocialClient client.UserSocialClient
|
||||
userHostClient client.UserHostClient
|
||||
walletClient client.WalletClient
|
||||
growthLevelClient client.GrowthLevelClient
|
||||
achievementClient client.AchievementClient
|
||||
rtcTokenConfig tencentrtc.TokenConfig
|
||||
}
|
||||
|
||||
@ -29,6 +31,8 @@ type Config struct {
|
||||
UserSocialClient client.UserSocialClient
|
||||
UserHostClient client.UserHostClient
|
||||
WalletClient client.WalletClient
|
||||
GrowthLevelClient client.GrowthLevelClient
|
||||
AchievementClient client.AchievementClient
|
||||
RTCTokenConfig tencentrtc.TokenConfig
|
||||
}
|
||||
|
||||
@ -41,6 +45,8 @@ func New(config Config) *Handler {
|
||||
userSocialClient: config.UserSocialClient,
|
||||
userHostClient: config.UserHostClient,
|
||||
walletClient: config.WalletClient,
|
||||
growthLevelClient: config.GrowthLevelClient,
|
||||
achievementClient: config.AchievementClient,
|
||||
rtcTokenConfig: config.RTCTokenConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ package roomapi
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@ -14,6 +15,7 @@ import (
|
||||
"hyapp/pkg/xerr"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
roomv1 "hyapp.local/api/proto/room/v1"
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
@ -839,6 +841,8 @@ func (h *Handler) joinRoom(writer http.ResponseWriter, request *http.Request) {
|
||||
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||
Role: body.Role,
|
||||
Password: body.Password,
|
||||
// 座驾快照在进房命令提交前生成;room-service 只保存快照,不回查 wallet。
|
||||
EntryVehicle: h.joinRoomEntryVehicleSnapshot(request),
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
@ -847,6 +851,55 @@ func (h *Handler) joinRoom(writer http.ResponseWriter, request *http.Request) {
|
||||
httpkit.WriteOK(writer, request, h.joinRoomInitialData(request, body.RoomID, resp))
|
||||
}
|
||||
|
||||
func (h *Handler) joinRoomEntryVehicleSnapshot(request *http.Request) *roomv1.RoomEntryVehicleSnapshot {
|
||||
if h.walletClient == nil {
|
||||
// 座驾只影响进房展示,不应该因为 wallet 暂不可用阻断 JoinRoom。
|
||||
return nil
|
||||
}
|
||||
userID := auth.UserIDFromContext(request.Context())
|
||||
if userID <= 0 {
|
||||
return nil
|
||||
}
|
||||
resp, err := h.walletClient.BatchGetUserEquippedResources(request.Context(), &walletv1.BatchGetUserEquippedResourcesRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
UserIds: []int64{userID},
|
||||
ResourceTypes: []string{"vehicle"},
|
||||
})
|
||||
if err != nil {
|
||||
// 佩戴态增强失败时降级为无座驾进房;房间 presence 仍由 room-service 提交。
|
||||
return nil
|
||||
}
|
||||
for _, user := range resp.GetUsers() {
|
||||
if user.GetUserId() != userID {
|
||||
// batch 响应按用户分组;忽略非当前用户,避免串用户座驾。
|
||||
continue
|
||||
}
|
||||
for _, item := range user.GetResources() {
|
||||
if item.GetResource().GetResourceType() != "vehicle" {
|
||||
continue
|
||||
}
|
||||
resource := item.GetResource()
|
||||
if resource.GetResourceId() <= 0 {
|
||||
continue
|
||||
}
|
||||
// 这里复制的是资源素材和 entitlement 有效期,不把 wallet 的内部权益结构透传给 room-service。
|
||||
return &roomv1.RoomEntryVehicleSnapshot{
|
||||
ResourceId: resource.GetResourceId(),
|
||||
ResourceCode: resource.GetResourceCode(),
|
||||
Name: resource.GetName(),
|
||||
AssetUrl: resource.GetAssetUrl(),
|
||||
PreviewUrl: resource.GetPreviewUrl(),
|
||||
AnimationUrl: resource.GetAnimationUrl(),
|
||||
MetadataJson: resource.GetMetadataJson(),
|
||||
EntitlementId: item.GetEntitlementId(),
|
||||
ExpiresAtMs: item.GetExpiresAtMs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) joinRoomInitialData(request *http.Request, requestedRoomID string, resp *roomv1.JoinRoomResponse) joinRoomData {
|
||||
if resp == nil {
|
||||
return joinRoomData{}
|
||||
@ -886,37 +939,11 @@ func (h *Handler) joinRoomRTCData(roomID string, viewerUserID int64) roomRTCData
|
||||
return rtc
|
||||
}
|
||||
|
||||
func (h *Handler) roomInitialProfiles(request *http.Request, snapshot *roomv1.RoomSnapshot, viewerUserID int64) []roomUserProfileData {
|
||||
if h.userProfileClient == nil {
|
||||
return nil
|
||||
}
|
||||
func (h *Handler) roomInitialProfiles(request *http.Request, snapshot *roomv1.RoomSnapshot, viewerUserID int64) []roomDisplayProfileData {
|
||||
userIDs := roomInitialProfileUserIDs(snapshot, viewerUserID, joinRoomContributionRankLimit)
|
||||
if len(userIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserIds: userIDs,
|
||||
})
|
||||
if err != nil {
|
||||
// 用户资料只服务首屏展示,失败时让客户端用 /users/profiles:batch 补偿,避免阻塞进房。
|
||||
return nil
|
||||
}
|
||||
|
||||
profiles := make([]roomUserProfileData, 0, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
user := resp.GetUsers()[userID]
|
||||
if user == nil {
|
||||
continue
|
||||
}
|
||||
profiles = append(profiles, roomUserProfileData{
|
||||
UserID: strconv.FormatInt(user.GetUserId(), 10),
|
||||
DisplayUserID: user.GetDisplayUserId(),
|
||||
Username: user.GetUsername(),
|
||||
Avatar: user.GetAvatar(),
|
||||
})
|
||||
}
|
||||
return profiles
|
||||
// 进房首屏资料和 /users/room-display-profiles:batch 使用同一份聚合逻辑,
|
||||
// 避免头像框、资料卡、徽章等展示资源在不同房间入口字段不一致。
|
||||
return h.roomDisplayProfiles(request, userIDs)
|
||||
}
|
||||
|
||||
// roomHeartbeat 把客户端显式心跳转换为 room-service presence refresh 命令。
|
||||
@ -1255,7 +1282,7 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
|
||||
targetType = "user"
|
||||
}
|
||||
targetUserIDs := normalizeGiftTargetUserIDs(body.TargetUserID, body.TargetUserIDs)
|
||||
if targetType == "user" && len(targetUserIDs) != 1 {
|
||||
if targetType == "user" && len(targetUserIDs) == 0 {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
@ -1392,12 +1419,19 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
|
||||
// 房间资料是展示增强,失败时保留主房间数据,让客户端后续重试 batch 接口。
|
||||
return nil
|
||||
}
|
||||
// 房间资料高频读取,头像框/资料卡/座驾/当前佩戴徽章用 wallet 批量查;
|
||||
// activity 的 badge 投影再补展示槽位,两个来源合并后客户端才能同时看到长短徽章。
|
||||
appearanceMap := h.roomAppearanceResourceMap(request, userIDs)
|
||||
badgeMap := h.roomBadgeDisplayMap(request, userIDs)
|
||||
levelMap := h.roomLevelDisplayProfileMap(request, userIDs)
|
||||
profiles := make(map[int64]roomDisplayProfileData, len(resp.GetUsers()))
|
||||
for _, userID := range userIDs {
|
||||
user := resp.GetUsers()[userID]
|
||||
if user == nil {
|
||||
continue
|
||||
}
|
||||
appearance := appearanceMap[userID]
|
||||
levels := levelMap[userID]
|
||||
profiles[userID] = roomDisplayProfileData{
|
||||
UserID: formatOptionalUserID(user.GetUserId()),
|
||||
Username: user.GetUsername(),
|
||||
@ -1410,16 +1444,462 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
|
||||
CountryDisplayName: strings.TrimSpace(user.GetCountryDisplayName()),
|
||||
CountryFlag: roomProfileCountryFlag(user.GetCountry()),
|
||||
VIP: map[string]any{},
|
||||
Level: map[string]any{},
|
||||
Badges: []map[string]any{},
|
||||
AvatarFrame: map[string]any{},
|
||||
Vehicle: map[string]any{},
|
||||
Level: levels.Levels,
|
||||
LevelBadges: levels.Badges,
|
||||
Badges: roomMergeBadgeItems(badgeMap[userID], appearance.Badges),
|
||||
AvatarFrame: appearance.AvatarFrame,
|
||||
ProfileCard: appearance.ProfileCard,
|
||||
Vehicle: appearance.Vehicle,
|
||||
Charm: 0,
|
||||
}
|
||||
// 显式补空对象/空数组,避免客户端在房间资料中同时处理 null 和空值。
|
||||
if profiles[userID].Badges == nil {
|
||||
profile := profiles[userID]
|
||||
profile.Badges = []map[string]any{}
|
||||
profiles[userID] = profile
|
||||
}
|
||||
if profiles[userID].AvatarFrame == nil {
|
||||
profile := profiles[userID]
|
||||
profile.AvatarFrame = map[string]any{}
|
||||
profiles[userID] = profile
|
||||
}
|
||||
if profiles[userID].Level == nil {
|
||||
profile := profiles[userID]
|
||||
profile.Level = roomDefaultLevelDisplay()
|
||||
profiles[userID] = profile
|
||||
}
|
||||
if profiles[userID].LevelBadges == nil {
|
||||
profile := profiles[userID]
|
||||
profile.LevelBadges = map[string]any{}
|
||||
profiles[userID] = profile
|
||||
}
|
||||
if profiles[userID].Vehicle == nil {
|
||||
profile := profiles[userID]
|
||||
profile.Vehicle = map[string]any{}
|
||||
profiles[userID] = profile
|
||||
}
|
||||
if profiles[userID].ProfileCard == nil {
|
||||
profile := profiles[userID]
|
||||
profile.ProfileCard = map[string]any{}
|
||||
profiles[userID] = profile
|
||||
}
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
type roomAppearanceResources struct {
|
||||
AvatarFrame map[string]any
|
||||
ProfileCard map[string]any
|
||||
Vehicle map[string]any
|
||||
Badges []map[string]any
|
||||
}
|
||||
|
||||
func (h *Handler) roomAppearanceResourceMap(request *http.Request, userIDs []int64) map[int64]roomAppearanceResources {
|
||||
result := make(map[int64]roomAppearanceResources, len(userIDs))
|
||||
if h.walletClient == nil || len(userIDs) == 0 {
|
||||
return result
|
||||
}
|
||||
// wallet 在该 RPC 内做过期佩戴过滤,所以 roomDisplayProfileMap 不需要重复判断 expires_at_ms。
|
||||
resp, err := h.walletClient.BatchGetUserEquippedResources(request.Context(), &walletv1.BatchGetUserEquippedResourcesRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
UserIds: userIDs,
|
||||
ResourceTypes: []string{"avatar_frame", "profile_card", "vehicle", "badge"},
|
||||
})
|
||||
if err != nil {
|
||||
// 装扮是展示增强,失败不影响房间主数据返回。
|
||||
return result
|
||||
}
|
||||
for _, user := range resp.GetUsers() {
|
||||
if user.GetUserId() <= 0 {
|
||||
continue
|
||||
}
|
||||
current := result[user.GetUserId()]
|
||||
for _, item := range user.GetResources() {
|
||||
// 同一资源类型只保留当前佩戴的一个权益;wallet equipment 主键保证唯一性。
|
||||
resource := roomAppearanceResourceFromEntitlement(item)
|
||||
switch item.GetResource().GetResourceType() {
|
||||
case "avatar_frame":
|
||||
current.AvatarFrame = resource
|
||||
case "profile_card":
|
||||
current.ProfileCard = resource
|
||||
case "vehicle":
|
||||
current.Vehicle = resource
|
||||
case "badge":
|
||||
current.Badges = append(current.Badges, roomBadgeItemFromEquippedResource(resource))
|
||||
}
|
||||
}
|
||||
result[user.GetUserId()] = current
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func roomBadgeItemFromEquippedResource(resource map[string]any) map[string]any {
|
||||
if len(resource) == 0 {
|
||||
return map[string]any{}
|
||||
}
|
||||
return map[string]any{
|
||||
"slot": "equipped",
|
||||
"position": 0,
|
||||
"badge_form": strings.TrimSpace(fmt.Sprint(resource["badge_form"])),
|
||||
"resource_id": resource["resource_id"],
|
||||
"entitlement_id": strings.TrimSpace(fmt.Sprint(resource["entitlement_id"])),
|
||||
"source_type": "wallet_equipment",
|
||||
"source_id": strings.TrimSpace(fmt.Sprint(resource["resource_code"])),
|
||||
"pin_mode": "equipped",
|
||||
"updated_at_ms": int64(0),
|
||||
"resource": resource,
|
||||
}
|
||||
}
|
||||
|
||||
func roomMergeBadgeItems(displayBadges []map[string]any, equippedBadges []map[string]any) []map[string]any {
|
||||
if len(displayBadges) == 0 && len(equippedBadges) == 0 {
|
||||
return []map[string]any{}
|
||||
}
|
||||
result := make([]map[string]any, 0, len(displayBadges)+len(equippedBadges))
|
||||
seen := make(map[string]struct{}, len(displayBadges)+len(equippedBadges))
|
||||
add := func(item map[string]any) {
|
||||
if len(item) == 0 {
|
||||
return
|
||||
}
|
||||
key := roomBadgeDedupeKey(item)
|
||||
if key != "" {
|
||||
if _, exists := seen[key]; exists {
|
||||
return
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
for _, item := range displayBadges {
|
||||
add(item)
|
||||
}
|
||||
for _, item := range equippedBadges {
|
||||
add(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func roomBadgeDedupeKey(item map[string]any) string {
|
||||
resourceID := strings.TrimSpace(fmt.Sprint(item["resource_id"]))
|
||||
entitlementID := strings.TrimSpace(fmt.Sprint(item["entitlement_id"]))
|
||||
if resourceID == "" || resourceID == "0" {
|
||||
if resource, ok := item["resource"].(map[string]any); ok {
|
||||
resourceID = strings.TrimSpace(fmt.Sprint(resource["resource_id"]))
|
||||
}
|
||||
}
|
||||
if entitlementID != "" {
|
||||
return "entitlement:" + entitlementID
|
||||
}
|
||||
if resourceID != "" && resourceID != "0" {
|
||||
return "resource:" + resourceID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type roomLevelDisplayResources struct {
|
||||
Levels map[string]any
|
||||
Badges map[string]any
|
||||
}
|
||||
|
||||
func (h *Handler) roomLevelDisplayProfileMap(request *http.Request, userIDs []int64) map[int64]roomLevelDisplayResources {
|
||||
result := make(map[int64]roomLevelDisplayResources, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
result[userID] = roomLevelDisplayResources{Levels: roomDefaultLevelDisplay(), Badges: map[string]any{}}
|
||||
}
|
||||
if h.growthLevelClient != nil && len(userIDs) > 0 {
|
||||
resp, err := h.growthLevelClient.BatchGetUserLevelDisplayProfiles(request.Context(), &activityv1.BatchGetUserLevelDisplayProfilesRequest{
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserIds: userIDs,
|
||||
})
|
||||
if err == nil {
|
||||
resourceIDs := make([]int64, 0, len(resp.GetProfiles())*3)
|
||||
for _, profile := range resp.GetProfiles() {
|
||||
resourceIDs = append(resourceIDs, profile.GetWealth().GetBadgeResourceId())
|
||||
resourceIDs = append(resourceIDs, profile.GetGame().GetBadgeResourceId())
|
||||
resourceIDs = append(resourceIDs, profile.GetCharm().GetBadgeResourceId())
|
||||
}
|
||||
resources := h.roomResourcesByID(request, resourceIDs)
|
||||
for _, profile := range resp.GetProfiles() {
|
||||
if profile.GetUserId() <= 0 {
|
||||
continue
|
||||
}
|
||||
current := roomLevelDisplayResources{Levels: roomDefaultLevelDisplay(), Badges: map[string]any{}}
|
||||
roomApplyLevelDisplayTrack(current, profile.GetWealth(), resources)
|
||||
roomApplyLevelDisplayTrack(current, profile.GetGame(), resources)
|
||||
roomApplyLevelDisplayTrack(current, profile.GetCharm(), resources)
|
||||
result[profile.GetUserId()] = current
|
||||
}
|
||||
}
|
||||
}
|
||||
h.fillRoomLevelBadgeEntitlementFallback(request, userIDs, result)
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *Handler) fillRoomLevelBadgeEntitlementFallback(request *http.Request, userIDs []int64, result map[int64]roomLevelDisplayResources) {
|
||||
if h.walletClient == nil || len(userIDs) == 0 {
|
||||
return
|
||||
}
|
||||
for _, userID := range userIDs {
|
||||
current := result[userID]
|
||||
if !roomLevelBadgesNeedFallback(current.Badges) {
|
||||
continue
|
||||
}
|
||||
// wallet 暂无按多用户批量查拥有资源 RPC;这里仅在 activity 投影缺轨道时逐用户读取 active badge 权益。
|
||||
// 已有投影仍优先返回,避免注册 1 级权益覆盖用户后续升级后的等级徽章。
|
||||
resp, err := h.walletClient.ListUserResources(request.Context(), &walletv1.ListUserResourcesRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
UserId: userID,
|
||||
ResourceType: "badge",
|
||||
ActiveOnly: true,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, item := range resp.GetResources() {
|
||||
roomApplyLevelBadgeEntitlement(current, item)
|
||||
if !roomLevelBadgesNeedFallback(current.Badges) {
|
||||
break
|
||||
}
|
||||
}
|
||||
result[userID] = current
|
||||
}
|
||||
}
|
||||
|
||||
func roomLevelBadgesNeedFallback(levelBadges map[string]any) bool {
|
||||
if levelBadges == nil {
|
||||
return true
|
||||
}
|
||||
for _, track := range []string{"wealth", "game", "charm"} {
|
||||
if _, ok := levelBadges[track]; !ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func roomApplyLevelBadgeEntitlement(current roomLevelDisplayResources, item *walletv1.UserResourceEntitlement) {
|
||||
if item == nil || current.Badges == nil {
|
||||
return
|
||||
}
|
||||
resource := roomAppearanceResourceFromEntitlement(item)
|
||||
track := strings.TrimSpace(fmt.Sprint(resource["level_track"]))
|
||||
if track == "" || fmt.Sprint(resource["badge_kind"]) != "level" || fmt.Sprint(resource["badge_form"]) != "strip" {
|
||||
return
|
||||
}
|
||||
if _, exists := current.Badges[track]; exists {
|
||||
return
|
||||
}
|
||||
resourceID, _ := resource["resource_id"].(int64)
|
||||
if resourceID <= 0 {
|
||||
return
|
||||
}
|
||||
current.Badges[track] = map[string]any{
|
||||
"track": track,
|
||||
"source_type": "wallet_entitlement",
|
||||
"source_level": roomLevelValue(current.Levels, track),
|
||||
"badge_kind": "level",
|
||||
"resource_id": resourceID,
|
||||
"entitlement_id": item.GetEntitlementId(),
|
||||
"updated_at_ms": item.GetUpdatedAtMs(),
|
||||
"resource": resource,
|
||||
}
|
||||
}
|
||||
|
||||
func roomLevelValue(levels map[string]any, track string) int64 {
|
||||
trackLevel, ok := levels[track].(map[string]any)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
switch value := trackLevel["level"].(type) {
|
||||
case int64:
|
||||
return value
|
||||
case int32:
|
||||
return int64(value)
|
||||
case int:
|
||||
return int64(value)
|
||||
case float64:
|
||||
return int64(value)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func roomApplyLevelDisplayTrack(current roomLevelDisplayResources, item *activityv1.LevelDisplayTrackProfile, resources map[int64]map[string]any) {
|
||||
if item == nil || strings.TrimSpace(item.GetTrack()) == "" {
|
||||
return
|
||||
}
|
||||
track := strings.TrimSpace(item.GetTrack())
|
||||
current.Levels[track] = map[string]any{
|
||||
"level": item.GetLevel(),
|
||||
"tier_id": item.GetTierId(),
|
||||
"badge_resource_id": item.GetBadgeResourceId(),
|
||||
"badge_source_level": item.GetBadgeSourceLevel(),
|
||||
"avatar_frame_id": item.GetAvatarFrameResourceId(),
|
||||
"updated_at_ms": item.GetUpdatedAtMs(),
|
||||
"display_badge_source": "level_rule",
|
||||
}
|
||||
if item.GetBadgeResourceId() <= 0 {
|
||||
return
|
||||
}
|
||||
badge := map[string]any{
|
||||
"track": track,
|
||||
"source_type": "level_rule",
|
||||
"source_level": item.GetBadgeSourceLevel(),
|
||||
"badge_kind": "level",
|
||||
"resource_id": item.GetBadgeResourceId(),
|
||||
"updated_at_ms": item.GetUpdatedAtMs(),
|
||||
}
|
||||
if resource, ok := resources[item.GetBadgeResourceId()]; ok {
|
||||
badge["resource"] = resource
|
||||
}
|
||||
current.Badges[track] = badge
|
||||
}
|
||||
|
||||
func roomDefaultLevelDisplay() map[string]any {
|
||||
return map[string]any{
|
||||
"wealth": map[string]any{"level": int32(0), "tier_id": int64(0), "badge_resource_id": int64(0), "badge_source_level": int32(0), "avatar_frame_id": int64(0), "updated_at_ms": int64(0), "display_badge_source": "level_rule"},
|
||||
"game": map[string]any{"level": int32(0), "tier_id": int64(0), "badge_resource_id": int64(0), "badge_source_level": int32(0), "avatar_frame_id": int64(0), "updated_at_ms": int64(0), "display_badge_source": "level_rule"},
|
||||
"charm": map[string]any{"level": int32(0), "tier_id": int64(0), "badge_resource_id": int64(0), "badge_source_level": int32(0), "avatar_frame_id": int64(0), "updated_at_ms": int64(0), "display_badge_source": "level_rule"},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) roomBadgeDisplayMap(request *http.Request, userIDs []int64) map[int64][]map[string]any {
|
||||
result := make(map[int64][]map[string]any, len(userIDs))
|
||||
if h.achievementClient == nil || len(userIDs) == 0 {
|
||||
return result
|
||||
}
|
||||
// activity 目前没有 badge 批量接口;这里逐用户读取,但所有 badge 资源素材统一去重后解析。
|
||||
raw := make(map[int64][]*activityv1.BadgeDisplayItem, len(userIDs))
|
||||
resourceIDs := make([]int64, 0)
|
||||
for _, userID := range userIDs {
|
||||
resp, err := h.achievementClient.ListMyBadges(request.Context(), &activityv1.ListMyBadgesRequest{
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
// 单个用户 badge 投影失败时只影响该用户徽章,不拖垮整个房间资料响应。
|
||||
continue
|
||||
}
|
||||
items := append([]*activityv1.BadgeDisplayItem{}, resp.GetStripBadges()...)
|
||||
items = append(items, resp.GetProfileTileBadges()...)
|
||||
raw[userID] = items
|
||||
for _, item := range items {
|
||||
resourceIDs = append(resourceIDs, item.GetResourceId())
|
||||
}
|
||||
}
|
||||
resources := h.roomResourcesByID(request, resourceIDs)
|
||||
for userID, items := range raw {
|
||||
result[userID] = roomBadgeItemsFromProto(items, resources)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *Handler) roomResourcesByID(request *http.Request, ids []int64) map[int64]map[string]any {
|
||||
ids = uniquePositiveUserIDs(ids)
|
||||
result := make(map[int64]map[string]any, len(ids))
|
||||
if h.walletClient == nil || len(ids) == 0 {
|
||||
return result
|
||||
}
|
||||
// badge display item 只带 resource_id;这里补齐客户端渲染需要的 asset/animation/metadata。
|
||||
for _, id := range ids {
|
||||
resp, err := h.walletClient.GetResource(request.Context(), &walletv1.GetResourceRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
ResourceId: id,
|
||||
})
|
||||
if err != nil {
|
||||
// 素材缺失时保留 badge item 本体,客户端至少能根据 resource_id 做兜底展示。
|
||||
continue
|
||||
}
|
||||
resource := roomAppearanceResourceFromProto(resp.GetResource(), "", 0)
|
||||
if resourceID, ok := resource["resource_id"].(int64); ok && resourceID > 0 {
|
||||
result[resourceID] = resource
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func roomBadgeItemsFromProto(items []*activityv1.BadgeDisplayItem, resources map[int64]map[string]any) []map[string]any {
|
||||
result := make([]map[string]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
data := map[string]any{
|
||||
"slot": item.GetSlot(),
|
||||
"position": item.GetPosition(),
|
||||
"badge_form": item.GetBadgeForm(),
|
||||
"resource_id": item.GetResourceId(),
|
||||
"entitlement_id": item.GetEntitlementId(),
|
||||
"source_type": item.GetSourceType(),
|
||||
"source_id": item.GetSourceId(),
|
||||
"pin_mode": item.GetPinMode(),
|
||||
"updated_at_ms": item.GetUpdatedAtMs(),
|
||||
}
|
||||
if resource, ok := resources[item.GetResourceId()]; ok {
|
||||
data["resource"] = resource
|
||||
}
|
||||
result = append(result, data)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func roomAppearanceResourceFromEntitlement(item *walletv1.UserResourceEntitlement) map[string]any {
|
||||
if item == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return roomAppearanceResourceFromProto(item.GetResource(), item.GetEntitlementId(), item.GetExpiresAtMs())
|
||||
}
|
||||
|
||||
func roomAppearanceResourceFromProto(item *walletv1.Resource, entitlementID string, expiresAtMS int64) map[string]any {
|
||||
if item == nil || item.GetResourceId() <= 0 {
|
||||
return map[string]any{}
|
||||
}
|
||||
resource := map[string]any{
|
||||
"resource_id": item.GetResourceId(),
|
||||
"resource_code": item.GetResourceCode(),
|
||||
"resource_type": item.GetResourceType(),
|
||||
"name": item.GetName(),
|
||||
"asset_url": item.GetAssetUrl(),
|
||||
"preview_url": item.GetPreviewUrl(),
|
||||
"animation_url": item.GetAnimationUrl(),
|
||||
"metadata_json": item.GetMetadataJson(),
|
||||
"entitlement_id": entitlementID,
|
||||
"expires_at_ms": expiresAtMS,
|
||||
}
|
||||
if item.GetResourceType() == "badge" {
|
||||
badgeForm, badgeKind, levelTrack := roomBadgeMetadataFields(item.GetMetadataJson())
|
||||
resource["badge_form"] = badgeForm
|
||||
resource["badge_kind"] = badgeKind
|
||||
resource["level_track"] = levelTrack
|
||||
}
|
||||
return resource
|
||||
}
|
||||
|
||||
func roomBadgeMetadataFields(raw string) (string, string, string) {
|
||||
payload := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &payload); err != nil {
|
||||
return "", "normal", ""
|
||||
}
|
||||
badgeForm := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["badge_form"])))
|
||||
if badgeForm != "strip" && badgeForm != "tile" {
|
||||
badgeForm = ""
|
||||
}
|
||||
badgeKind := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["badge_kind"])))
|
||||
if badgeKind != "level" {
|
||||
badgeKind = "normal"
|
||||
}
|
||||
levelTrack := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["level_track"])))
|
||||
switch levelTrack {
|
||||
case "wealth", "game", "charm":
|
||||
default:
|
||||
levelTrack = ""
|
||||
}
|
||||
return badgeForm, badgeKind, levelTrack
|
||||
}
|
||||
|
||||
func roomProfileAgeFromBirth(birth string, now time.Time) int32 {
|
||||
birth = strings.TrimSpace(birth)
|
||||
if birth == "" {
|
||||
|
||||
@ -48,15 +48,15 @@ type createRoomData struct {
|
||||
}
|
||||
|
||||
type joinRoomData struct {
|
||||
Result roomCommandResultData `json:"result"`
|
||||
Room roomInitialData `json:"room"`
|
||||
Viewer roomViewerData `json:"viewer"`
|
||||
Seats []roomSeatData `json:"seats"`
|
||||
ContributionRank []roomRankItemData `json:"contribution_rank"`
|
||||
Profiles []roomUserProfileData `json:"profiles"`
|
||||
IM roomIMData `json:"im"`
|
||||
RTC roomRTCData `json:"rtc"`
|
||||
ServerTimeMS int64 `json:"server_time_ms"`
|
||||
Result roomCommandResultData `json:"result"`
|
||||
Room roomInitialData `json:"room"`
|
||||
Viewer roomViewerData `json:"viewer"`
|
||||
Seats []roomSeatData `json:"seats"`
|
||||
ContributionRank []roomRankItemData `json:"contribution_rank"`
|
||||
Profiles []roomDisplayProfileData `json:"profiles"`
|
||||
IM roomIMData `json:"im"`
|
||||
RTC roomRTCData `json:"rtc"`
|
||||
ServerTimeMS int64 `json:"server_time_ms"`
|
||||
}
|
||||
|
||||
type roomDetailData struct {
|
||||
@ -151,8 +151,10 @@ type roomDisplayProfileData struct {
|
||||
CountryFlag string `json:"country_flag"`
|
||||
VIP map[string]any `json:"vip"`
|
||||
Level map[string]any `json:"level"`
|
||||
LevelBadges map[string]any `json:"level_badges"`
|
||||
Badges []map[string]any `json:"badges"`
|
||||
AvatarFrame map[string]any `json:"avatar_frame"`
|
||||
ProfileCard map[string]any `json:"profile_card"`
|
||||
Vehicle map[string]any `json:"vehicle"`
|
||||
Charm int64 `json:"charm"`
|
||||
}
|
||||
@ -339,13 +341,6 @@ type roomRankItemData struct {
|
||||
UpdatedAtMS int64 `json:"updated_at_ms,omitempty"`
|
||||
}
|
||||
|
||||
type roomUserProfileData struct {
|
||||
UserID string `json:"user_id"`
|
||||
DisplayUserID string `json:"display_user_id,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
}
|
||||
|
||||
type roomIMData struct {
|
||||
GroupID string `json:"group_id"`
|
||||
NeedJoinGroup bool `json:"need_join_group"`
|
||||
|
||||
@ -29,6 +29,8 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
||||
UserSocialClient: h.userSocialClient,
|
||||
UserHostClient: h.userHostClient,
|
||||
WalletClient: h.walletClient,
|
||||
GrowthLevelClient: h.growthLevelClient,
|
||||
AchievementClient: h.achievementClient,
|
||||
RTCTokenConfig: tencentrtc.TokenConfig{
|
||||
Enabled: h.tencentRTC.Enabled,
|
||||
SDKAppID: h.tencentRTC.SDKAppID,
|
||||
@ -54,6 +56,8 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
||||
UserCountryClient: h.userCountryClient,
|
||||
UserHostClient: h.userHostClient,
|
||||
WalletClient: h.walletClient,
|
||||
GrowthLevelClient: h.growthLevelClient,
|
||||
AchievementClient: h.achievementClient,
|
||||
BroadcastClient: h.broadcastClient,
|
||||
})
|
||||
messageAPI := messageapi.New(messageapi.Config{
|
||||
@ -112,6 +116,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
||||
userHandlers.GetMyGiftWall = walletAPI.GetMyGiftWall
|
||||
userHandlers.ListMyResources = resourceAPI.ListMyResources
|
||||
userHandlers.EquipMyResource = resourceAPI.EquipMyResource
|
||||
userHandlers.UnequipMyResource = resourceAPI.UnequipMyResource
|
||||
|
||||
return httproutes.New(httproutes.Config{
|
||||
PublicWrap: h.publicAPIHandler,
|
||||
|
||||
@ -0,0 +1,422 @@
|
||||
package userapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
)
|
||||
|
||||
const (
|
||||
agencySalaryAssetType = "AGENCY_SALARY_USD"
|
||||
hostSalaryAssetType = "HOST_SALARY_USD"
|
||||
agencyStatusActive = "active"
|
||||
membershipStatusActive = "active"
|
||||
membershipTypeOwner = "owner"
|
||||
applicationStatusPending = "pending"
|
||||
)
|
||||
|
||||
type agencyCenterSalaryData struct {
|
||||
AssetType string `json:"asset_type"`
|
||||
AvailableAmount int64 `json:"available_amount"`
|
||||
FrozenAmount int64 `json:"frozen_amount"`
|
||||
DisplayAmount float64 `json:"display_amount"`
|
||||
}
|
||||
|
||||
type agencyCenterUserData struct {
|
||||
UserID string `json:"user_id"`
|
||||
DisplayUserID string `json:"display_user_id"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
}
|
||||
|
||||
type agencyCenterHostData struct {
|
||||
MembershipID string `json:"membership_id"`
|
||||
AgencyID string `json:"agency_id"`
|
||||
HostUserID string `json:"host_user_id"`
|
||||
DisplayUserID string `json:"display_user_id"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
RegionID int64 `json:"region_id"`
|
||||
MembershipType string `json:"membership_type"`
|
||||
Status string `json:"status"`
|
||||
JoinedAtMS int64 `json:"joined_at_ms"`
|
||||
Salary agencyCenterSalaryData `json:"salary"`
|
||||
}
|
||||
|
||||
type agencyCenterApplicationData struct {
|
||||
ApplicationID string `json:"application_id"`
|
||||
CommandID string `json:"command_id"`
|
||||
ApplicantUserID string `json:"applicant_user_id"`
|
||||
AgencyID string `json:"agency_id"`
|
||||
RegionID int64 `json:"region_id"`
|
||||
Status string `json:"status"`
|
||||
ReviewReason string `json:"review_reason,omitempty"`
|
||||
ReviewedAtMS int64 `json:"reviewed_at_ms,omitempty"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
Applicant agencyCenterUserData `json:"applicant"`
|
||||
}
|
||||
|
||||
func (h *Handler) getAgencyCenterOverview(writer http.ResponseWriter, request *http.Request) {
|
||||
agency, ok := h.resolveCurrentOwnerAgency(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if h.walletClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
|
||||
salary, err := h.salaryBalance(request, agency.GetOwnerUserId(), agencySalaryAssetType)
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
applications, err := h.userHostClient.GetAgencyApplications(request.Context(), &userv1.GetAgencyApplicationsRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
AgencyId: agency.GetAgencyId(),
|
||||
Status: applicationStatusPending,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
var bd *agencyCenterUserData
|
||||
if agency.GetParentBdUserId() > 0 {
|
||||
profiles, err := h.userProfiles(request, []int64{agency.GetParentBdUserId()})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
if profile, exists := profiles[agency.GetParentBdUserId()]; exists {
|
||||
bd = &profile
|
||||
}
|
||||
}
|
||||
|
||||
httpkit.WriteOK(writer, request, map[string]any{
|
||||
"agency": hostAgencyFromProto(agency),
|
||||
"bd": bd,
|
||||
"salary": salary,
|
||||
"pending_application_count": len(applications.GetApplications()),
|
||||
"server_time_ms": time.Now().UnixMilli(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) listAgencyCenterHosts(writer http.ResponseWriter, request *http.Request) {
|
||||
agency, ok := h.resolveCurrentOwnerAgency(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if h.walletClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
resp, err := h.userHostClient.GetAgencyMembers(request.Context(), &userv1.GetAgencyMembersRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
AgencyId: agency.GetAgencyId(),
|
||||
Status: membershipStatusActive,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
memberships := make([]*userv1.AgencyMembership, 0, len(resp.GetMemberships()))
|
||||
userIDs := make([]int64, 0, len(resp.GetMemberships()))
|
||||
for _, membership := range resp.GetMemberships() {
|
||||
if membership.GetHostUserId() == agency.GetOwnerUserId() || membership.GetMembershipType() == membershipTypeOwner {
|
||||
continue
|
||||
}
|
||||
memberships = append(memberships, membership)
|
||||
userIDs = append(userIDs, membership.GetHostUserId())
|
||||
}
|
||||
profiles, err := h.userProfiles(request, userIDs)
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]agencyCenterHostData, 0, len(memberships))
|
||||
for _, membership := range memberships {
|
||||
salary, err := h.salaryBalance(request, membership.GetHostUserId(), hostSalaryAssetType)
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
profile := profiles[membership.GetHostUserId()]
|
||||
items = append(items, agencyCenterHostData{
|
||||
MembershipID: int64String(membership.GetMembershipId()),
|
||||
AgencyID: int64String(membership.GetAgencyId()),
|
||||
HostUserID: int64String(membership.GetHostUserId()),
|
||||
DisplayUserID: profile.DisplayUserID,
|
||||
Username: profile.Username,
|
||||
Avatar: profile.Avatar,
|
||||
RegionID: membership.GetRegionId(),
|
||||
MembershipType: membership.GetMembershipType(),
|
||||
Status: membership.GetStatus(),
|
||||
JoinedAtMS: membership.GetJoinedAtMs(),
|
||||
Salary: salary,
|
||||
})
|
||||
}
|
||||
|
||||
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
func (h *Handler) listAgencyCenterApplications(writer http.ResponseWriter, request *http.Request) {
|
||||
agency, ok := h.resolveCurrentOwnerAgency(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
status := strings.TrimSpace(request.URL.Query().Get("status"))
|
||||
if status == "" {
|
||||
status = applicationStatusPending
|
||||
}
|
||||
if status != applicationStatusPending && status != "approved" && status != "rejected" {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.userHostClient.GetAgencyApplications(request.Context(), &userv1.GetAgencyApplicationsRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
AgencyId: agency.GetAgencyId(),
|
||||
Status: status,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
userIDs := make([]int64, 0, len(resp.GetApplications()))
|
||||
for _, application := range resp.GetApplications() {
|
||||
userIDs = append(userIDs, application.GetApplicantUserId())
|
||||
}
|
||||
profiles, err := h.userProfiles(request, userIDs)
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]agencyCenterApplicationData, 0, len(resp.GetApplications()))
|
||||
for _, application := range resp.GetApplications() {
|
||||
items = append(items, agencyCenterApplicationData{
|
||||
ApplicationID: int64String(application.GetApplicationId()),
|
||||
CommandID: application.GetCommandId(),
|
||||
ApplicantUserID: int64String(application.GetApplicantUserId()),
|
||||
AgencyID: int64String(application.GetAgencyId()),
|
||||
RegionID: application.GetRegionId(),
|
||||
Status: application.GetStatus(),
|
||||
ReviewReason: application.GetReviewReason(),
|
||||
ReviewedAtMS: application.GetReviewedAtMs(),
|
||||
CreatedAtMS: application.GetCreatedAtMs(),
|
||||
UpdatedAtMS: application.GetUpdatedAtMs(),
|
||||
Applicant: profiles[application.GetApplicantUserId()],
|
||||
})
|
||||
}
|
||||
|
||||
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
func (h *Handler) reviewAgencyCenterApplication(writer http.ResponseWriter, request *http.Request) {
|
||||
applicationID, ok := httpkit.PositiveInt64PathValue(request, "application_id")
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
if _, ok := h.resolveCurrentOwnerAgency(writer, request); !ok {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Decision string `json:"decision"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
commandID := strings.TrimSpace(body.CommandID)
|
||||
decision := strings.ToLower(strings.TrimSpace(body.Decision))
|
||||
if commandID == "" || (decision != "approve" && decision != "reject") {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.userHostClient.ReviewAgencyApplication(request.Context(), &userv1.ReviewAgencyApplicationRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
CommandId: commandID,
|
||||
ReviewerUserId: auth.UserIDFromContext(request.Context()),
|
||||
ApplicationId: applicationID,
|
||||
Decision: decision,
|
||||
Reason: strings.TrimSpace(body.Reason),
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
httpkit.WriteOK(writer, request, map[string]any{
|
||||
"application": hostAgencyApplicationFromProto(resp.GetApplication()),
|
||||
"membership": agencyMembershipData(resp.GetMembership()),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) removeAgencyCenterHost(writer http.ResponseWriter, request *http.Request) {
|
||||
hostUserID, ok := httpkit.PositiveInt64PathValue(request, "host_user_id")
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
agency, ok := h.resolveCurrentOwnerAgency(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
return
|
||||
}
|
||||
commandID := strings.TrimSpace(body.CommandID)
|
||||
if commandID == "" {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.userHostClient.KickAgencyHost(request.Context(), &userv1.KickAgencyHostRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
CommandId: commandID,
|
||||
OperatorUserId: auth.UserIDFromContext(request.Context()),
|
||||
AgencyId: agency.GetAgencyId(),
|
||||
HostUserId: hostUserID,
|
||||
Reason: strings.TrimSpace(body.Reason),
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
httpkit.WriteOK(writer, request, map[string]any{"membership": agencyMembershipData(resp.GetMembership())})
|
||||
}
|
||||
|
||||
func (h *Handler) resolveCurrentOwnerAgency(writer http.ResponseWriter, request *http.Request) (*userv1.Agency, bool) {
|
||||
if h.userHostClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return nil, false
|
||||
}
|
||||
userID := auth.UserIDFromContext(request.Context())
|
||||
roleResp, err := h.userHostClient.GetUserRoleSummary(request.Context(), &userv1.GetUserRoleSummaryRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return nil, false
|
||||
}
|
||||
summary := roleResp.GetSummary()
|
||||
if summary == nil || !summary.GetIsManager() || summary.GetAgencyId() <= 0 {
|
||||
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||
return nil, false
|
||||
}
|
||||
agencyResp, err := h.userHostClient.GetAgency(request.Context(), &userv1.GetAgencyRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
AgencyId: summary.GetAgencyId(),
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return nil, false
|
||||
}
|
||||
agency := agencyResp.GetAgency()
|
||||
if agency == nil || agency.GetOwnerUserId() != userID || agency.GetStatus() != agencyStatusActive {
|
||||
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||
return nil, false
|
||||
}
|
||||
return agency, true
|
||||
}
|
||||
|
||||
func (h *Handler) userProfiles(request *http.Request, userIDs []int64) (map[int64]agencyCenterUserData, error) {
|
||||
profiles := make(map[int64]agencyCenterUserData, len(userIDs))
|
||||
if len(userIDs) == 0 {
|
||||
return profiles, nil
|
||||
}
|
||||
if h.userProfileClient == nil {
|
||||
return profiles, nil
|
||||
}
|
||||
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserIds: userIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for userID, user := range resp.GetUsers() {
|
||||
profiles[userID] = agencyCenterUserFromProto(user)
|
||||
}
|
||||
for _, userID := range userIDs {
|
||||
if _, exists := profiles[userID]; !exists {
|
||||
profiles[userID] = agencyCenterUserData{UserID: int64String(userID)}
|
||||
}
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (h *Handler) salaryBalance(request *http.Request, userID int64, assetType string) (agencyCenterSalaryData, error) {
|
||||
resp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
UserId: userID,
|
||||
AssetTypes: []string{assetType},
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
})
|
||||
if err != nil {
|
||||
return agencyCenterSalaryData{}, err
|
||||
}
|
||||
for _, balance := range resp.GetBalances() {
|
||||
if balance.GetAssetType() == assetType {
|
||||
return salaryDataFromProto(balance), nil
|
||||
}
|
||||
}
|
||||
return agencyCenterSalaryData{AssetType: assetType}, nil
|
||||
}
|
||||
|
||||
func salaryDataFromProto(balance *walletv1.AssetBalance) agencyCenterSalaryData {
|
||||
if balance == nil {
|
||||
return agencyCenterSalaryData{}
|
||||
}
|
||||
return agencyCenterSalaryData{
|
||||
AssetType: balance.GetAssetType(),
|
||||
AvailableAmount: balance.GetAvailableAmount(),
|
||||
FrozenAmount: balance.GetFrozenAmount(),
|
||||
DisplayAmount: float64(balance.GetAvailableAmount()) / 100,
|
||||
}
|
||||
}
|
||||
|
||||
func agencyCenterUserFromProto(user *userv1.User) agencyCenterUserData {
|
||||
if user == nil {
|
||||
return agencyCenterUserData{}
|
||||
}
|
||||
return agencyCenterUserData{
|
||||
UserID: int64String(user.GetUserId()),
|
||||
DisplayUserID: user.GetDisplayUserId(),
|
||||
Username: user.GetUsername(),
|
||||
Avatar: user.GetAvatar(),
|
||||
}
|
||||
}
|
||||
|
||||
func agencyMembershipData(membership *userv1.AgencyMembership) map[string]any {
|
||||
if membership == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return map[string]any{
|
||||
"membership_id": int64String(membership.GetMembershipId()),
|
||||
"agency_id": int64String(membership.GetAgencyId()),
|
||||
"host_user_id": int64String(membership.GetHostUserId()),
|
||||
"region_id": membership.GetRegionId(),
|
||||
"membership_type": membership.GetMembershipType(),
|
||||
"status": membership.GetStatus(),
|
||||
"joined_at_ms": membership.GetJoinedAtMs(),
|
||||
"ended_at_ms": membership.GetEndedAtMs(),
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,470 @@
|
||||
package userapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/services/gateway-service/internal/auth"
|
||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||
)
|
||||
|
||||
const (
|
||||
// /appearance 只暴露客户端能直接佩戴或展示的装扮维度;类型值保持和 wallet resource_type 一致。
|
||||
appearanceResourceAvatarFrame = "avatar_frame"
|
||||
appearanceResourceProfileCard = "profile_card"
|
||||
appearanceResourceVehicle = "vehicle"
|
||||
appearanceResourceBadge = "badge"
|
||||
)
|
||||
|
||||
var errAppearanceUpstream = errors.New("upstream service error")
|
||||
|
||||
type appearanceData struct {
|
||||
UserID string `json:"user_id"`
|
||||
AvatarFrame map[string]any `json:"avatar_frame"`
|
||||
ProfileCard map[string]any `json:"profile_card"`
|
||||
Vehicle map[string]any `json:"vehicle"`
|
||||
Level map[string]any `json:"level"`
|
||||
LevelBadges map[string]any `json:"level_badges"`
|
||||
EquippedBadges []map[string]any `json:"equipped_badges"`
|
||||
Badges []map[string]any `json:"badges"`
|
||||
BadgeProfile appearanceBadgeData `json:"badge_profile"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
ServerTimeMS int64 `json:"server_time_ms"`
|
||||
}
|
||||
|
||||
type appearanceBadgeData struct {
|
||||
StripBadges []map[string]any `json:"strip_badges"`
|
||||
ProfileTileBadges []map[string]any `json:"profile_tile_badges"`
|
||||
HonorBadges []map[string]any `json:"honor_badges"`
|
||||
ServerTimeMS int64 `json:"server_time_ms"`
|
||||
}
|
||||
|
||||
func (h *Handler) getMyAppearance(writer http.ResponseWriter, request *http.Request) {
|
||||
h.writeAppearance(writer, request, auth.UserIDFromContext(request.Context()))
|
||||
}
|
||||
|
||||
func (h *Handler) getUserAppearance(writer http.ResponseWriter, request *http.Request) {
|
||||
userID, ok := httpkit.PositiveInt64PathValue(request, "user_id")
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
h.writeAppearance(writer, request, userID)
|
||||
}
|
||||
|
||||
func (h *Handler) writeAppearance(writer http.ResponseWriter, request *http.Request, userID int64) {
|
||||
if userID <= 0 {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
// wallet 是装扮权益和当前佩戴的事实源;缺 wallet 时 appearance 不能返回可信数据。
|
||||
if h.walletClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
data, err := h.appearanceForUser(request, userID)
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
httpkit.WriteOK(writer, request, data)
|
||||
}
|
||||
|
||||
func (h *Handler) appearanceForUser(request *http.Request, userID int64) (appearanceData, error) {
|
||||
// 先初始化为空对象/数组,保证客户端不需要处理 null 和字段缺失两套形态。
|
||||
data := appearanceData{
|
||||
UserID: strconv.FormatInt(userID, 10),
|
||||
AvatarFrame: map[string]any{},
|
||||
ProfileCard: map[string]any{},
|
||||
Vehicle: map[string]any{},
|
||||
Level: defaultAppearanceLevelDisplay(),
|
||||
LevelBadges: map[string]any{},
|
||||
EquippedBadges: []map[string]any{},
|
||||
Badges: []map[string]any{},
|
||||
Resources: []map[string]any{},
|
||||
}
|
||||
// 当前佩戴只走 BatchGetUserEquippedResources:
|
||||
// 1. wallet 内部会过滤过期佩戴;
|
||||
// 2. 后续扩展批量 appearance 时可以复用同一 RPC,不引入 N+1。
|
||||
equipped, err := h.walletClient.BatchGetUserEquippedResources(request.Context(), &walletv1.BatchGetUserEquippedResourcesRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
UserIds: []int64{userID},
|
||||
ResourceTypes: []string{appearanceResourceAvatarFrame, appearanceResourceProfileCard, appearanceResourceVehicle, appearanceResourceBadge},
|
||||
})
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
for _, user := range equipped.GetUsers() {
|
||||
if user.GetUserId() != userID {
|
||||
// batch RPC 按用户分组返回;这里防御上游误返回其他用户数据。
|
||||
continue
|
||||
}
|
||||
for _, item := range user.GetResources() {
|
||||
// Resources 保留完整佩戴列表,便于客户端以后按 resource_type 自行扩展展示位。
|
||||
resource := appearanceResourceFromEntitlement(item)
|
||||
data.Resources = append(data.Resources, resource)
|
||||
switch item.GetResource().GetResourceType() {
|
||||
case appearanceResourceAvatarFrame:
|
||||
data.AvatarFrame = resource
|
||||
case appearanceResourceProfileCard:
|
||||
data.ProfileCard = resource
|
||||
case appearanceResourceVehicle:
|
||||
data.Vehicle = resource
|
||||
case appearanceResourceBadge:
|
||||
data.EquippedBadges = append(data.EquippedBadges, resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// badge_display 是 activity-service 的投影;wallet 只提供 badge 资源素材和权益事实。
|
||||
badgeProfile, err := h.badgeProfileForUser(request, userID)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
data.BadgeProfile = badgeProfile
|
||||
// 顶层 badges 给高频展示位直接消费;完整 profile 仍保留在 badge_profile 中。
|
||||
data.Badges = badgeProfile.StripBadges
|
||||
level, levelBadges := h.levelDisplayForUser(request, userID)
|
||||
data.Level = level
|
||||
data.LevelBadges = levelBadges
|
||||
h.fillAppearanceLevelBadgeEntitlementFallback(request, userID, data.Level, data.LevelBadges)
|
||||
data.ServerTimeMS = badgeProfile.ServerTimeMS
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (h *Handler) fillAppearanceLevelBadgeEntitlementFallback(request *http.Request, userID int64, levels map[string]any, levelBadges map[string]any) {
|
||||
if h.walletClient == nil || userID <= 0 || !appearanceLevelBadgesNeedFallback(levelBadges) {
|
||||
return
|
||||
}
|
||||
// activity 的等级投影仍是首选来源;这里只在投影缺轨道时读取 wallet active 权益。
|
||||
// 注册补发的一等级徽章当前已经落在权益表里,这层兜底让老用户不等等级事件也能展示素材。
|
||||
resp, err := h.walletClient.ListUserResources(request.Context(), &walletv1.ListUserResourcesRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
UserId: userID,
|
||||
ResourceType: appearanceResourceBadge,
|
||||
ActiveOnly: true,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, item := range resp.GetResources() {
|
||||
appearanceApplyLevelBadgeEntitlement(levels, levelBadges, item)
|
||||
if !appearanceLevelBadgesNeedFallback(levelBadges) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appearanceLevelBadgesNeedFallback(levelBadges map[string]any) bool {
|
||||
if levelBadges == nil {
|
||||
return true
|
||||
}
|
||||
for _, track := range []string{"wealth", "game", "charm"} {
|
||||
if _, ok := levelBadges[track]; !ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appearanceApplyLevelBadgeEntitlement(levels map[string]any, levelBadges map[string]any, item *walletv1.UserResourceEntitlement) {
|
||||
if item == nil || levelBadges == nil {
|
||||
return
|
||||
}
|
||||
resource := appearanceResourceFromEntitlement(item)
|
||||
track := strings.TrimSpace(fmt.Sprint(resource["level_track"]))
|
||||
if track == "" || fmt.Sprint(resource["badge_kind"]) != "level" || fmt.Sprint(resource["badge_form"]) != "strip" {
|
||||
return
|
||||
}
|
||||
if _, exists := levelBadges[track]; exists {
|
||||
return
|
||||
}
|
||||
resourceID, _ := resource["resource_id"].(int64)
|
||||
if resourceID <= 0 {
|
||||
return
|
||||
}
|
||||
levelBadges[track] = map[string]any{
|
||||
"track": track,
|
||||
"source_type": "wallet_entitlement",
|
||||
"source_level": appearanceLevelValue(levels, track),
|
||||
"badge_kind": "level",
|
||||
"resource_id": resourceID,
|
||||
"entitlement_id": item.GetEntitlementId(),
|
||||
"updated_at_ms": item.GetUpdatedAtMs(),
|
||||
"resource": resource,
|
||||
}
|
||||
}
|
||||
|
||||
func appearanceLevelValue(levels map[string]any, track string) int64 {
|
||||
trackLevel, ok := levels[track].(map[string]any)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
switch value := trackLevel["level"].(type) {
|
||||
case int64:
|
||||
return value
|
||||
case int32:
|
||||
return int64(value)
|
||||
case int:
|
||||
return int64(value)
|
||||
case float64:
|
||||
return int64(value)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) levelDisplayForUser(request *http.Request, userID int64) (map[string]any, map[string]any) {
|
||||
levels := defaultAppearanceLevelDisplay()
|
||||
levelBadges := map[string]any{}
|
||||
if h.growthLevelClient == nil {
|
||||
return levels, levelBadges
|
||||
}
|
||||
resp, err := h.growthLevelClient.BatchGetUserLevelDisplayProfiles(request.Context(), &activityv1.BatchGetUserLevelDisplayProfilesRequest{
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserIds: []int64{userID},
|
||||
})
|
||||
if err != nil {
|
||||
// 等级徽章只是展示增强,activity 短暂失败时保留装扮和普通徽章结果。
|
||||
return levels, levelBadges
|
||||
}
|
||||
resourceIDs := make([]int64, 0, 3)
|
||||
var profile *activityv1.UserLevelDisplayProfile
|
||||
for _, item := range resp.GetProfiles() {
|
||||
if item.GetUserId() == userID {
|
||||
profile = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if profile == nil {
|
||||
return levels, levelBadges
|
||||
}
|
||||
resourceIDs = append(resourceIDs, profile.GetWealth().GetBadgeResourceId(), profile.GetGame().GetBadgeResourceId(), profile.GetCharm().GetBadgeResourceId())
|
||||
resources := map[int64]map[string]any{}
|
||||
if resolved, err := h.appearanceResourcesByID(request, resourceIDs); err == nil {
|
||||
resources = resolved
|
||||
}
|
||||
applyAppearanceLevelTrack(levels, levelBadges, profile.GetWealth(), resources)
|
||||
applyAppearanceLevelTrack(levels, levelBadges, profile.GetGame(), resources)
|
||||
applyAppearanceLevelTrack(levels, levelBadges, profile.GetCharm(), resources)
|
||||
return levels, levelBadges
|
||||
}
|
||||
|
||||
func applyAppearanceLevelTrack(levels map[string]any, levelBadges map[string]any, item *activityv1.LevelDisplayTrackProfile, resources map[int64]map[string]any) {
|
||||
if item == nil || strings.TrimSpace(item.GetTrack()) == "" {
|
||||
return
|
||||
}
|
||||
track := strings.TrimSpace(item.GetTrack())
|
||||
levels[track] = map[string]any{
|
||||
"level": item.GetLevel(),
|
||||
"tier_id": item.GetTierId(),
|
||||
"badge_resource_id": item.GetBadgeResourceId(),
|
||||
"badge_source_level": item.GetBadgeSourceLevel(),
|
||||
"avatar_frame_id": item.GetAvatarFrameResourceId(),
|
||||
"updated_at_ms": item.GetUpdatedAtMs(),
|
||||
"display_badge_source": "level_rule",
|
||||
}
|
||||
if item.GetBadgeResourceId() <= 0 {
|
||||
return
|
||||
}
|
||||
badge := map[string]any{
|
||||
"track": track,
|
||||
"source_type": "level_rule",
|
||||
"source_level": item.GetBadgeSourceLevel(),
|
||||
"badge_kind": "level",
|
||||
"resource_id": item.GetBadgeResourceId(),
|
||||
"updated_at_ms": item.GetUpdatedAtMs(),
|
||||
}
|
||||
if resource, ok := resources[item.GetBadgeResourceId()]; ok {
|
||||
badge["resource"] = resource
|
||||
}
|
||||
levelBadges[track] = badge
|
||||
}
|
||||
|
||||
func defaultAppearanceLevelDisplay() map[string]any {
|
||||
return map[string]any{
|
||||
"wealth": map[string]any{"level": int32(0), "tier_id": int64(0), "badge_resource_id": int64(0), "badge_source_level": int32(0), "avatar_frame_id": int64(0), "updated_at_ms": int64(0), "display_badge_source": "level_rule"},
|
||||
"game": map[string]any{"level": int32(0), "tier_id": int64(0), "badge_resource_id": int64(0), "badge_source_level": int32(0), "avatar_frame_id": int64(0), "updated_at_ms": int64(0), "display_badge_source": "level_rule"},
|
||||
"charm": map[string]any{"level": int32(0), "tier_id": int64(0), "badge_resource_id": int64(0), "badge_source_level": int32(0), "avatar_frame_id": int64(0), "updated_at_ms": int64(0), "display_badge_source": "level_rule"},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) badgeProfileForUser(request *http.Request, userID int64) (appearanceBadgeData, error) {
|
||||
result := appearanceBadgeData{
|
||||
StripBadges: []map[string]any{},
|
||||
ProfileTileBadges: []map[string]any{},
|
||||
HonorBadges: []map[string]any{},
|
||||
}
|
||||
if h.achievementClient == nil {
|
||||
// 未接 activity 时保留装扮数据返回,badge 投影为空;这不会影响头像框/座驾展示。
|
||||
return result, nil
|
||||
}
|
||||
resp, err := h.achievementClient.ListMyBadges(request.Context(), &activityv1.ListMyBadgesRequest{
|
||||
Meta: httpkit.ActivityMeta(request),
|
||||
UserId: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
// activity 的 badge item 只保存 resource_id;资源图标/动画素材仍从 wallet resource 表解析。
|
||||
resources, err := h.appearanceResourcesByID(request, badgeResourceIDs(resp))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.StripBadges = badgeItemsFromProto(resp.GetStripBadges(), resources)
|
||||
result.ProfileTileBadges = badgeItemsFromProto(resp.GetProfileTileBadges(), resources)
|
||||
result.HonorBadges = badgeItemsFromProto(resp.GetHonorBadges(), resources)
|
||||
result.ServerTimeMS = resp.GetServerTimeMs()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *Handler) appearanceResourcesByID(request *http.Request, ids []int64) (map[int64]map[string]any, error) {
|
||||
ids = uniqueAppearanceResourceIDs(ids)
|
||||
resources := make(map[int64]map[string]any, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return resources, nil
|
||||
}
|
||||
if h.walletClient == nil {
|
||||
// 只有 badge 素材解析会走到这里;缺 wallet 表示无法把 badge_id 转成可展示资源。
|
||||
return nil, errAppearanceUpstream
|
||||
}
|
||||
for _, id := range ids {
|
||||
resp, err := h.walletClient.GetResource(request.Context(), &walletv1.GetResourceRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
ResourceId: id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resource := appearanceResourceFromProto(resp.GetResource(), "", 0)
|
||||
if resourceID, ok := resource["resource_id"].(int64); ok && resourceID > 0 {
|
||||
resources[resourceID] = resource
|
||||
}
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func appearanceResourceFromEntitlement(item *walletv1.UserResourceEntitlement) map[string]any {
|
||||
if item == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
// entitlement_id/expires_at_ms 是佩戴态字段,resource 本身不带这两个用户维度。
|
||||
return appearanceResourceFromProto(item.GetResource(), item.GetEntitlementId(), item.GetExpiresAtMs())
|
||||
}
|
||||
|
||||
func appearanceResourceFromProto(item *walletv1.Resource, entitlementID string, expiresAtMS int64) map[string]any {
|
||||
if item == nil || item.GetResourceId() <= 0 {
|
||||
return map[string]any{}
|
||||
}
|
||||
resource := map[string]any{
|
||||
"resource_id": item.GetResourceId(),
|
||||
"resource_code": item.GetResourceCode(),
|
||||
"resource_type": item.GetResourceType(),
|
||||
"name": item.GetName(),
|
||||
"asset_url": item.GetAssetUrl(),
|
||||
"preview_url": item.GetPreviewUrl(),
|
||||
"animation_url": item.GetAnimationUrl(),
|
||||
"metadata_json": item.GetMetadataJson(),
|
||||
"entitlement_id": entitlementID,
|
||||
"expires_at_ms": expiresAtMS,
|
||||
}
|
||||
if item.GetResourceType() == appearanceResourceBadge {
|
||||
badgeForm, badgeKind, levelTrack := appearanceBadgeMetadataFields(item.GetMetadataJson())
|
||||
resource["badge_form"] = badgeForm
|
||||
resource["badge_kind"] = badgeKind
|
||||
resource["level_track"] = levelTrack
|
||||
}
|
||||
return resource
|
||||
}
|
||||
|
||||
func appearanceBadgeMetadataFields(raw string) (string, string, string) {
|
||||
payload := map[string]any{}
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &payload); err != nil {
|
||||
return "", "normal", ""
|
||||
}
|
||||
badgeForm := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["badge_form"])))
|
||||
if badgeForm != "strip" && badgeForm != "tile" {
|
||||
badgeForm = ""
|
||||
}
|
||||
badgeKind := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["badge_kind"])))
|
||||
if badgeKind != "level" {
|
||||
badgeKind = "normal"
|
||||
}
|
||||
levelTrack := strings.ToLower(strings.TrimSpace(fmt.Sprint(payload["level_track"])))
|
||||
switch levelTrack {
|
||||
case "wealth", "game", "charm":
|
||||
default:
|
||||
levelTrack = ""
|
||||
}
|
||||
return badgeForm, badgeKind, levelTrack
|
||||
}
|
||||
|
||||
func badgeItemsFromProto(items []*activityv1.BadgeDisplayItem, resources map[int64]map[string]any) []map[string]any {
|
||||
result := make([]map[string]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
// badge item 保留 activity 的展示位字段,同时在 resource 内嵌 wallet 资源素材。
|
||||
data := map[string]any{
|
||||
"slot": item.GetSlot(),
|
||||
"position": item.GetPosition(),
|
||||
"badge_form": item.GetBadgeForm(),
|
||||
"resource_id": item.GetResourceId(),
|
||||
"entitlement_id": item.GetEntitlementId(),
|
||||
"source_type": item.GetSourceType(),
|
||||
"source_id": item.GetSourceId(),
|
||||
"pin_mode": item.GetPinMode(),
|
||||
"updated_at_ms": item.GetUpdatedAtMs(),
|
||||
}
|
||||
if resource, ok := resources[item.GetResourceId()]; ok {
|
||||
data["resource"] = resource
|
||||
}
|
||||
result = append(result, data)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func badgeResourceIDs(resp *activityv1.ListMyBadgesResponse) []int64 {
|
||||
if resp == nil {
|
||||
return nil
|
||||
}
|
||||
// 三个展示区都可能引用 badge 资源;统一收集后去重解析素材。
|
||||
ids := make([]int64, 0, len(resp.GetStripBadges())+len(resp.GetProfileTileBadges())+len(resp.GetHonorBadges()))
|
||||
for _, item := range resp.GetStripBadges() {
|
||||
ids = append(ids, item.GetResourceId())
|
||||
}
|
||||
for _, item := range resp.GetProfileTileBadges() {
|
||||
ids = append(ids, item.GetResourceId())
|
||||
}
|
||||
for _, item := range resp.GetHonorBadges() {
|
||||
ids = append(ids, item.GetResourceId())
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func uniqueAppearanceResourceIDs(ids []int64) []int64 {
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
result := make([]int64, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[id]; exists {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
result = append(result, id)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -16,6 +16,8 @@ type Handler struct {
|
||||
userCountryClient client.UserCountryQueryClient
|
||||
userHostClient client.UserHostClient
|
||||
walletClient client.WalletClient
|
||||
growthLevelClient client.GrowthLevelClient
|
||||
achievementClient client.AchievementClient
|
||||
broadcastClient client.BroadcastClient
|
||||
}
|
||||
|
||||
@ -26,6 +28,8 @@ type Config struct {
|
||||
UserCountryClient client.UserCountryQueryClient
|
||||
UserHostClient client.UserHostClient
|
||||
WalletClient client.WalletClient
|
||||
GrowthLevelClient client.GrowthLevelClient
|
||||
AchievementClient client.AchievementClient
|
||||
BroadcastClient client.BroadcastClient
|
||||
}
|
||||
|
||||
@ -37,31 +41,40 @@ func New(config Config) *Handler {
|
||||
userCountryClient: config.UserCountryClient,
|
||||
userHostClient: config.UserHostClient,
|
||||
walletClient: config.WalletClient,
|
||||
growthLevelClient: config.GrowthLevelClient,
|
||||
achievementClient: config.AchievementClient,
|
||||
broadcastClient: config.BroadcastClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
||||
return httproutes.UserHandlers{
|
||||
ResolveDisplayUserID: h.resolveDisplayUserID,
|
||||
GetMyOverview: h.getMyOverview,
|
||||
GetMyIdentity: h.getMyIdentity,
|
||||
GetMyHostIdentity: h.getMyHostIdentity,
|
||||
GetMyRoleSummary: h.getMyRoleSummary,
|
||||
SearchHostAgencies: h.searchHostAgencies,
|
||||
ApplyToHostAgency: h.applyToHostAgency,
|
||||
CompleteMyOnboarding: h.completeMyOnboarding,
|
||||
UpdateMyProfile: h.updateMyProfile,
|
||||
ChangeMyCountry: h.changeMyCountry,
|
||||
ChangeMyDisplayUserID: h.changeMyDisplayUserID,
|
||||
ApplyMyPrettyDisplayUserID: h.applyMyPrettyDisplayUserID,
|
||||
ListMyProfileVisitors: h.listMyProfileVisitors,
|
||||
ListMyFollowing: h.listMyFollowing,
|
||||
ListMyFriends: h.listMyFriends,
|
||||
ListMyFriendApplications: h.listMyFriendApplications,
|
||||
GetMyProfile: h.getMyProfile,
|
||||
UserSocialAction: h.userSocialAction,
|
||||
SubmitReport: h.submitReport,
|
||||
ResolveDisplayUserID: h.resolveDisplayUserID,
|
||||
GetMyOverview: h.getMyOverview,
|
||||
GetMyIdentity: h.getMyIdentity,
|
||||
GetMyHostIdentity: h.getMyHostIdentity,
|
||||
GetMyRoleSummary: h.getMyRoleSummary,
|
||||
SearchHostAgencies: h.searchHostAgencies,
|
||||
ApplyToHostAgency: h.applyToHostAgency,
|
||||
GetAgencyCenterOverview: h.getAgencyCenterOverview,
|
||||
ListAgencyCenterHosts: h.listAgencyCenterHosts,
|
||||
ListAgencyCenterApplications: h.listAgencyCenterApplications,
|
||||
ReviewAgencyCenterApplication: h.reviewAgencyCenterApplication,
|
||||
RemoveAgencyCenterHost: h.removeAgencyCenterHost,
|
||||
CompleteMyOnboarding: h.completeMyOnboarding,
|
||||
UpdateMyProfile: h.updateMyProfile,
|
||||
ChangeMyCountry: h.changeMyCountry,
|
||||
ChangeMyDisplayUserID: h.changeMyDisplayUserID,
|
||||
ApplyMyPrettyDisplayUserID: h.applyMyPrettyDisplayUserID,
|
||||
ListMyProfileVisitors: h.listMyProfileVisitors,
|
||||
ListMyFollowing: h.listMyFollowing,
|
||||
ListMyFriends: h.listMyFriends,
|
||||
ListMyFriendApplications: h.listMyFriendApplications,
|
||||
GetMyProfile: h.getMyProfile,
|
||||
GetMyAppearance: h.getMyAppearance,
|
||||
GetUserAppearance: h.getUserAppearance,
|
||||
UserSocialAction: h.userSocialAction,
|
||||
SubmitReport: h.submitReport,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@ import (
|
||||
type WalletClient interface {
|
||||
// DebitGift 执行送礼扣费;失败时 room-service 不进入 Room Cell 状态提交。
|
||||
DebitGift(ctx context.Context, req *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error)
|
||||
// BatchDebitGift 在 wallet-service 单事务内结算多目标送礼;任一目标失败时整批失败。
|
||||
BatchDebitGift(ctx context.Context, req *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error)
|
||||
// GrantResourceGroup 发放宝箱奖励资源组;命令 ID 必须由 room-service 按宝箱结算事实生成。
|
||||
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error)
|
||||
}
|
||||
|
||||
@ -28,6 +28,12 @@ func (c *grpcWalletClient) DebitGift(ctx context.Context, req *walletv1.DebitGif
|
||||
return c.client.DebitGift(ctx, req)
|
||||
}
|
||||
|
||||
// BatchDebitGift 直接转调 wallet-service 批量送礼接口。
|
||||
func (c *grpcWalletClient) BatchDebitGift(ctx context.Context, req *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
|
||||
// 多目标送礼必须由 wallet-service 单事务结算,room-service 不能自己循环单目标扣费。
|
||||
return c.client.BatchDebitGift(ctx, req)
|
||||
}
|
||||
|
||||
// GrantResourceGroup 直接转调 wallet-service 资源组发放接口。
|
||||
func (c *grpcWalletClient) GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
|
||||
// 宝箱奖励以 resource group 为后台配置单位,wallet-service 负责展开资产和权益。
|
||||
|
||||
@ -120,6 +120,8 @@ func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.Room
|
||||
}
|
||||
base.ActorUserID = body.GetUserId()
|
||||
base.TargetUserID = body.GetUserId()
|
||||
// RoomUserJoined 的座驾必须从 outbox body 取快照,不能在补偿投递时按当前佩戴重算。
|
||||
base.EntryVehicle = roomEntryVehicleFromEvent(body.GetEntryVehicle())
|
||||
base.Attributes = map[string]string{"role": body.GetRole()}
|
||||
return base, true, nil
|
||||
case "RoomUserLeft":
|
||||
@ -356,6 +358,23 @@ func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.Room
|
||||
}
|
||||
}
|
||||
|
||||
func roomEntryVehicleFromEvent(item *roomeventsv1.RoomEntryVehicleSnapshot) *tencentim.RoomEntryVehicleSnapshot {
|
||||
if item == nil || item.GetResourceId() <= 0 {
|
||||
return nil
|
||||
}
|
||||
return &tencentim.RoomEntryVehicleSnapshot{
|
||||
ResourceID: item.GetResourceId(),
|
||||
ResourceCode: item.GetResourceCode(),
|
||||
Name: item.GetName(),
|
||||
AssetURL: item.GetAssetUrl(),
|
||||
PreviewURL: item.GetPreviewUrl(),
|
||||
AnimationURL: item.GetAnimationUrl(),
|
||||
MetadataJSON: item.GetMetadataJson(),
|
||||
EntitlementID: item.GetEntitlementId(),
|
||||
ExpiresAtMS: item.GetExpiresAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func eventTypeForClient(eventType string) string {
|
||||
// protobuf 事件名使用 PascalCase,客户端 TIMCustomElem 使用 snake_case 作为稳定协议字段。
|
||||
switch eventType {
|
||||
|
||||
@ -32,6 +32,39 @@ func TestRoomEventFromEnvelopeSkipsNonIMEvents(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomUserJoinedCarriesEntryVehicleSnapshot(t *testing.T) {
|
||||
record, err := outbox.Build("room-vehicle", "RoomUserJoined", 9, time.Now(), &roomeventsv1.RoomUserJoined{
|
||||
UserId: 42,
|
||||
Role: "audience",
|
||||
VisibleRegionId: 86,
|
||||
EntryVehicle: &roomeventsv1.RoomEntryVehicleSnapshot{
|
||||
ResourceId: 7001,
|
||||
ResourceCode: "vehicle_gold",
|
||||
Name: "Gold Vehicle",
|
||||
AssetUrl: "https://cdn.example.com/vehicle.png",
|
||||
PreviewUrl: "https://cdn.example.com/vehicle-preview.png",
|
||||
AnimationUrl: "https://cdn.example.com/vehicle.svga",
|
||||
MetadataJson: `{"speed":"fast"}`,
|
||||
EntitlementId: "ent-vehicle",
|
||||
ExpiresAtMs: 1999999999999,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Build RoomUserJoined envelope failed: %v", err)
|
||||
}
|
||||
|
||||
event, publish, err := roomEventFromEnvelope(record.Envelope)
|
||||
if err != nil {
|
||||
t.Fatalf("RoomUserJoined should decode: %v", err)
|
||||
}
|
||||
if !publish {
|
||||
t.Fatal("RoomUserJoined must publish Tencent IM message")
|
||||
}
|
||||
if event.EntryVehicle == nil || event.EntryVehicle.ResourceID != 7001 || event.EntryVehicle.AnimationURL != "https://cdn.example.com/vehicle.svga" {
|
||||
t.Fatalf("entry vehicle snapshot mismatch: %+v", event.EntryVehicle)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomBackgroundChangedPublishesDedicatedIMEvent(t *testing.T) {
|
||||
record, err := outbox.Build("room-bg", "RoomBackgroundChanged", 7, time.Now(), &roomeventsv1.RoomBackgroundChanged{
|
||||
ActorUserId: 42,
|
||||
|
||||
@ -105,10 +105,24 @@ type SetRoomBackground struct {
|
||||
func (SetRoomBackground) Type() string { return "set_room_background" }
|
||||
|
||||
// JoinRoom 定义业务进房请求。
|
||||
type EntryVehicleSnapshot struct {
|
||||
ResourceID int64 `json:"resource_id,omitempty"`
|
||||
ResourceCode string `json:"resource_code,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
AssetURL string `json:"asset_url,omitempty"`
|
||||
PreviewURL string `json:"preview_url,omitempty"`
|
||||
AnimationURL string `json:"animation_url,omitempty"`
|
||||
MetadataJSON string `json:"metadata_json,omitempty"`
|
||||
EntitlementID string `json:"entitlement_id,omitempty"`
|
||||
ExpiresAtMS int64 `json:"expires_at_ms,omitempty"`
|
||||
}
|
||||
|
||||
type JoinRoom struct {
|
||||
Base
|
||||
// Role 是用户在房间内的轻量角色,默认 audience。
|
||||
Role string `json:"role"`
|
||||
// EntryVehicle 是 gateway 在进房时解析出的当前有效佩戴座驾快照,用于进房 IM。
|
||||
EntryVehicle EntryVehicleSnapshot `json:"entry_vehicle,omitempty"`
|
||||
}
|
||||
|
||||
// Type 返回命令类型。
|
||||
@ -358,9 +372,9 @@ type SendGift struct {
|
||||
Base
|
||||
// TargetType 当前只支持 user;保留字段避免后续 all_mic/all_room/couple 再改命令结构。
|
||||
TargetType string `json:"target_type"`
|
||||
// TargetUserIDs 是新协议目标用户集合;当前 target_type=user 时只允许一个用户。
|
||||
// TargetUserIDs 是新协议目标用户集合;target_type=user 时允许多个房间内用户。
|
||||
TargetUserIDs []int64 `json:"target_user_ids,omitempty"`
|
||||
// TargetUserID 是礼物接收方,当前版本要求接收方在房间内。
|
||||
// TargetUserID 是兼容单目标字段和批量主目标,所有接收方仍要求在房间内。
|
||||
TargetUserID int64 `json:"target_user_id"`
|
||||
// GiftID 是礼物配置标识,具体配置有效性当前由调用方和 wallet 链路保证。
|
||||
GiftID string `json:"gift_id"`
|
||||
|
||||
@ -20,6 +20,10 @@ func (followTestWallet) DebitGift(context.Context, *walletv1.DebitGiftRequest) (
|
||||
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-follow-test"}, nil
|
||||
}
|
||||
|
||||
func (followTestWallet) BatchDebitGift(context.Context, *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
|
||||
return &walletv1.BatchDebitGiftResponse{Aggregate: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-follow-test"}}, nil
|
||||
}
|
||||
|
||||
func (followTestWallet) GrantResourceGroup(context.Context, *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
|
||||
return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{GrantId: "grant-follow-test"}}, nil
|
||||
}
|
||||
|
||||
@ -3,9 +3,11 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||
roomv1 "hyapp.local/api/proto/room/v1"
|
||||
@ -39,7 +41,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
if cmd.TargetType == "" {
|
||||
cmd.TargetType = "user"
|
||||
}
|
||||
if cmd.TargetType == "user" && len(cmd.TargetUserIDs) == 1 {
|
||||
if cmd.TargetType == "user" && len(cmd.TargetUserIDs) > 0 {
|
||||
cmd.TargetUserID = cmd.TargetUserIDs[0]
|
||||
}
|
||||
|
||||
@ -53,12 +55,14 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
// v1 HTTP 契约已预留 all_mic/all_room/couple;账务拆单和房间表现策略补齐前先 fail-close。
|
||||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_type is unsupported")
|
||||
}
|
||||
if len(cmd.TargetUserIDs) != 1 || cmd.TargetUserID <= 0 {
|
||||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_ids must contain one user")
|
||||
if len(cmd.TargetUserIDs) == 0 || cmd.TargetUserID <= 0 {
|
||||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_ids is required")
|
||||
}
|
||||
if _, exists := current.OnlineUsers[cmd.TargetUserID]; !exists {
|
||||
// v1 要求礼物目标在房间内,避免给不存在的房间表现目标计榜。
|
||||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not in room")
|
||||
for _, targetUserID := range cmd.TargetUserIDs {
|
||||
if _, exists := current.OnlineUsers[targetUserID]; !exists {
|
||||
// 每个接收方都必须在房间 presence 内;批量扣费前先全部校验,避免钱包出现无房间表现的入账。
|
||||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not in room")
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.GiftID == "" || cmd.GiftCount <= 0 {
|
||||
@ -100,27 +104,16 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
}
|
||||
|
||||
var billing *walletv1.DebitGiftResponse
|
||||
var targetBillings []giftTargetBilling
|
||||
var walletDebitMS int64
|
||||
settledCommand := cmd
|
||||
var luckyGift *roomv1.LuckyGiftDrawResult
|
||||
var luckyGifts []*roomv1.LuckyGiftDrawResult
|
||||
if err := s.withLuckyGiftSendLock(ctx, luckyEnabled, cmd.ActorUserID(), func() error {
|
||||
// 钱包扣费在房间状态变更前完成;失败时不写 command log、不进入 Room Cell 提交态。
|
||||
walletStartedAt := time.Now()
|
||||
var err error
|
||||
billing, err = s.wallet.DebitGift(ctx, &walletv1.DebitGiftRequest{
|
||||
CommandId: cmd.ID(),
|
||||
RoomId: cmd.RoomID(),
|
||||
SenderUserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
GiftId: cmd.GiftID,
|
||||
GiftCount: cmd.GiftCount,
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
RegionId: roomMeta.VisibleRegionID,
|
||||
SenderRegionId: cmd.SenderRegionID,
|
||||
TargetIsHost: cmd.TargetIsHost,
|
||||
TargetHostRegionId: cmd.TargetHostRegionID,
|
||||
TargetAgencyOwnerUserId: cmd.TargetAgencyOwnerUserID,
|
||||
})
|
||||
billing, targetBillings, err = s.debitGiftTargets(ctx, cmd, roomMeta)
|
||||
walletDebitMS = elapsedMS(walletStartedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -141,35 +134,15 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
luckyEnabled = s.shouldDrawLuckyGift(cmd.PoolID, billing.GetGiftTypeCode())
|
||||
}
|
||||
if luckyEnabled {
|
||||
// 抽奖必须发生在扣费成功之后;activity-service 只接收钱包结算后的 coin_spent,不信任客户端价格。
|
||||
drawResp, err := s.luckyGift.ExecuteLuckyGiftDraw(ctx, &activityv1.ExecuteLuckyGiftDrawRequest{
|
||||
LuckyGift: &activityv1.LuckyGiftMeta{
|
||||
Meta: activityMetaFromRoom(ctx, req.GetMeta()),
|
||||
CommandId: cmd.ID(),
|
||||
UserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
// 目前没有独立设备 ID 字段,优先用 session_id;没有时退回 command_id,保证风控 scope 不为空。
|
||||
DeviceId: luckyGiftDeviceID(cmd),
|
||||
RoomId: cmd.RoomID(),
|
||||
AnchorId: luckyGiftAnchorID(roomMeta),
|
||||
GiftId: cmd.GiftID,
|
||||
GiftCount: cmd.GiftCount,
|
||||
CoinSpent: billing.GetCoinSpent(),
|
||||
// 房间链路统一使用 UTC epoch ms;不能把本地时区时间传给活动风控窗口。
|
||||
PaidAtMs: now.UTC().UnixMilli(),
|
||||
PoolId: cmd.PoolID,
|
||||
VisibleRegionId: roomMeta.VisibleRegionID,
|
||||
},
|
||||
})
|
||||
luckyGifts, err = s.executeLuckyGiftDraws(ctx, req.GetMeta(), cmd, roomMeta, targetBillings, now)
|
||||
if err != nil {
|
||||
// 抽奖失败时整笔送礼失败并依赖 wallet 幂等重试;不能落普通礼物后丢失幸运礼物抽奖事实。
|
||||
return err
|
||||
}
|
||||
if drawResp == nil {
|
||||
return xerr.New(xerr.Unavailable, "lucky gift draw response is empty")
|
||||
if len(luckyGifts) > 0 {
|
||||
// lucky_gifts 保留每个收礼目标的独立抽奖事实;lucky_gift 返回整次送礼的聚合表现,复用已有批量送礼的倍率累加语义。
|
||||
luckyGift = aggregateLuckyGiftResults(cmd.ID(), luckyGifts)
|
||||
}
|
||||
// 同步返回只给当前送礼用户做即时表现;金币余额以 activity/wallet 快路径回执为准,房间开奖结果仍由 activity outbox 补偿。
|
||||
luckyGift = luckyGiftResultFromProto(drawResp.GetResult())
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
@ -190,20 +163,24 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
return mutationResult{}, nil, err
|
||||
}
|
||||
|
||||
// 送礼事件、热度事件和榜单事件使用同一个房间版本,方便消费者关联同一次命令。
|
||||
giftEvent, err := outbox.Build(current.RoomID, "RoomGiftSent", current.Version, now, &roomeventsv1.RoomGiftSent{
|
||||
SenderUserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
GiftId: cmd.GiftID,
|
||||
PoolId: cmd.PoolID,
|
||||
GiftCount: cmd.GiftCount,
|
||||
GiftValue: heatValue,
|
||||
BillingReceiptId: billing.GetBillingReceiptId(),
|
||||
VisibleRegionId: roomMeta.VisibleRegionID,
|
||||
CommandId: cmd.ID(),
|
||||
})
|
||||
if err != nil {
|
||||
return mutationResult{}, nil, err
|
||||
// 送礼事件按 target 拆分,保证 IM、统计和礼物墙消费者拿到准确的接收方和账务回执。
|
||||
giftEvents := make([]outbox.Record, 0, len(targetBillings))
|
||||
for _, targetBilling := range targetBillings {
|
||||
giftEvent, err := outbox.Build(current.RoomID, "RoomGiftSent", current.Version, now, &roomeventsv1.RoomGiftSent{
|
||||
SenderUserId: cmd.ActorUserID(),
|
||||
TargetUserId: targetBilling.TargetUserID,
|
||||
GiftId: cmd.GiftID,
|
||||
PoolId: cmd.PoolID,
|
||||
GiftCount: cmd.GiftCount,
|
||||
GiftValue: targetBilling.Billing.GetHeatValue(),
|
||||
BillingReceiptId: targetBilling.Billing.GetBillingReceiptId(),
|
||||
VisibleRegionId: roomMeta.VisibleRegionID,
|
||||
CommandId: targetBilling.CommandID,
|
||||
})
|
||||
if err != nil {
|
||||
return mutationResult{}, nil, err
|
||||
}
|
||||
giftEvents = append(giftEvents, giftEvent)
|
||||
}
|
||||
|
||||
heatEvent, err := outbox.Build(current.RoomID, "RoomHeatChanged", current.Version, now, &roomeventsv1.RoomHeatChanged{
|
||||
@ -224,7 +201,9 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
return mutationResult{}, nil, err
|
||||
}
|
||||
|
||||
records := []outbox.Record{giftEvent, heatEvent, rankEvent}
|
||||
records := make([]outbox.Record, 0, len(giftEvents)+4)
|
||||
records = append(records, giftEvents...)
|
||||
records = append(records, heatEvent, rankEvent)
|
||||
if treasureApply.progressEvent != nil {
|
||||
progressEvent, err := outbox.Build(current.RoomID, "RoomTreasureProgressChanged", current.Version, now, treasureApply.progressEvent)
|
||||
if err != nil {
|
||||
@ -246,6 +225,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
roomHeat: current.Heat,
|
||||
giftRank: cloneProtoRank(current.GiftRank),
|
||||
luckyGift: luckyGift,
|
||||
luckyGifts: luckyGifts,
|
||||
commandPayload: commandPayload,
|
||||
walletDebitMS: walletDebitMS,
|
||||
roomGiftLeaderboard: &RoomGiftLeaderboardIncrement{
|
||||
@ -265,7 +245,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
},
|
||||
syncEvent: &tencentim.RoomEvent{
|
||||
// 同步广播只选 GiftSent 主事件作为客户端房间系统消息,Heat/Rank 通过字段携带。
|
||||
EventID: giftEvent.EventID,
|
||||
EventID: giftEvents[0].EventID,
|
||||
RoomID: current.RoomID,
|
||||
EventType: "room_gift_sent",
|
||||
ActorUserID: cmd.ActorUserID(),
|
||||
@ -280,6 +260,8 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
"billing_receipt_id": billing.GetBillingReceiptId(),
|
||||
"coin_spent": fmt.Sprintf("%d", settledCommand.CoinSpent),
|
||||
"gift_point_added": fmt.Sprintf("%d", settledCommand.GiftPointAdded),
|
||||
"target_count": fmt.Sprintf("%d", len(cmd.TargetUserIDs)),
|
||||
"target_user_ids": giftTargetUserIDsAttribute(cmd.TargetUserIDs),
|
||||
"price_version": settledCommand.PriceVersion,
|
||||
"host_period_diamond_added": fmt.Sprintf("%d", settledCommand.HostPeriodDiamondAdded),
|
||||
"host_period_cycle_key": settledCommand.HostPeriodCycleKey,
|
||||
@ -299,9 +281,112 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
Room: result.snapshot,
|
||||
Treasure: result.snapshot.GetTreasure(),
|
||||
LuckyGift: result.luckyGift,
|
||||
LuckyGifts: result.luckyGifts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type giftTargetBilling struct {
|
||||
TargetUserID int64
|
||||
CommandID string
|
||||
Billing *walletv1.DebitGiftResponse
|
||||
}
|
||||
|
||||
func (s *Service) debitGiftTargets(ctx context.Context, cmd command.SendGift, roomMeta RoomMeta) (*walletv1.DebitGiftResponse, []giftTargetBilling, error) {
|
||||
if len(cmd.TargetUserIDs) == 1 {
|
||||
// 单目标保留原 wallet command_id,避免改变已有幂等键、账务回执和排障路径。
|
||||
billing, err := s.wallet.DebitGift(ctx, &walletv1.DebitGiftRequest{
|
||||
CommandId: cmd.ID(),
|
||||
RoomId: cmd.RoomID(),
|
||||
SenderUserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
GiftId: cmd.GiftID,
|
||||
GiftCount: cmd.GiftCount,
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
RegionId: roomMeta.VisibleRegionID,
|
||||
SenderRegionId: cmd.SenderRegionID,
|
||||
TargetIsHost: cmd.TargetIsHost,
|
||||
TargetHostRegionId: cmd.TargetHostRegionID,
|
||||
TargetAgencyOwnerUserId: cmd.TargetAgencyOwnerUserID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return billing, []giftTargetBilling{{TargetUserID: cmd.TargetUserID, CommandID: cmd.ID(), Billing: billing}}, nil
|
||||
}
|
||||
|
||||
targets := make([]*walletv1.DebitGiftTarget, 0, len(cmd.TargetUserIDs))
|
||||
for _, targetUserID := range cmd.TargetUserIDs {
|
||||
target := &walletv1.DebitGiftTarget{
|
||||
CommandId: giftTargetCommandID(cmd.ID(), targetUserID, len(cmd.TargetUserIDs)),
|
||||
TargetUserId: targetUserID,
|
||||
}
|
||||
if targetUserID == cmd.TargetUserID && cmd.TargetIsHost {
|
||||
// 现有 room proto 只有单个 host scope;多目标时只允许把该快照用于对应的第一个目标,不能错误扩散到其他接收方。
|
||||
target.TargetIsHost = true
|
||||
target.TargetHostRegionId = cmd.TargetHostRegionID
|
||||
target.TargetAgencyOwnerUserId = cmd.TargetAgencyOwnerUserID
|
||||
}
|
||||
targets = append(targets, target)
|
||||
}
|
||||
resp, err := s.wallet.BatchDebitGift(ctx, &walletv1.BatchDebitGiftRequest{
|
||||
CommandId: cmd.ID(),
|
||||
RoomId: cmd.RoomID(),
|
||||
SenderUserId: cmd.ActorUserID(),
|
||||
GiftId: cmd.GiftID,
|
||||
GiftCount: cmd.GiftCount,
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
RegionId: roomMeta.VisibleRegionID,
|
||||
SenderRegionId: cmd.SenderRegionID,
|
||||
Targets: targets,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
targetBillings, err := giftTargetBillingsFromBatch(resp, cmd.TargetUserIDs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return resp.GetAggregate(), targetBillings, nil
|
||||
}
|
||||
|
||||
func (s *Service) executeLuckyGiftDraws(ctx context.Context, meta *roomv1.RequestMeta, cmd command.SendGift, roomMeta RoomMeta, targetBillings []giftTargetBilling, now time.Time) ([]*roomv1.LuckyGiftDrawResult, error) {
|
||||
results := make([]*roomv1.LuckyGiftDrawResult, 0, len(targetBillings))
|
||||
for _, targetBilling := range targetBillings {
|
||||
if targetBilling.Billing == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "wallet target debit response is empty")
|
||||
}
|
||||
// 抽奖必须按每个收礼目标独立落事实;command_id、target_user_id 和 coin_spent 都来自该目标的 wallet 子交易。
|
||||
drawResp, err := s.luckyGift.ExecuteLuckyGiftDraw(ctx, &activityv1.ExecuteLuckyGiftDrawRequest{
|
||||
LuckyGift: &activityv1.LuckyGiftMeta{
|
||||
Meta: activityMetaFromRoom(ctx, meta),
|
||||
CommandId: targetBilling.CommandID,
|
||||
UserId: cmd.ActorUserID(),
|
||||
TargetUserId: targetBilling.TargetUserID,
|
||||
// 目前没有独立设备 ID 字段,优先用 session_id;没有时退回目标子 command_id,保证每次抽奖 scope 不为空。
|
||||
DeviceId: luckyGiftDeviceID(cmd, targetBilling.CommandID),
|
||||
RoomId: cmd.RoomID(),
|
||||
AnchorId: luckyGiftAnchorID(roomMeta),
|
||||
GiftId: cmd.GiftID,
|
||||
GiftCount: cmd.GiftCount,
|
||||
CoinSpent: targetBilling.Billing.GetCoinSpent(),
|
||||
// 房间链路统一使用 UTC epoch ms;不能把本地时区时间传给活动风控窗口。
|
||||
PaidAtMs: now.UTC().UnixMilli(),
|
||||
PoolId: cmd.PoolID,
|
||||
VisibleRegionId: roomMeta.VisibleRegionID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if drawResp == nil || drawResp.GetResult() == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "lucky gift draw response is empty")
|
||||
}
|
||||
// 同步返回只给当前送礼用户做即时表现;金币余额以 activity/wallet 快路径回执为准,房间开奖结果仍由 activity outbox 补偿。
|
||||
results = append(results, luckyGiftResultFromProto(drawResp.GetResult(), targetBilling.TargetUserID))
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func roomGiftLeaderboardCoinSpent(billing *walletv1.DebitGiftResponse) int64 {
|
||||
if billing == nil {
|
||||
return 0
|
||||
@ -352,10 +437,13 @@ func activityMetaFromRoom(ctx context.Context, meta *roomv1.RequestMeta) *activi
|
||||
}
|
||||
}
|
||||
|
||||
func luckyGiftDeviceID(cmd command.SendGift) string {
|
||||
func luckyGiftDeviceID(cmd command.SendGift, fallbackCommandID string) string {
|
||||
if value := strings.TrimSpace(cmd.SessionID); value != "" {
|
||||
return value
|
||||
}
|
||||
if value := strings.TrimSpace(fallbackCommandID); value != "" {
|
||||
return value
|
||||
}
|
||||
return cmd.ID()
|
||||
}
|
||||
|
||||
@ -366,7 +454,7 @@ func luckyGiftAnchorID(roomMeta RoomMeta) string {
|
||||
return fmt.Sprintf("%d", roomMeta.OwnerUserID)
|
||||
}
|
||||
|
||||
func luckyGiftResultFromProto(result *activityv1.LuckyGiftDrawResult) *roomv1.LuckyGiftDrawResult {
|
||||
func luckyGiftResultFromProto(result *activityv1.LuckyGiftDrawResult, targetUserID int64) *roomv1.LuckyGiftDrawResult {
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
@ -390,9 +478,66 @@ func luckyGiftResultFromProto(result *activityv1.LuckyGiftDrawResult) *roomv1.Lu
|
||||
CreatedAtMs: result.GetCreatedAtMs(),
|
||||
WalletTransactionId: result.GetWalletTransactionId(),
|
||||
CoinBalanceAfter: result.GetCoinBalanceAfter(),
|
||||
TargetUserId: targetUserID,
|
||||
}
|
||||
}
|
||||
|
||||
func aggregateLuckyGiftResults(commandID string, results []*roomv1.LuckyGiftDrawResult) *roomv1.LuckyGiftDrawResult {
|
||||
if len(results) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(results) == 1 {
|
||||
return results[0]
|
||||
}
|
||||
aggregate := proto.Clone(results[0]).(*roomv1.LuckyGiftDrawResult)
|
||||
aggregate.CommandId = commandID
|
||||
aggregate.SelectedTierId = "batch"
|
||||
aggregate.TargetUserId = 0
|
||||
aggregate.WalletTransactionId = ""
|
||||
aggregate.MultiplierPpm = 0
|
||||
aggregate.BaseRewardCoins = 0
|
||||
aggregate.RoomAtmosphereRewardCoins = 0
|
||||
aggregate.ActivitySubsidyCoins = 0
|
||||
aggregate.EffectiveRewardCoins = 0
|
||||
aggregate.RewardStatus = "granted"
|
||||
aggregate.StageFeedback = false
|
||||
aggregate.HighMultiplier = false
|
||||
aggregate.CoinBalanceAfter = 0
|
||||
for _, result := range results {
|
||||
if result == nil {
|
||||
continue
|
||||
}
|
||||
aggregate.RuleVersion = result.GetRuleVersion()
|
||||
aggregate.ExperiencePool = result.GetExperiencePool()
|
||||
aggregate.CreatedAtMs = result.GetCreatedAtMs()
|
||||
aggregate.MultiplierPpm += result.GetMultiplierPpm()
|
||||
aggregate.BaseRewardCoins += result.GetBaseRewardCoins()
|
||||
aggregate.RoomAtmosphereRewardCoins += result.GetRoomAtmosphereRewardCoins()
|
||||
aggregate.ActivitySubsidyCoins += result.GetActivitySubsidyCoins()
|
||||
aggregate.EffectiveRewardCoins += result.GetEffectiveRewardCoins()
|
||||
aggregate.StageFeedback = aggregate.StageFeedback || result.GetStageFeedback()
|
||||
aggregate.HighMultiplier = aggregate.HighMultiplier || result.GetHighMultiplier()
|
||||
if result.GetCoinBalanceAfter() > 0 {
|
||||
// 多目标会产生多笔返奖交易;聚合字段不能表达多个交易号,只保留最后一次快路径回执后的余额给客户端刷新资产。
|
||||
aggregate.CoinBalanceAfter = result.GetCoinBalanceAfter()
|
||||
}
|
||||
switch result.GetRewardStatus() {
|
||||
case "pending":
|
||||
aggregate.RewardStatus = "pending"
|
||||
case "failed":
|
||||
if aggregate.RewardStatus != "pending" {
|
||||
aggregate.RewardStatus = "failed"
|
||||
}
|
||||
case "granted":
|
||||
default:
|
||||
if aggregate.RewardStatus == "" {
|
||||
aggregate.RewardStatus = result.GetRewardStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
return aggregate
|
||||
}
|
||||
|
||||
func normalizeGiftTargetType(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
switch raw {
|
||||
@ -419,6 +564,52 @@ func normalizeGiftTargetUserIDs(targetUserID int64, targetUserIDs []int64) []int
|
||||
return ids
|
||||
}
|
||||
|
||||
func giftTargetCommandID(commandID string, targetUserID int64, targetCount int) string {
|
||||
if targetCount <= 1 {
|
||||
return commandID
|
||||
}
|
||||
// 钱包以 command_id 做幂等主键;多目标必须按接收方派生稳定子命令,才能在同一批内保存独立交易。
|
||||
return fmt.Sprintf("%s:target:%d", commandID, targetUserID)
|
||||
}
|
||||
|
||||
func giftTargetBillingsFromBatch(resp *walletv1.BatchDebitGiftResponse, targetUserIDs []int64) ([]giftTargetBilling, error) {
|
||||
if resp == nil || resp.GetAggregate() == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "wallet batch debit response is empty")
|
||||
}
|
||||
receipts := make(map[int64]*walletv1.BatchDebitGiftReceipt, len(resp.GetReceipts()))
|
||||
for _, receipt := range resp.GetReceipts() {
|
||||
if receipt.GetTargetUserId() <= 0 || receipt.GetBilling() == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "wallet batch debit receipt is incomplete")
|
||||
}
|
||||
receipts[receipt.GetTargetUserId()] = receipt
|
||||
}
|
||||
targetBillings := make([]giftTargetBilling, 0, len(targetUserIDs))
|
||||
for _, targetUserID := range targetUserIDs {
|
||||
receipt := receipts[targetUserID]
|
||||
if receipt == nil {
|
||||
// 批量钱包响应必须覆盖每个房间目标;少一条就不能提交房间表现和 outbox。
|
||||
return nil, xerr.New(xerr.Unavailable, "wallet batch debit receipt is missing")
|
||||
}
|
||||
targetBillings = append(targetBillings, giftTargetBilling{
|
||||
TargetUserID: targetUserID,
|
||||
CommandID: receipt.GetCommandId(),
|
||||
Billing: receipt.GetBilling(),
|
||||
})
|
||||
}
|
||||
return targetBillings, nil
|
||||
}
|
||||
|
||||
func giftTargetUserIDsAttribute(targetUserIDs []int64) string {
|
||||
if len(targetUserIDs) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(targetUserIDs))
|
||||
for _, targetUserID := range targetUserIDs {
|
||||
parts = append(parts, strconv.FormatInt(targetUserID, 10))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func findRankItem(items []state.RankItem, userID int64) state.RankItem {
|
||||
// 送礼后通常能找到发送方榜单项;找不到时返回 user_id 占位避免空事件。
|
||||
for _, item := range items {
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
roomv1 "hyapp.local/api/proto/room/v1"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
@ -16,8 +17,9 @@ import (
|
||||
)
|
||||
|
||||
type luckyGiftTestClient struct {
|
||||
checks []*activityv1.CheckLuckyGiftRequest
|
||||
draws []*activityv1.ExecuteLuckyGiftDrawRequest
|
||||
checks []*activityv1.CheckLuckyGiftRequest
|
||||
draws []*activityv1.ExecuteLuckyGiftDrawRequest
|
||||
drawResults []*activityv1.LuckyGiftDrawResult
|
||||
}
|
||||
|
||||
func (c *luckyGiftTestClient) CheckLuckyGift(_ context.Context, req *activityv1.CheckLuckyGiftRequest) (*activityv1.CheckLuckyGiftResponse, error) {
|
||||
@ -35,6 +37,23 @@ func (c *luckyGiftTestClient) CheckLuckyGift(_ context.Context, req *activityv1.
|
||||
func (c *luckyGiftTestClient) ExecuteLuckyGiftDraw(_ context.Context, req *activityv1.ExecuteLuckyGiftDrawRequest) (*activityv1.ExecuteLuckyGiftDrawResponse, error) {
|
||||
c.draws = append(c.draws, req)
|
||||
meta := req.GetLuckyGift()
|
||||
if len(c.drawResults) > 0 {
|
||||
index := len(c.draws) - 1
|
||||
if index >= len(c.drawResults) {
|
||||
index = len(c.drawResults) - 1
|
||||
}
|
||||
result := proto.Clone(c.drawResults[index]).(*activityv1.LuckyGiftDrawResult)
|
||||
if result.CommandId == "" {
|
||||
result.CommandId = meta.GetCommandId()
|
||||
}
|
||||
if result.PoolId == "" {
|
||||
result.PoolId = meta.GetPoolId()
|
||||
}
|
||||
if result.GiftId == "" {
|
||||
result.GiftId = meta.GetGiftId()
|
||||
}
|
||||
return &activityv1.ExecuteLuckyGiftDrawResponse{Result: result}, nil
|
||||
}
|
||||
return &activityv1.ExecuteLuckyGiftDrawResponse{Result: &activityv1.LuckyGiftDrawResult{
|
||||
DrawId: "lucky_draw_test",
|
||||
CommandId: meta.GetCommandId(),
|
||||
@ -63,7 +82,32 @@ func TestSendGiftReturnsLuckyGiftDrawResult(t *testing.T) {
|
||||
HeatValue: 100,
|
||||
GiftTypeCode: "super_lucky",
|
||||
}}}
|
||||
luckyGift := &luckyGiftTestClient{}
|
||||
luckyGift := &luckyGiftTestClient{drawResults: []*activityv1.LuckyGiftDrawResult{
|
||||
{
|
||||
DrawId: "lucky_draw_target_202",
|
||||
SelectedTierId: "novice_2x",
|
||||
MultiplierPpm: 2_000_000,
|
||||
BaseRewardCoins: 200,
|
||||
EffectiveRewardCoins: 200,
|
||||
RewardStatus: "granted",
|
||||
WalletTransactionId: "wallet_tx_lucky_202",
|
||||
CoinBalanceAfter: 8800,
|
||||
CreatedAtMs: 1_779_258_000_000,
|
||||
},
|
||||
{
|
||||
DrawId: "lucky_draw_target_303",
|
||||
SelectedTierId: "normal_3x",
|
||||
MultiplierPpm: 3_000_000,
|
||||
BaseRewardCoins: 300,
|
||||
EffectiveRewardCoins: 300,
|
||||
RewardStatus: "granted",
|
||||
WalletTransactionId: "wallet_tx_lucky_303",
|
||||
CoinBalanceAfter: 9100,
|
||||
CreatedAtMs: 1_779_258_000_001,
|
||||
StageFeedback: true,
|
||||
HighMultiplier: true,
|
||||
},
|
||||
}}
|
||||
svc := roomservice.New(roomservice.Config{
|
||||
NodeID: "node-lucky-test",
|
||||
LeaseTTL: 10 * time.Second,
|
||||
@ -99,6 +143,9 @@ func TestSendGiftReturnsLuckyGiftDrawResult(t *testing.T) {
|
||||
if resp.GetLuckyGift().GetDrawId() != "lucky_draw_test" || resp.GetLuckyGift().GetMultiplierPpm() != 2_000_000 || resp.GetLuckyGift().GetEffectiveRewardCoins() != 200 || resp.GetLuckyGift().GetRewardStatus() != "granted" || resp.GetLuckyGift().GetWalletTransactionId() != "wallet_tx_lucky" || resp.GetLuckyGift().GetCoinBalanceAfter() != 8800 {
|
||||
t.Fatalf("lucky gift result mismatch: %+v", resp.GetLuckyGift())
|
||||
}
|
||||
if len(resp.GetLuckyGifts()) != 1 || resp.GetLuckyGifts()[0].GetTargetUserId() != viewerID {
|
||||
t.Fatalf("single-target lucky_gifts response mismatch: %+v", resp.GetLuckyGifts())
|
||||
}
|
||||
if len(luckyGift.checks) != 1 || luckyGift.checks[0].GetPoolId() != "super_lucky" || luckyGift.checks[0].GetGiftId() != "rose" {
|
||||
t.Fatalf("lucky check request mismatch: %+v", luckyGift.checks)
|
||||
}
|
||||
@ -114,3 +161,76 @@ func TestSendGiftReturnsLuckyGiftDrawResult(t *testing.T) {
|
||||
t.Fatalf("lucky draw meta mismatch: %+v", drawMeta)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendGiftReturnsLuckyGiftDrawResultsForMultipleTargets(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
wallet := &treasureTestWallet{debits: []*walletv1.DebitGiftResponse{
|
||||
{BillingReceiptId: "receipt-lucky-202", CoinSpent: 100, ChargeAmount: 100, GiftPointAdded: 100, HeatValue: 100, GiftTypeCode: "super_lucky"},
|
||||
{BillingReceiptId: "receipt-lucky-303", CoinSpent: 200, ChargeAmount: 200, GiftPointAdded: 200, HeatValue: 200, GiftTypeCode: "super_lucky"},
|
||||
}}
|
||||
luckyGift := &luckyGiftTestClient{}
|
||||
svc := roomservice.New(roomservice.Config{
|
||||
NodeID: "node-lucky-multi-test",
|
||||
LeaseTTL: 10 * time.Second,
|
||||
RankLimit: 20,
|
||||
SnapshotEveryN: 1,
|
||||
}, router.NewMemoryDirectory(), repository, wallet, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher(), luckyGift)
|
||||
|
||||
roomID := "room-lucky-gift-multi"
|
||||
ownerID := int64(101)
|
||||
firstTargetID := int64(202)
|
||||
secondTargetID := int64(303)
|
||||
createTreasureRoom(t, ctx, svc, roomID, ownerID, 9001)
|
||||
joinTreasureRoom(t, ctx, svc, roomID, firstTargetID)
|
||||
joinTreasureRoom(t, ctx, svc, roomID, secondTargetID)
|
||||
|
||||
resp, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
|
||||
Meta: &roomv1.RequestMeta{
|
||||
RequestId: "req-lucky-multi",
|
||||
CommandId: "cmd-lucky-multi",
|
||||
ActorUserId: ownerID,
|
||||
RoomId: roomID,
|
||||
SessionId: "device-session-multi",
|
||||
AppCode: appcode.Default,
|
||||
SentAtMs: time.Date(2026, 5, 20, 12, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
},
|
||||
TargetType: "user",
|
||||
TargetUserIds: []int64{firstTargetID, secondTargetID},
|
||||
GiftId: "rose",
|
||||
GiftCount: 1,
|
||||
PoolId: "super_lucky",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send multi-target lucky gift failed: %v", err)
|
||||
}
|
||||
if len(luckyGift.checks) != 1 || luckyGift.checks[0].GetPoolId() != "super_lucky" {
|
||||
t.Fatalf("multi-target lucky gift should check pool once: %+v", luckyGift.checks)
|
||||
}
|
||||
if len(luckyGift.draws) != 2 {
|
||||
t.Fatalf("multi-target lucky gift should draw once per target, got %d", len(luckyGift.draws))
|
||||
}
|
||||
firstDraw := luckyGift.draws[0].GetLuckyGift()
|
||||
secondDraw := luckyGift.draws[1].GetLuckyGift()
|
||||
if firstDraw.GetCommandId() != "cmd-lucky-multi:target:202" || firstDraw.GetTargetUserId() != firstTargetID || firstDraw.GetCoinSpent() != 100 {
|
||||
t.Fatalf("first target lucky draw mismatch: %+v", firstDraw)
|
||||
}
|
||||
if secondDraw.GetCommandId() != "cmd-lucky-multi:target:303" || secondDraw.GetTargetUserId() != secondTargetID || secondDraw.GetCoinSpent() != 200 {
|
||||
t.Fatalf("second target lucky draw mismatch: %+v", secondDraw)
|
||||
}
|
||||
if resp.GetLuckyGift().GetCommandId() != "cmd-lucky-multi" ||
|
||||
resp.GetLuckyGift().GetTargetUserId() != 0 ||
|
||||
resp.GetLuckyGift().GetSelectedTierId() != "batch" ||
|
||||
resp.GetLuckyGift().GetMultiplierPpm() != 5_000_000 ||
|
||||
resp.GetLuckyGift().GetEffectiveRewardCoins() != 500 ||
|
||||
resp.GetLuckyGift().GetWalletTransactionId() != "" ||
|
||||
resp.GetLuckyGift().GetCoinBalanceAfter() != 9100 ||
|
||||
!resp.GetLuckyGift().GetStageFeedback() ||
|
||||
!resp.GetLuckyGift().GetHighMultiplier() ||
|
||||
len(resp.GetLuckyGifts()) != 2 {
|
||||
t.Fatalf("multi-target lucky response must include aggregate and list results: %+v", resp)
|
||||
}
|
||||
if resp.GetLuckyGifts()[0].GetCommandId() != "cmd-lucky-multi:target:202" || resp.GetLuckyGifts()[0].GetMultiplierPpm() != 2_000_000 || resp.GetLuckyGifts()[1].GetCommandId() != "cmd-lucky-multi:target:303" || resp.GetLuckyGifts()[1].GetMultiplierPpm() != 3_000_000 || resp.GetLuckyGifts()[1].GetTargetUserId() != secondTargetID {
|
||||
t.Fatalf("multi-target lucky_gifts response mismatch: %+v", resp.GetLuckyGifts())
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,8 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
|
||||
cmd := command.JoinRoom{
|
||||
Base: baseFromMeta(req.GetMeta()),
|
||||
Role: req.GetRole(),
|
||||
// entry_vehicle 来自 gateway 入房瞬间的 wallet 快照,写入 command log 后可稳定重放。
|
||||
EntryVehicle: entryVehicleSnapshotFromProto(req.GetEntryVehicle()),
|
||||
}
|
||||
|
||||
if cmd.Role == "" {
|
||||
@ -76,6 +78,8 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
|
||||
UserId: cmd.ActorUserID(),
|
||||
Role: cmd.Role,
|
||||
VisibleRegionId: current.VisibleRegionID,
|
||||
// outbox 事件必须带座驾快照;否则异步补偿投递 IM 时会丢失入场座驾。
|
||||
EntryVehicle: eventEntryVehicleFromCommand(cmd.EntryVehicle),
|
||||
})
|
||||
if err != nil {
|
||||
return mutationResult{}, nil, err
|
||||
@ -93,6 +97,8 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
|
||||
ActorUserID: cmd.ActorUserID(),
|
||||
TargetUserID: cmd.ActorUserID(),
|
||||
RoomVersion: current.Version,
|
||||
// syncEvent 与 outbox 事件保持同一份快照,避免低延迟路径和补偿路径 payload 不一致。
|
||||
EntryVehicle: imEntryVehicleFromCommand(cmd.EntryVehicle),
|
||||
},
|
||||
}, []outbox.Record{joinedEvent}, nil
|
||||
})
|
||||
@ -108,6 +114,60 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
|
||||
}, nil
|
||||
}
|
||||
|
||||
func entryVehicleSnapshotFromProto(item *roomv1.RoomEntryVehicleSnapshot) command.EntryVehicleSnapshot {
|
||||
if item == nil || item.GetResourceId() <= 0 {
|
||||
return command.EntryVehicleSnapshot{}
|
||||
}
|
||||
// 只裁剪字符串,不做有效期二次判断;wallet batch RPC 已经负责过期佩戴过滤。
|
||||
return command.EntryVehicleSnapshot{
|
||||
ResourceID: item.GetResourceId(),
|
||||
ResourceCode: strings.TrimSpace(item.GetResourceCode()),
|
||||
Name: strings.TrimSpace(item.GetName()),
|
||||
AssetURL: strings.TrimSpace(item.GetAssetUrl()),
|
||||
PreviewURL: strings.TrimSpace(item.GetPreviewUrl()),
|
||||
AnimationURL: strings.TrimSpace(item.GetAnimationUrl()),
|
||||
MetadataJSON: strings.TrimSpace(item.GetMetadataJson()),
|
||||
EntitlementID: strings.TrimSpace(item.GetEntitlementId()),
|
||||
ExpiresAtMS: item.GetExpiresAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func eventEntryVehicleFromCommand(item command.EntryVehicleSnapshot) *roomeventsv1.RoomEntryVehicleSnapshot {
|
||||
if item.ResourceID <= 0 {
|
||||
return nil
|
||||
}
|
||||
// 事件 proto 是补偿投递、统计消费的稳定事实,字段不依赖后续资源表变更。
|
||||
return &roomeventsv1.RoomEntryVehicleSnapshot{
|
||||
ResourceId: item.ResourceID,
|
||||
ResourceCode: item.ResourceCode,
|
||||
Name: item.Name,
|
||||
AssetUrl: item.AssetURL,
|
||||
PreviewUrl: item.PreviewURL,
|
||||
AnimationUrl: item.AnimationURL,
|
||||
MetadataJson: item.MetadataJSON,
|
||||
EntitlementId: item.EntitlementID,
|
||||
ExpiresAtMs: item.ExpiresAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
func imEntryVehicleFromCommand(item command.EntryVehicleSnapshot) *tencentim.RoomEntryVehicleSnapshot {
|
||||
if item.ResourceID <= 0 {
|
||||
return nil
|
||||
}
|
||||
// Tencent IM payload 使用客户端展示字段,保持和 /appearance 的资源字段命名一致。
|
||||
return &tencentim.RoomEntryVehicleSnapshot{
|
||||
ResourceID: item.ResourceID,
|
||||
ResourceCode: item.ResourceCode,
|
||||
Name: item.Name,
|
||||
AssetURL: item.AssetURL,
|
||||
PreviewURL: item.PreviewURL,
|
||||
AnimationURL: item.AnimationURL,
|
||||
MetadataJSON: item.MetadataJSON,
|
||||
EntitlementID: item.EntitlementID,
|
||||
ExpiresAtMS: item.ExpiresAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
// projectRoomVisitBestEffort 记录 Mine/Visited tab 的房间访问索引。
|
||||
// 这是列表读模型,不参与 Room Cell 提交;失败只记录日志,不能影响已经成功的 JoinRoom。
|
||||
func (s *Service) projectRoomVisitBestEffort(ctx context.Context, userID int64, roomID string) {
|
||||
|
||||
@ -29,13 +29,16 @@ func (c *fixedRoomTreasureClock) Now() time.Time {
|
||||
}
|
||||
|
||||
type treasureTestWallet struct {
|
||||
debits []*walletv1.DebitGiftResponse
|
||||
grants []*walletv1.GrantResourceGroupRequest
|
||||
lastDebit *walletv1.DebitGiftRequest
|
||||
debits []*walletv1.DebitGiftResponse
|
||||
grants []*walletv1.GrantResourceGroupRequest
|
||||
lastDebit *walletv1.DebitGiftRequest
|
||||
debitRequests []*walletv1.DebitGiftRequest
|
||||
lastBatch *walletv1.BatchDebitGiftRequest
|
||||
}
|
||||
|
||||
func (w *treasureTestWallet) DebitGift(_ context.Context, req *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error) {
|
||||
w.lastDebit = req
|
||||
w.debitRequests = append(w.debitRequests, req)
|
||||
if len(w.debits) == 0 {
|
||||
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-default", GiftPointAdded: 1, HeatValue: 1}, nil
|
||||
}
|
||||
@ -44,6 +47,114 @@ func (w *treasureTestWallet) DebitGift(_ context.Context, req *walletv1.DebitGif
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func (w *treasureTestWallet) BatchDebitGift(_ context.Context, req *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
|
||||
w.lastBatch = req
|
||||
receipts := make([]*walletv1.BatchDebitGiftReceipt, 0, len(req.GetTargets()))
|
||||
aggregate := &walletv1.DebitGiftResponse{}
|
||||
for _, target := range req.GetTargets() {
|
||||
var next *walletv1.DebitGiftResponse
|
||||
if len(w.debits) == 0 {
|
||||
next = &walletv1.DebitGiftResponse{BillingReceiptId: fmt.Sprintf("receipt-%d", target.GetTargetUserId()), GiftPointAdded: 1, HeatValue: 1, CoinSpent: 1, ChargeAmount: 1}
|
||||
} else {
|
||||
next = w.debits[0]
|
||||
w.debits = w.debits[1:]
|
||||
}
|
||||
aggregate.BillingReceiptId = joinReceiptIDs(aggregate.GetBillingReceiptId(), next.GetBillingReceiptId())
|
||||
aggregate.CoinSpent += next.GetCoinSpent()
|
||||
aggregate.ChargeAmount += next.GetChargeAmount()
|
||||
aggregate.GiftPointAdded += next.GetGiftPointAdded()
|
||||
aggregate.HeatValue += next.GetHeatValue()
|
||||
aggregate.HostPeriodDiamondAdded += next.GetHostPeriodDiamondAdded()
|
||||
if aggregate.PriceVersion == "" {
|
||||
aggregate.PriceVersion = next.GetPriceVersion()
|
||||
}
|
||||
if aggregate.GiftTypeCode == "" {
|
||||
aggregate.GiftTypeCode = next.GetGiftTypeCode()
|
||||
}
|
||||
aggregate.BalanceAfter = next.GetBalanceAfter()
|
||||
receipts = append(receipts, &walletv1.BatchDebitGiftReceipt{
|
||||
TargetUserId: target.GetTargetUserId(),
|
||||
CommandId: target.GetCommandId(),
|
||||
Billing: next,
|
||||
})
|
||||
}
|
||||
return &walletv1.BatchDebitGiftResponse{Aggregate: aggregate, Receipts: receipts}, nil
|
||||
}
|
||||
|
||||
func joinReceiptIDs(left string, right string) string {
|
||||
if left == "" {
|
||||
return right
|
||||
}
|
||||
if right == "" {
|
||||
return left
|
||||
}
|
||||
return left + "," + right
|
||||
}
|
||||
|
||||
func TestSendGiftBatchDebitsMultipleTargets(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
now := &fixedRoomTreasureClock{now: time.Date(2026, 5, 20, 12, 0, 0, 0, time.UTC)}
|
||||
wallet := &treasureTestWallet{debits: []*walletv1.DebitGiftResponse{
|
||||
{BillingReceiptId: "receipt-202", CoinSpent: 10, ChargeAmount: 10, GiftPointAdded: 5, HeatValue: 11, GiftTypeCode: "normal", PriceVersion: "v1", BalanceAfter: 90},
|
||||
{BillingReceiptId: "receipt-303", CoinSpent: 20, ChargeAmount: 20, GiftPointAdded: 7, HeatValue: 19, GiftTypeCode: "normal", PriceVersion: "v1", BalanceAfter: 70},
|
||||
}}
|
||||
svc := newTreasureTestService(t, repository, wallet, now)
|
||||
|
||||
roomID := "room-gift-multi-target"
|
||||
ownerID := int64(101)
|
||||
firstTargetID := int64(202)
|
||||
secondTargetID := int64(303)
|
||||
createTreasureRoom(t, ctx, svc, roomID, ownerID, 9001)
|
||||
joinTreasureRoom(t, ctx, svc, roomID, firstTargetID)
|
||||
joinTreasureRoom(t, ctx, svc, roomID, secondTargetID)
|
||||
|
||||
resp, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
|
||||
Meta: &roomv1.RequestMeta{
|
||||
RequestId: "req-gift-multi",
|
||||
CommandId: "cmd-gift-multi",
|
||||
ActorUserId: ownerID,
|
||||
RoomId: roomID,
|
||||
AppCode: appcode.Default,
|
||||
SentAtMs: now.Now().UnixMilli(),
|
||||
},
|
||||
TargetType: "user",
|
||||
TargetUserIds: []int64{firstTargetID, secondTargetID},
|
||||
GiftId: "rose",
|
||||
GiftCount: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send multi-target gift failed: %v", err)
|
||||
}
|
||||
if resp.GetRoomHeat() != 30 || resp.GetBillingReceiptId() != "receipt-202,receipt-303" {
|
||||
t.Fatalf("multi-target gift response must aggregate wallet results: %+v", resp)
|
||||
}
|
||||
if len(wallet.debitRequests) != 0 {
|
||||
t.Fatalf("multi-target gift must use wallet BatchDebitGift, single debits=%+v", wallet.debitRequests)
|
||||
}
|
||||
if wallet.lastBatch == nil || len(wallet.lastBatch.GetTargets()) != 2 {
|
||||
t.Fatalf("wallet batch request missing targets: %+v", wallet.lastBatch)
|
||||
}
|
||||
if wallet.lastBatch.GetTargets()[0].GetCommandId() != "cmd-gift-multi:target:202" || wallet.lastBatch.GetTargets()[1].GetCommandId() != "cmd-gift-multi:target:303" {
|
||||
t.Fatalf("wallet batch target command ids mismatch: %+v", wallet.lastBatch.GetTargets())
|
||||
}
|
||||
|
||||
events := roomGiftSentEvents(t, ctx, repository)
|
||||
if len(events) != 2 {
|
||||
t.Fatalf("multi-target gift must emit one GiftSent per target, got %d", len(events))
|
||||
}
|
||||
got := map[int64]*roomeventsv1.RoomGiftSent{}
|
||||
for _, event := range events {
|
||||
got[event.GetTargetUserId()] = event
|
||||
}
|
||||
if got[firstTargetID].GetBillingReceiptId() != "receipt-202" || got[firstTargetID].GetGiftValue() != 11 || got[firstTargetID].GetCommandId() != "cmd-gift-multi:target:202" {
|
||||
t.Fatalf("first target GiftSent mismatch: %+v", got[firstTargetID])
|
||||
}
|
||||
if got[secondTargetID].GetBillingReceiptId() != "receipt-303" || got[secondTargetID].GetGiftValue() != 19 || got[secondTargetID].GetCommandId() != "cmd-gift-multi:target:303" {
|
||||
t.Fatalf("second target GiftSent mismatch: %+v", got[secondTargetID])
|
||||
}
|
||||
}
|
||||
|
||||
func (w *treasureTestWallet) GrantResourceGroup(_ context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
|
||||
w.grants = append(w.grants, req)
|
||||
return &walletv1.ResourceGrantResponse{
|
||||
@ -487,6 +598,27 @@ func treasureProgressEvents(t *testing.T, ctx context.Context, repository *mysql
|
||||
return events
|
||||
}
|
||||
|
||||
func roomGiftSentEvents(t *testing.T, ctx context.Context, repository *mysqltest.Repository) []*roomeventsv1.RoomGiftSent {
|
||||
t.Helper()
|
||||
|
||||
records, err := repository.ListPendingOutbox(ctx, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("list pending outbox failed: %v", err)
|
||||
}
|
||||
events := make([]*roomeventsv1.RoomGiftSent, 0)
|
||||
for _, record := range records {
|
||||
if record.EventType != "RoomGiftSent" {
|
||||
continue
|
||||
}
|
||||
var event roomeventsv1.RoomGiftSent
|
||||
if err := proto.Unmarshal(record.Envelope.GetBody(), &event); err != nil {
|
||||
t.Fatalf("decode gift sent event failed: %v", err)
|
||||
}
|
||||
events = append(events, &event)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func countTreasureCountdownEvents(t *testing.T, ctx context.Context, repository *mysqltest.Repository) int {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@ -113,6 +113,8 @@ type mutationResult struct {
|
||||
giftRank []*roomv1.RankItem
|
||||
// luckyGift 是扣费成功后同步执行的幸运礼物抽奖结果;普通礼物保持 nil。
|
||||
luckyGift *roomv1.LuckyGiftDrawResult
|
||||
// luckyGifts 是多目标幸运礼物的逐接收方抽奖结果;单目标时只包含 luckyGift。
|
||||
luckyGifts []*roomv1.LuckyGiftDrawResult
|
||||
// commandPayload 允许少数命令把外部依赖的结算快照写入 command log,用于恢复。
|
||||
commandPayload []byte
|
||||
// syncEvent 标记该命令存在腾讯云 IM 房间消息形态;实际投递只由 outbox worker 异步执行。
|
||||
|
||||
@ -30,6 +30,11 @@ func (fakeWallet) DebitGift(context.Context, *walletv1.DebitGiftRequest) (*walle
|
||||
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-test"}, nil
|
||||
}
|
||||
|
||||
// BatchDebitGift 返回固定结果,避免 transport 测试依赖 wallet-service 批量扣费实现。
|
||||
func (fakeWallet) BatchDebitGift(context.Context, *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
|
||||
return &walletv1.BatchDebitGiftResponse{Aggregate: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-test"}}, nil
|
||||
}
|
||||
|
||||
// GrantResourceGroup 返回固定发放结果,避免 transport 测试依赖 wallet-service 资源组实现。
|
||||
func (fakeWallet) GrantResourceGroup(context.Context, *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
|
||||
return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{GrantId: "grant-test"}}, nil
|
||||
|
||||
@ -168,6 +168,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
}
|
||||
var activityConn *grpc.ClientConn
|
||||
var registrationRewardIssuer authservice.RegistrationRewardIssuer
|
||||
var registrationLevelBadgeIssuer authservice.RegistrationLevelBadgeIssuer
|
||||
if cfg.ActivityService.Enabled {
|
||||
activityConn, err = grpcclient.Dial(cfg.ActivityService.Addr, grpcclient.Config{
|
||||
DefaultTimeout: time.Duration(cfg.ActivityService.RequestTimeoutMs) * time.Millisecond,
|
||||
@ -183,7 +184,9 @@ func New(cfg config.Config) (*App, error) {
|
||||
_ = mysqlRepo.Close()
|
||||
return nil, err
|
||||
}
|
||||
registrationRewardIssuer = activityclient.NewGRPC(activityConn)
|
||||
activityClient := activityclient.NewGRPC(activityConn)
|
||||
registrationRewardIssuer = activityClient
|
||||
registrationLevelBadgeIssuer = activityClient
|
||||
}
|
||||
|
||||
// auth service 负责登录、session 和 token;用户主数据和短号事务仍通过各自领域存储完成。
|
||||
@ -214,6 +217,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
authservice.WithIPDecisionCache(ipDecisionCache),
|
||||
authservice.WithIPGeoProviders(authservice.BuildIPGeoProviders(cfg.LoginRisk.Providers)),
|
||||
authservice.WithRegistrationRewardIssuer(registrationRewardIssuer),
|
||||
authservice.WithRegistrationLevelBadgeIssuer(registrationLevelBadgeIssuer),
|
||||
authservice.WithIMAccountImporter(imAccountImporter),
|
||||
)
|
||||
// user service 负责用户主状态和 display_user_id/靓号用例,不进入房间高频流程。
|
||||
|
||||
@ -11,19 +11,41 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
client activityv1.RegistrationRewardServiceClient
|
||||
registrationRewardClient activityv1.RegistrationRewardServiceClient
|
||||
growthLevelClient activityv1.GrowthLevelServiceClient
|
||||
}
|
||||
|
||||
func NewGRPC(conn grpc.ClientConnInterface) *Client {
|
||||
return &Client{client: activityv1.NewRegistrationRewardServiceClient(conn)}
|
||||
return &Client{
|
||||
registrationRewardClient: activityv1.NewRegistrationRewardServiceClient(conn),
|
||||
growthLevelClient: activityv1.NewGrowthLevelServiceClient(conn),
|
||||
}
|
||||
}
|
||||
|
||||
// IssueRegistrationReward 只发起注册奖励幂等命令;可领取判断和实际发放都在 activity-service。
|
||||
func (c *Client) IssueRegistrationReward(ctx context.Context, command authservice.RegistrationRewardCommand) error {
|
||||
if c == nil || c.client == nil {
|
||||
if c == nil || c.registrationRewardClient == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := c.client.IssueRegistrationReward(ctx, &activityv1.IssueRegistrationRewardRequest{
|
||||
_, err := c.registrationRewardClient.IssueRegistrationReward(ctx, &activityv1.IssueRegistrationRewardRequest{
|
||||
Meta: &activityv1.RequestMeta{
|
||||
RequestId: command.RequestID,
|
||||
Caller: "user-service",
|
||||
AppCode: command.AppCode,
|
||||
SentAtMs: time.Now().UnixMilli(),
|
||||
},
|
||||
UserId: command.UserID,
|
||||
CommandId: command.CommandID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// IssueRegistrationLevelBadges 发起注册等级徽章发放;资源选择和钱包发放都在 activity-service。
|
||||
func (c *Client) IssueRegistrationLevelBadges(ctx context.Context, command authservice.RegistrationLevelBadgeCommand) error {
|
||||
if c == nil || c.growthLevelClient == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := c.growthLevelClient.IssueRegistrationLevelBadges(ctx, &activityv1.IssueRegistrationLevelBadgesRequest{
|
||||
Meta: &activityv1.RequestMeta{
|
||||
RequestId: command.RequestID,
|
||||
Caller: "user-service",
|
||||
|
||||
@ -110,6 +110,19 @@ type RegistrationRewardIssuer interface {
|
||||
IssueRegistrationReward(ctx context.Context, command RegistrationRewardCommand) error
|
||||
}
|
||||
|
||||
// RegistrationLevelBadgeCommand 是三方首次注册成功后发往 activity-service 的等级徽章发放命令。
|
||||
type RegistrationLevelBadgeCommand struct {
|
||||
AppCode string
|
||||
RequestID string
|
||||
UserID int64
|
||||
CommandID string
|
||||
}
|
||||
|
||||
// RegistrationLevelBadgeIssuer 隔离注册等级徽章发放 RPC;失败不能回滚用户注册主事务。
|
||||
type RegistrationLevelBadgeIssuer interface {
|
||||
IssueRegistrationLevelBadges(ctx context.Context, command RegistrationLevelBadgeCommand) error
|
||||
}
|
||||
|
||||
// IMAccountImporter 把 user-service 用户导入腾讯 IM 账号体系。
|
||||
type IMAccountImporter interface {
|
||||
ImportAccount(ctx context.Context, userID int64, nickname string, faceURL string) error
|
||||
@ -199,6 +212,8 @@ type Service struct {
|
||||
ipGeoProviders []IPGeoProvider
|
||||
// registrationRewardIssuer 在注册事务提交后异步式触发奖励;失败只记录日志,不回滚用户注册。
|
||||
registrationRewardIssuer RegistrationRewardIssuer
|
||||
// registrationLevelBadgeIssuer 在注册事务提交后补发三条等级轨道的 1 级徽章。
|
||||
registrationLevelBadgeIssuer RegistrationLevelBadgeIssuer
|
||||
// imAccountImporter 在用户创建成功后补齐腾讯 IM 账号;UserSig 入口仍会兜底补偿历史账号。
|
||||
imAccountImporter IMAccountImporter
|
||||
}
|
||||
@ -282,6 +297,15 @@ func WithRegistrationRewardIssuer(issuer RegistrationRewardIssuer) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithRegistrationLevelBadgeIssuer 注入注册等级徽章发放 client;nil 表示本环境不启用该集成。
|
||||
func WithRegistrationLevelBadgeIssuer(issuer RegistrationLevelBadgeIssuer) Option {
|
||||
return func(s *Service) {
|
||||
if issuer != nil {
|
||||
s.registrationLevelBadgeIssuer = issuer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithIMAccountImporter 注入腾讯 IM 账号导入边界;nil 表示当前环境不触发真实 IM 副作用。
|
||||
func WithIMAccountImporter(importer IMAccountImporter) Option {
|
||||
return func(s *Service) {
|
||||
|
||||
@ -177,6 +177,7 @@ func (s *Service) createThirdPartyUser(ctx context.Context, provider string, pro
|
||||
s.importIMAccountAfterCommit(ctx, user, meta)
|
||||
s.enqueueLoginIPRiskJob(ctx, meta, token, normalizeThirdPartyChannel(provider), registration.Platform, registration.Language, registration.Timezone)
|
||||
s.issueRegistrationRewardAfterCommit(ctx, registration.AppCode, user.UserID, meta)
|
||||
s.issueRegistrationLevelBadgesAfterCommit(ctx, registration.AppCode, user.UserID, meta)
|
||||
return token, true, nil
|
||||
}
|
||||
if xerr.IsCode(err, xerr.DisplayUserIDExists) {
|
||||
@ -225,6 +226,27 @@ func (s *Service) issueRegistrationRewardAfterCommit(ctx context.Context, appCod
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) issueRegistrationLevelBadgesAfterCommit(ctx context.Context, appCode string, userID int64, meta Meta) {
|
||||
if s.registrationLevelBadgeIssuer == nil {
|
||||
return
|
||||
}
|
||||
commandID := fmt.Sprintf("registration_level_badges:%s:%d", appcode.Normalize(appCode), userID)
|
||||
if err := s.registrationLevelBadgeIssuer.IssueRegistrationLevelBadges(ctx, RegistrationLevelBadgeCommand{
|
||||
AppCode: appcode.Normalize(appCode),
|
||||
RequestID: meta.RequestID,
|
||||
UserID: userID,
|
||||
CommandID: commandID,
|
||||
}); err != nil {
|
||||
// 等级徽章是注册后置权益;用户注册事务已经提交,只能依赖同 command_id 重试或补发修复。
|
||||
logx.Warn(logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", appcode.Normalize(appCode))), "registration_level_badges_issue_failed",
|
||||
slog.String("component", "user_auth"),
|
||||
slog.Int64("user_id", userID),
|
||||
slog.String("command_id", commandID),
|
||||
slog.String("error", err.Error()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeThirdPartyRegistration(registration authdomain.ThirdPartyRegistration, meta Meta) authdomain.ThirdPartyRegistration {
|
||||
// 注册资料来自公网入口,进入领域前统一裁剪空白,避免数据库里出现不可见差异。
|
||||
registration.Username = strings.TrimSpace(registration.Username)
|
||||
|
||||
@ -34,6 +34,7 @@ type Repository interface {
|
||||
GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error)
|
||||
ListActiveCoinSellersInMyRegion(ctx context.Context, userID int64) ([]hostdomain.CoinSellerListItem, error)
|
||||
GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error)
|
||||
GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, error)
|
||||
HasActiveAgencyOwner(ctx context.Context, userID int64) (int64, bool, error)
|
||||
ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error)
|
||||
ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error)
|
||||
@ -184,6 +185,17 @@ func (s *Service) KickAgencyHost(ctx context.Context, command KickAgencyHostInpu
|
||||
})
|
||||
}
|
||||
|
||||
// GetAgency 读取 Agency 详情;调用方负责根据业务入口校验 owner 或管理员身份。
|
||||
func (s *Service) GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, error) {
|
||||
if s.repository == nil {
|
||||
return hostdomain.Agency{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||||
}
|
||||
if agencyID <= 0 {
|
||||
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "agency_id is required")
|
||||
}
|
||||
return s.repository.GetAgency(ctx, agencyID)
|
||||
}
|
||||
|
||||
// InviteAgency 创建邀请用户成为 Agency 拥有者的待处理邀请。
|
||||
func (s *Service) InviteAgency(ctx context.Context, command InviteAgencyInput) (hostdomain.RoleInvitation, error) {
|
||||
if err := s.requireWriteDependencies(); err != nil {
|
||||
|
||||
@ -110,6 +110,11 @@ func (r *Repository) GetCoinSellerProfile(ctx context.Context, userID int64) (ho
|
||||
return queryCoinSellerProfile(ctx, r.db, "WHERE user_id = ?", userID)
|
||||
}
|
||||
|
||||
// GetAgency 读取 Agency 当前事实。
|
||||
func (r *Repository) GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, error) {
|
||||
return queryAgency(ctx, r.db, "WHERE agency_id = ?", agencyID)
|
||||
}
|
||||
|
||||
// ListActiveCoinSellersInMyRegion 返回当前用户所在区域的启用币商列表。
|
||||
// 该查询从 users.region_id 出发过滤,避免把区域条件交给 gateway 或客户端传入。
|
||||
func (r *Repository) ListActiveCoinSellersInMyRegion(ctx context.Context, userID int64) ([]hostdomain.CoinSellerListItem, error) {
|
||||
|
||||
@ -238,6 +238,20 @@ func (s *Server) GetUserRoleSummary(ctx context.Context, req *userv1.GetUserRole
|
||||
return &userv1.GetUserRoleSummaryResponse{Summary: toProtoUserRoleSummary(summary)}, nil
|
||||
}
|
||||
|
||||
// GetAgency 返回 Agency 当前事实。
|
||||
func (s *Server) GetAgency(ctx context.Context, req *userv1.GetAgencyRequest) (*userv1.GetAgencyResponse, error) {
|
||||
if s.hostSvc == nil {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
|
||||
}
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
agency, err := s.hostSvc.GetAgency(ctx, req.GetAgencyId())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
|
||||
return &userv1.GetAgencyResponse{Agency: toProtoAgency(agency)}, nil
|
||||
}
|
||||
|
||||
// CheckBusinessCapability 返回 H5/App 业务入口可用性,具体身份规则由 host service 统一判断。
|
||||
func (s *Server) CheckBusinessCapability(ctx context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error) {
|
||||
if s.hostSvc == nil {
|
||||
|
||||
@ -38,6 +38,10 @@ rocketmq:
|
||||
enabled: true
|
||||
topic: "hyapp_wallet_outbox"
|
||||
producer_group: "hyapp-wallet-outbox-producer"
|
||||
realtime_outbox:
|
||||
enabled: true
|
||||
topic: "hyapp_wallet_realtime_outbox"
|
||||
producer_group: "hyapp-wallet-realtime-outbox-producer"
|
||||
google_play:
|
||||
enabled: false
|
||||
package_name: ""
|
||||
@ -50,8 +54,22 @@ google_play:
|
||||
outbox_worker:
|
||||
enabled: true
|
||||
poll_interval: "1s"
|
||||
batch_size: 100
|
||||
batch_size: 200
|
||||
concurrency: 4
|
||||
publish_timeout: "3s"
|
||||
max_retry_count: 10
|
||||
initial_backoff: "5s"
|
||||
max_backoff: "5m"
|
||||
realtime_outbox_worker:
|
||||
enabled: true
|
||||
poll_interval: "500ms"
|
||||
batch_size: 50
|
||||
concurrency: 2
|
||||
publish_timeout: "3s"
|
||||
max_retry_count: 10
|
||||
initial_backoff: "2s"
|
||||
max_backoff: "1m"
|
||||
event_types:
|
||||
- "WalletRedPacketCreated"
|
||||
- "WalletRedPacketClaimed"
|
||||
- "WalletRedPacketRefunded"
|
||||
|
||||
@ -38,6 +38,10 @@ rocketmq:
|
||||
enabled: true
|
||||
topic: "hyapp_wallet_outbox"
|
||||
producer_group: "hyapp-wallet-outbox-producer"
|
||||
realtime_outbox:
|
||||
enabled: true
|
||||
topic: "hyapp_wallet_realtime_outbox"
|
||||
producer_group: "hyapp-wallet-realtime-outbox-producer"
|
||||
google_play:
|
||||
enabled: false
|
||||
package_name: "com.example.hyapp"
|
||||
@ -50,8 +54,22 @@ google_play:
|
||||
outbox_worker:
|
||||
enabled: true
|
||||
poll_interval: "1s"
|
||||
batch_size: 100
|
||||
batch_size: 200
|
||||
concurrency: 8
|
||||
publish_timeout: "3s"
|
||||
max_retry_count: 10
|
||||
initial_backoff: "5s"
|
||||
max_backoff: "5m"
|
||||
realtime_outbox_worker:
|
||||
enabled: true
|
||||
poll_interval: "500ms"
|
||||
batch_size: 50
|
||||
concurrency: 4
|
||||
publish_timeout: "3s"
|
||||
max_retry_count: 10
|
||||
initial_backoff: "2s"
|
||||
max_backoff: "1m"
|
||||
event_types:
|
||||
- "WalletRedPacketCreated"
|
||||
- "WalletRedPacketClaimed"
|
||||
- "WalletRedPacketRefunded"
|
||||
|
||||
@ -38,6 +38,10 @@ rocketmq:
|
||||
enabled: false
|
||||
topic: "hyapp_wallet_outbox"
|
||||
producer_group: "hyapp-wallet-outbox-producer"
|
||||
realtime_outbox:
|
||||
enabled: false
|
||||
topic: "hyapp_wallet_realtime_outbox"
|
||||
producer_group: "hyapp-wallet-realtime-outbox-producer"
|
||||
google_play:
|
||||
enabled: false
|
||||
package_name: ""
|
||||
@ -51,7 +55,21 @@ outbox_worker:
|
||||
enabled: false
|
||||
poll_interval: "1s"
|
||||
batch_size: 100
|
||||
concurrency: 1
|
||||
publish_timeout: "3s"
|
||||
max_retry_count: 10
|
||||
initial_backoff: "5s"
|
||||
max_backoff: "5m"
|
||||
realtime_outbox_worker:
|
||||
enabled: false
|
||||
poll_interval: "500ms"
|
||||
batch_size: 50
|
||||
concurrency: 2
|
||||
publish_timeout: "3s"
|
||||
max_retry_count: 10
|
||||
initial_backoff: "2s"
|
||||
max_backoff: "1m"
|
||||
event_types:
|
||||
- "WalletRedPacketCreated"
|
||||
- "WalletRedPacketClaimed"
|
||||
- "WalletRedPacketRefunded"
|
||||
|
||||
@ -75,6 +75,8 @@ CREATE TABLE IF NOT EXISTS wallet_outbox (
|
||||
PRIMARY KEY (app_code, event_id),
|
||||
KEY idx_wallet_outbox_pending (app_code, status, next_retry_at_ms, created_at_ms, event_id),
|
||||
KEY idx_wallet_outbox_claim (app_code, status, lock_until_ms, created_at_ms, event_id),
|
||||
KEY idx_wallet_outbox_event_pending (app_code, status, event_type, next_retry_at_ms, created_at_ms, event_id),
|
||||
KEY idx_wallet_outbox_event_claim (app_code, status, event_type, lock_until_ms, created_at_ms, event_id),
|
||||
KEY idx_wallet_outbox_retention (app_code, status, updated_at_ms, event_id),
|
||||
KEY idx_wallet_outbox_event_created (app_code, event_type, created_at_ms, event_id),
|
||||
KEY idx_wallet_outbox_asset_event_created (app_code, asset_type, event_type, created_at_ms, event_id),
|
||||
@ -346,6 +348,25 @@ PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 实时红包/房间 UI 事件按 event_type 独立 claim;缺这个索引会在通用账务 backlog 上扫描,仍然可能延迟弹窗。
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND INDEX_NAME = 'idx_wallet_outbox_event_pending') = 0,
|
||||
'ALTER TABLE wallet_outbox ADD INDEX idx_wallet_outbox_event_pending (app_code, status, event_type, next_retry_at_ms, created_at_ms, event_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND INDEX_NAME = 'idx_wallet_outbox_event_claim') = 0,
|
||||
'ALTER TABLE wallet_outbox ADD INDEX idx_wallet_outbox_event_claim (app_code, status, event_type, lock_until_ms, created_at_ms, event_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND INDEX_NAME = 'idx_wallet_outbox_retention') = 0,
|
||||
'ALTER TABLE wallet_outbox ADD INDEX idx_wallet_outbox_retention (app_code, status, updated_at_ms, event_id)',
|
||||
|
||||
@ -31,23 +31,26 @@ import (
|
||||
|
||||
// App 装配 wallet-service gRPC 入口。
|
||||
type App struct {
|
||||
server *grpc.Server
|
||||
listener net.Listener
|
||||
health *grpchealth.ServingChecker
|
||||
healthHTTP *healthhttp.Server
|
||||
mysqlRepo *mysqlstorage.Repository
|
||||
walletSvc *walletservice.Service
|
||||
activityConn *grpc.ClientConn
|
||||
outboxProducer *rocketmqx.Producer
|
||||
projectionConsumer *rocketmqx.Consumer
|
||||
outboxWorkerCfg config.OutboxWorkerConfig
|
||||
projectionWorkerCfg config.ProjectionWorkerConfig
|
||||
walletOutboxTopic string
|
||||
nodeID string
|
||||
redPacketExpiryWorkerCfg config.RedPacketExpiryWorkerConfig
|
||||
stopWorker context.CancelFunc
|
||||
workers sync.WaitGroup
|
||||
closeOnce sync.Once
|
||||
server *grpc.Server
|
||||
listener net.Listener
|
||||
health *grpchealth.ServingChecker
|
||||
healthHTTP *healthhttp.Server
|
||||
mysqlRepo *mysqlstorage.Repository
|
||||
walletSvc *walletservice.Service
|
||||
activityConn *grpc.ClientConn
|
||||
outboxProducer *rocketmqx.Producer
|
||||
realtimeOutboxProducer *rocketmqx.Producer
|
||||
projectionConsumer *rocketmqx.Consumer
|
||||
outboxWorkerCfg config.OutboxWorkerConfig
|
||||
realtimeOutboxWorkerCfg config.OutboxWorkerConfig
|
||||
projectionWorkerCfg config.ProjectionWorkerConfig
|
||||
walletOutboxTopic string
|
||||
realtimeWalletOutboxTopic string
|
||||
nodeID string
|
||||
redPacketExpiryWorkerCfg config.RedPacketExpiryWorkerConfig
|
||||
stopWorker context.CancelFunc
|
||||
workers sync.WaitGroup
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// New 初始化 wallet-service。
|
||||
@ -74,7 +77,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
}
|
||||
var outboxProducer *rocketmqx.Producer
|
||||
if cfg.RocketMQ.WalletOutbox.Enabled {
|
||||
outboxProducer, err = rocketmqx.NewProducer(rocketMQProducerConfig(cfg.RocketMQ))
|
||||
outboxProducer, err = rocketmqx.NewProducer(rocketMQProducerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.ProducerGroup))
|
||||
if err != nil {
|
||||
_ = activityConn.Close()
|
||||
_ = listener.Close()
|
||||
@ -82,15 +85,24 @@ func New(cfg config.Config) (*App, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var realtimeOutboxProducer *rocketmqx.Producer
|
||||
if cfg.RocketMQ.RealtimeOutbox.Enabled {
|
||||
realtimeOutboxProducer, err = rocketmqx.NewProducer(rocketMQProducerConfig(cfg.RocketMQ, cfg.RocketMQ.RealtimeOutbox.ProducerGroup))
|
||||
if err != nil {
|
||||
shutdownProducers(outboxProducer)
|
||||
_ = activityConn.Close()
|
||||
_ = listener.Close()
|
||||
_ = repository.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("wallet-service")))
|
||||
svc := walletservice.New(repository, client.NewActivityAchievementClient(activityConn))
|
||||
if cfg.GooglePlay.Enabled {
|
||||
googleClient, err := googleplay.New(cfg.GooglePlay)
|
||||
if err != nil {
|
||||
if outboxProducer != nil {
|
||||
_ = outboxProducer.Shutdown()
|
||||
}
|
||||
shutdownProducers(outboxProducer, realtimeOutboxProducer)
|
||||
_ = activityConn.Close()
|
||||
_ = listener.Close()
|
||||
_ = repository.Close()
|
||||
@ -107,9 +119,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
healthgrpc.RegisterHealthServer(server, grpchealth.NewServer(health))
|
||||
healthHTTP, err := healthhttp.New(cfg.HealthHTTPAddr, cfg.NodeID, health)
|
||||
if err != nil {
|
||||
if outboxProducer != nil {
|
||||
_ = outboxProducer.Shutdown()
|
||||
}
|
||||
shutdownProducers(outboxProducer, realtimeOutboxProducer)
|
||||
_ = activityConn.Close()
|
||||
_ = listener.Close()
|
||||
_ = repository.Close()
|
||||
@ -117,9 +127,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
}
|
||||
projectionConsumer, err := newWalletProjectionConsumer(cfg, svc)
|
||||
if err != nil {
|
||||
if outboxProducer != nil {
|
||||
_ = outboxProducer.Shutdown()
|
||||
}
|
||||
shutdownProducers(outboxProducer, realtimeOutboxProducer)
|
||||
_ = activityConn.Close()
|
||||
_ = listener.Close()
|
||||
_ = repository.Close()
|
||||
@ -127,20 +135,23 @@ func New(cfg config.Config) (*App, error) {
|
||||
}
|
||||
|
||||
return &App{
|
||||
server: server,
|
||||
listener: listener,
|
||||
health: health,
|
||||
healthHTTP: healthHTTP,
|
||||
mysqlRepo: repository,
|
||||
walletSvc: svc,
|
||||
activityConn: activityConn,
|
||||
outboxProducer: outboxProducer,
|
||||
projectionConsumer: projectionConsumer,
|
||||
outboxWorkerCfg: cfg.OutboxWorker,
|
||||
projectionWorkerCfg: cfg.ProjectionWorker,
|
||||
walletOutboxTopic: cfg.RocketMQ.WalletOutbox.Topic,
|
||||
nodeID: cfg.NodeID,
|
||||
redPacketExpiryWorkerCfg: cfg.RedPacketExpiryWorker,
|
||||
server: server,
|
||||
listener: listener,
|
||||
health: health,
|
||||
healthHTTP: healthHTTP,
|
||||
mysqlRepo: repository,
|
||||
walletSvc: svc,
|
||||
activityConn: activityConn,
|
||||
outboxProducer: outboxProducer,
|
||||
realtimeOutboxProducer: realtimeOutboxProducer,
|
||||
projectionConsumer: projectionConsumer,
|
||||
outboxWorkerCfg: cfg.OutboxWorker,
|
||||
realtimeOutboxWorkerCfg: cfg.RealtimeOutboxWorker,
|
||||
projectionWorkerCfg: cfg.ProjectionWorker,
|
||||
walletOutboxTopic: cfg.RocketMQ.WalletOutbox.Topic,
|
||||
realtimeWalletOutboxTopic: cfg.RocketMQ.RealtimeOutbox.Topic,
|
||||
nodeID: cfg.NodeID,
|
||||
redPacketExpiryWorkerCfg: cfg.RedPacketExpiryWorker,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -189,12 +200,16 @@ func (a *App) runBackgroundWorkers() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
a.stopWorker = cancel
|
||||
if a.outboxWorkerCfg.Enabled && a.outboxProducer != nil {
|
||||
outboxWorkerID := "wallet-outbox-" + a.nodeID
|
||||
a.workers.Add(1)
|
||||
go func() {
|
||||
defer a.workers.Done()
|
||||
a.runWalletOutboxWorker(ctx, outboxWorkerID)
|
||||
}()
|
||||
excludedRealtimeTypes := []string(nil)
|
||||
if a.realtimeOutboxWorkerCfg.Enabled && a.realtimeOutboxProducer != nil {
|
||||
// 普通账务 worker 在实时通道可用时主动跳过红包 UI 事件,避免两个 worker 抢同一行,
|
||||
// 也避免红包事实继续排在 WalletBalanceChanged 和礼物流水之后。
|
||||
excludedRealtimeTypes = a.realtimeOutboxWorkerCfg.EventTypes
|
||||
}
|
||||
a.startWalletOutboxWorkers(ctx, "wallet-outbox", a.outboxWorkerCfg, a.outboxProducer, a.walletOutboxTopic, nil, excludedRealtimeTypes)
|
||||
}
|
||||
if a.realtimeOutboxWorkerCfg.Enabled && a.realtimeOutboxProducer != nil {
|
||||
a.startWalletOutboxWorkers(ctx, "wallet-realtime-outbox", a.realtimeOutboxWorkerCfg, a.realtimeOutboxProducer, a.realtimeWalletOutboxTopic, a.realtimeOutboxWorkerCfg.EventTypes, nil)
|
||||
}
|
||||
if a.redPacketExpiryWorkerCfg.Enabled {
|
||||
redPacketWorkerID := "red-packet-expiry-" + a.nodeID
|
||||
@ -228,12 +243,25 @@ func (a *App) runBackgroundWorkers() {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) runWalletOutboxWorker(ctx context.Context, workerID string) {
|
||||
options := a.outboxWorkerCfg
|
||||
func (a *App) startWalletOutboxWorkers(ctx context.Context, workerName string, options config.OutboxWorkerConfig, producer *rocketmqx.Producer, topic string, includeEventTypes []string, excludeEventTypes []string) {
|
||||
if options.Concurrency <= 0 {
|
||||
options.Concurrency = 1
|
||||
}
|
||||
for workerIndex := 1; workerIndex <= options.Concurrency; workerIndex++ {
|
||||
workerID := fmt.Sprintf("%s-%s-%02d", workerName, a.nodeID, workerIndex)
|
||||
a.workers.Add(1)
|
||||
go func() {
|
||||
defer a.workers.Done()
|
||||
a.runWalletOutboxWorker(ctx, workerID, options, producer, topic, includeEventTypes, excludeEventTypes)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) runWalletOutboxWorker(ctx context.Context, workerID string, options config.OutboxWorkerConfig, producer *rocketmqx.Producer, topic string, includeEventTypes []string, excludeEventTypes []string) {
|
||||
ticker := time.NewTicker(options.PollInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
processed, err := a.processWalletOutboxBatch(ctx, workerID, options)
|
||||
processed, err := a.processWalletOutboxBatch(ctx, workerID, options, producer, topic, includeEventTypes, excludeEventTypes)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
logx.Error(ctx, "wallet_outbox_publish_failed", err, slog.String("worker_id", workerID))
|
||||
}
|
||||
@ -248,11 +276,14 @@ func (a *App) runWalletOutboxWorker(ctx context.Context, workerID string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) processWalletOutboxBatch(ctx context.Context, workerID string, options config.OutboxWorkerConfig) (int, error) {
|
||||
if a.mysqlRepo == nil || a.outboxProducer == nil {
|
||||
func (a *App) processWalletOutboxBatch(ctx context.Context, workerID string, options config.OutboxWorkerConfig, producer *rocketmqx.Producer, topic string, includeEventTypes []string, excludeEventTypes []string) (int, error) {
|
||||
if a.mysqlRepo == nil || producer == nil {
|
||||
return 0, nil
|
||||
}
|
||||
records, err := a.mysqlRepo.ClaimPendingWalletOutbox(ctx, workerID, options.BatchSize, time.Now().UTC().Add(options.PublishTimeout).UnixMilli())
|
||||
records, err := a.mysqlRepo.ClaimPendingWalletOutboxFiltered(ctx, workerID, options.BatchSize, time.Now().UTC().Add(options.PublishTimeout).UnixMilli(), mysqlstorage.WalletOutboxClaimFilter{
|
||||
IncludeEventTypes: includeEventTypes,
|
||||
ExcludeEventTypes: excludeEventTypes,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -267,7 +298,7 @@ func (a *App) processWalletOutboxBatch(ctx context.Context, workerID string, opt
|
||||
continue
|
||||
}
|
||||
publishCtx, cancel := context.WithTimeout(appcode.WithContext(context.Background(), record.AppCode), options.PublishTimeout)
|
||||
err := a.publishWalletOutboxRecord(publishCtx, record)
|
||||
err := a.publishWalletOutboxRecord(publishCtx, producer, topic, record)
|
||||
cancel()
|
||||
if err != nil {
|
||||
nextRetryAtMS := time.Now().UTC().Add(walletOutboxBackoff(record.RetryCount+1, options)).UnixMilli()
|
||||
@ -289,7 +320,7 @@ func (a *App) processWalletOutboxBatch(ctx context.Context, workerID string, opt
|
||||
return len(records), nil
|
||||
}
|
||||
|
||||
func (a *App) publishWalletOutboxRecord(ctx context.Context, record mysqlstorage.WalletOutboxRecord) error {
|
||||
func (a *App) publishWalletOutboxRecord(ctx context.Context, producer *rocketmqx.Producer, topic string, record mysqlstorage.WalletOutboxRecord) error {
|
||||
body, err := walletmq.EncodeWalletOutboxMessage(walletmq.WalletOutboxMessage{
|
||||
AppCode: record.AppCode,
|
||||
EventID: record.EventID,
|
||||
@ -306,8 +337,8 @@ func (a *App) publishWalletOutboxRecord(ctx context.Context, record mysqlstorage
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.outboxProducer.SendSync(ctx, rocketmqx.Message{
|
||||
Topic: a.walletOutboxTopic,
|
||||
return producer.SendSync(ctx, rocketmqx.Message{
|
||||
Topic: topic,
|
||||
Tag: walletmq.TagWalletOutboxEvent,
|
||||
Keys: []string{record.EventID, record.TransactionID, record.CommandID},
|
||||
Body: body,
|
||||
@ -349,14 +380,20 @@ func (a *App) startMQ() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if a.projectionConsumer != nil {
|
||||
if err := a.projectionConsumer.Start(); err != nil {
|
||||
if a.realtimeOutboxProducer != nil {
|
||||
if err := a.realtimeOutboxProducer.Start(); err != nil {
|
||||
if a.outboxProducer != nil {
|
||||
_ = a.outboxProducer.Shutdown()
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
if a.projectionConsumer != nil {
|
||||
if err := a.projectionConsumer.Start(); err != nil {
|
||||
shutdownProducers(a.outboxProducer, a.realtimeOutboxProducer)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -371,6 +408,11 @@ func (a *App) shutdownMQ() {
|
||||
logx.Warn(context.Background(), "rocketmq_producer_shutdown_failed", slog.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
if a.realtimeOutboxProducer != nil {
|
||||
if err := a.realtimeOutboxProducer.Shutdown(); err != nil {
|
||||
logx.Warn(context.Background(), "rocketmq_realtime_producer_shutdown_failed", slog.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newWalletProjectionConsumer(cfg config.Config, svc *walletservice.Service) (*rocketmqx.Consumer, error) {
|
||||
@ -413,7 +455,7 @@ func rocketMQProjectionConsumerConfig(cfg config.RocketMQConfig, projection conf
|
||||
}
|
||||
}
|
||||
|
||||
func rocketMQProducerConfig(cfg config.RocketMQConfig) rocketmqx.ProducerConfig {
|
||||
func rocketMQProducerConfig(cfg config.RocketMQConfig, groupName string) rocketmqx.ProducerConfig {
|
||||
return rocketmqx.ProducerConfig{
|
||||
EndpointConfig: rocketmqx.EndpointConfig{
|
||||
NameServers: cfg.NameServers,
|
||||
@ -424,12 +466,20 @@ func rocketMQProducerConfig(cfg config.RocketMQConfig) rocketmqx.ProducerConfig
|
||||
Namespace: cfg.Namespace,
|
||||
VIPChannel: cfg.VIPChannel,
|
||||
},
|
||||
GroupName: cfg.WalletOutbox.ProducerGroup,
|
||||
GroupName: groupName,
|
||||
SendTimeout: cfg.SendTimeout,
|
||||
Retry: cfg.Retry,
|
||||
}
|
||||
}
|
||||
|
||||
func shutdownProducers(producers ...*rocketmqx.Producer) {
|
||||
for _, producer := range producers {
|
||||
if producer != nil {
|
||||
_ = producer.Shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) runHealthHTTP() {
|
||||
if a.healthHTTP == nil {
|
||||
return
|
||||
|
||||
@ -29,6 +29,8 @@ type Config struct {
|
||||
GooglePlay GooglePlayConfig `yaml:"google_play"`
|
||||
// OutboxWorker 控制 wallet_outbox 到 MQ 的补偿投递。
|
||||
OutboxWorker OutboxWorkerConfig `yaml:"outbox_worker"`
|
||||
// RealtimeOutboxWorker 控制红包等实时 UI 事件到独立 MQ topic 的补偿投递。
|
||||
RealtimeOutboxWorker OutboxWorkerConfig `yaml:"realtime_outbox_worker"`
|
||||
// ProjectionWorker 控制 wallet_outbox MQ 到本服务读模型的投影消费。
|
||||
ProjectionWorker ProjectionWorkerConfig `yaml:"projection_worker"`
|
||||
// MySQLAutoMigrate 保留给显式本地迁移;线上 schema 由发布流程或 initdb 管理。
|
||||
@ -57,6 +59,7 @@ type RocketMQConfig struct {
|
||||
SendTimeout time.Duration `yaml:"send_timeout"`
|
||||
Retry int `yaml:"retry"`
|
||||
WalletOutbox WalletOutboxMQConfig `yaml:"wallet_outbox"`
|
||||
RealtimeOutbox WalletOutboxMQConfig `yaml:"realtime_outbox"`
|
||||
}
|
||||
|
||||
// WalletOutboxMQConfig 保存 wallet_outbox topic 的生产者配置。
|
||||
@ -71,10 +74,12 @@ type OutboxWorkerConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
PollInterval time.Duration `yaml:"poll_interval"`
|
||||
BatchSize int `yaml:"batch_size"`
|
||||
Concurrency int `yaml:"concurrency"`
|
||||
PublishTimeout time.Duration `yaml:"publish_timeout"`
|
||||
MaxRetryCount int `yaml:"max_retry_count"`
|
||||
InitialBackoff time.Duration `yaml:"initial_backoff"`
|
||||
MaxBackoff time.Duration `yaml:"max_backoff"`
|
||||
EventTypes []string `yaml:"event_types"`
|
||||
}
|
||||
|
||||
// ProjectionWorkerConfig 控制礼物墙和 badge 展示读模型从 wallet_outbox MQ 投影的节奏。
|
||||
@ -127,11 +132,23 @@ func Default() Config {
|
||||
Enabled: false,
|
||||
PollInterval: time.Second,
|
||||
BatchSize: 100,
|
||||
Concurrency: 1,
|
||||
PublishTimeout: 3 * time.Second,
|
||||
MaxRetryCount: 10,
|
||||
InitialBackoff: 5 * time.Second,
|
||||
MaxBackoff: 5 * time.Minute,
|
||||
},
|
||||
RealtimeOutboxWorker: OutboxWorkerConfig{
|
||||
Enabled: false,
|
||||
PollInterval: time.Second,
|
||||
BatchSize: 50,
|
||||
Concurrency: 2,
|
||||
PublishTimeout: 3 * time.Second,
|
||||
MaxRetryCount: 10,
|
||||
InitialBackoff: 2 * time.Second,
|
||||
MaxBackoff: time.Minute,
|
||||
EventTypes: defaultRealtimeOutboxEventTypes(),
|
||||
},
|
||||
ProjectionWorker: ProjectionWorkerConfig{
|
||||
Enabled: false,
|
||||
PollInterval: 60 * time.Second,
|
||||
@ -159,6 +176,11 @@ func defaultRocketMQConfig() RocketMQConfig {
|
||||
Topic: "hyapp_wallet_outbox",
|
||||
ProducerGroup: "hyapp-wallet-outbox-producer",
|
||||
},
|
||||
RealtimeOutbox: WalletOutboxMQConfig{
|
||||
Enabled: false,
|
||||
Topic: "hyapp_wallet_realtime_outbox",
|
||||
ProducerGroup: "hyapp-wallet-realtime-outbox-producer",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,30 +252,17 @@ func Load(path string) (Config, error) {
|
||||
return Config{}, errors.New("google_play service account is required")
|
||||
}
|
||||
}
|
||||
if cfg.OutboxWorker.PollInterval <= 0 {
|
||||
cfg.OutboxWorker.PollInterval = Default().OutboxWorker.PollInterval
|
||||
}
|
||||
if cfg.OutboxWorker.BatchSize <= 0 {
|
||||
cfg.OutboxWorker.BatchSize = Default().OutboxWorker.BatchSize
|
||||
}
|
||||
if cfg.OutboxWorker.PublishTimeout <= 0 {
|
||||
cfg.OutboxWorker.PublishTimeout = Default().OutboxWorker.PublishTimeout
|
||||
}
|
||||
if cfg.OutboxWorker.MaxRetryCount <= 0 {
|
||||
cfg.OutboxWorker.MaxRetryCount = Default().OutboxWorker.MaxRetryCount
|
||||
}
|
||||
if cfg.OutboxWorker.InitialBackoff <= 0 {
|
||||
cfg.OutboxWorker.InitialBackoff = Default().OutboxWorker.InitialBackoff
|
||||
}
|
||||
if cfg.OutboxWorker.MaxBackoff <= 0 {
|
||||
cfg.OutboxWorker.MaxBackoff = Default().OutboxWorker.MaxBackoff
|
||||
}
|
||||
if cfg.OutboxWorker.MaxBackoff < cfg.OutboxWorker.InitialBackoff {
|
||||
cfg.OutboxWorker.MaxBackoff = cfg.OutboxWorker.InitialBackoff
|
||||
}
|
||||
cfg.OutboxWorker = normalizeOutboxWorkerConfig(cfg.OutboxWorker, Default().OutboxWorker)
|
||||
if cfg.OutboxWorker.Enabled && !cfg.RocketMQ.WalletOutbox.Enabled {
|
||||
return Config{}, errors.New("outbox_worker requires rocketmq.wallet_outbox.enabled")
|
||||
}
|
||||
cfg.RealtimeOutboxWorker = normalizeOutboxWorkerConfig(cfg.RealtimeOutboxWorker, Default().RealtimeOutboxWorker)
|
||||
if len(cfg.RealtimeOutboxWorker.EventTypes) == 0 {
|
||||
cfg.RealtimeOutboxWorker.EventTypes = defaultRealtimeOutboxEventTypes()
|
||||
}
|
||||
if cfg.RealtimeOutboxWorker.Enabled && !cfg.RocketMQ.RealtimeOutbox.Enabled {
|
||||
return Config{}, errors.New("realtime_outbox_worker requires rocketmq.realtime_outbox.enabled")
|
||||
}
|
||||
if cfg.ProjectionWorker.PollInterval <= 0 {
|
||||
cfg.ProjectionWorker.PollInterval = Default().ProjectionWorker.PollInterval
|
||||
}
|
||||
@ -299,7 +308,13 @@ func normalizeRocketMQConfig(cfg RocketMQConfig) (RocketMQConfig, error) {
|
||||
if cfg.WalletOutbox.ProducerGroup = strings.TrimSpace(cfg.WalletOutbox.ProducerGroup); cfg.WalletOutbox.ProducerGroup == "" {
|
||||
cfg.WalletOutbox.ProducerGroup = defaults.WalletOutbox.ProducerGroup
|
||||
}
|
||||
if cfg.WalletOutbox.Enabled {
|
||||
if cfg.RealtimeOutbox.Topic = strings.TrimSpace(cfg.RealtimeOutbox.Topic); cfg.RealtimeOutbox.Topic == "" {
|
||||
cfg.RealtimeOutbox.Topic = defaults.RealtimeOutbox.Topic
|
||||
}
|
||||
if cfg.RealtimeOutbox.ProducerGroup = strings.TrimSpace(cfg.RealtimeOutbox.ProducerGroup); cfg.RealtimeOutbox.ProducerGroup == "" {
|
||||
cfg.RealtimeOutbox.ProducerGroup = defaults.RealtimeOutbox.ProducerGroup
|
||||
}
|
||||
if cfg.WalletOutbox.Enabled || cfg.RealtimeOutbox.Enabled {
|
||||
cfg.Enabled = true
|
||||
}
|
||||
if cfg.Enabled {
|
||||
@ -313,12 +328,60 @@ func normalizeRocketMQConfig(cfg RocketMQConfig) (RocketMQConfig, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func normalizeOutboxWorkerConfig(cfg OutboxWorkerConfig, defaults OutboxWorkerConfig) OutboxWorkerConfig {
|
||||
if cfg.PollInterval <= 0 {
|
||||
cfg.PollInterval = defaults.PollInterval
|
||||
}
|
||||
if cfg.BatchSize <= 0 {
|
||||
cfg.BatchSize = defaults.BatchSize
|
||||
}
|
||||
if cfg.BatchSize > 500 {
|
||||
cfg.BatchSize = 500
|
||||
}
|
||||
if cfg.Concurrency <= 0 {
|
||||
cfg.Concurrency = defaults.Concurrency
|
||||
}
|
||||
if cfg.Concurrency > 64 {
|
||||
cfg.Concurrency = 64
|
||||
}
|
||||
if cfg.PublishTimeout <= 0 {
|
||||
cfg.PublishTimeout = defaults.PublishTimeout
|
||||
}
|
||||
if cfg.MaxRetryCount <= 0 {
|
||||
cfg.MaxRetryCount = defaults.MaxRetryCount
|
||||
}
|
||||
if cfg.InitialBackoff <= 0 {
|
||||
cfg.InitialBackoff = defaults.InitialBackoff
|
||||
}
|
||||
if cfg.MaxBackoff <= 0 {
|
||||
cfg.MaxBackoff = defaults.MaxBackoff
|
||||
}
|
||||
if cfg.MaxBackoff < cfg.InitialBackoff {
|
||||
cfg.MaxBackoff = cfg.InitialBackoff
|
||||
}
|
||||
cfg.EventTypes = normalizeStringSlice(cfg.EventTypes)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func normalizeStringSlice(values []string) []string {
|
||||
normalized := make([]string, 0, len(values))
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" {
|
||||
if _, exists := seen[value]; exists {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
normalized = append(normalized, value)
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func defaultRealtimeOutboxEventTypes() []string {
|
||||
return []string{
|
||||
"WalletRedPacketCreated",
|
||||
"WalletRedPacketClaimed",
|
||||
"WalletRedPacketRefunded",
|
||||
}
|
||||
}
|
||||
|
||||
38
services/wallet-service/internal/config/config_test.go
Normal file
38
services/wallet-service/internal/config/config_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLoadLocalKeepsOutboxMQDisabled(t *testing.T) {
|
||||
cfg, err := Load("../../configs/config.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Load local config failed: %v", err)
|
||||
}
|
||||
if cfg.RocketMQ.Enabled || cfg.RocketMQ.WalletOutbox.Enabled || cfg.RocketMQ.RealtimeOutbox.Enabled {
|
||||
t.Fatalf("local config must not require RocketMQ: %+v", cfg.RocketMQ)
|
||||
}
|
||||
if cfg.OutboxWorker.Concurrency != 1 || cfg.RealtimeOutboxWorker.Concurrency != 2 {
|
||||
t.Fatalf("local outbox worker defaults mismatch: outbox=%+v realtime=%+v", cfg.OutboxWorker, cfg.RealtimeOutboxWorker)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTencentExampleSplitsRealtimeOutboxAndRaisesThroughput(t *testing.T) {
|
||||
cfg, err := Load("../../configs/config.tencent.example.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Load tencent example failed: %v", err)
|
||||
}
|
||||
if !cfg.RocketMQ.Enabled || !cfg.RocketMQ.WalletOutbox.Enabled || !cfg.RocketMQ.RealtimeOutbox.Enabled {
|
||||
t.Fatalf("tencent example must enable both wallet outbox producers: %+v", cfg.RocketMQ)
|
||||
}
|
||||
if cfg.RocketMQ.WalletOutbox.Topic == cfg.RocketMQ.RealtimeOutbox.Topic {
|
||||
t.Fatalf("realtime outbox topic must be separated from generic wallet topic: %+v", cfg.RocketMQ)
|
||||
}
|
||||
if cfg.OutboxWorker.BatchSize != 200 || cfg.OutboxWorker.Concurrency != 8 {
|
||||
t.Fatalf("generic outbox throughput config mismatch: %+v", cfg.OutboxWorker)
|
||||
}
|
||||
if cfg.RealtimeOutboxWorker.BatchSize != 50 || cfg.RealtimeOutboxWorker.Concurrency != 4 {
|
||||
t.Fatalf("realtime outbox worker config mismatch: %+v", cfg.RealtimeOutboxWorker)
|
||||
}
|
||||
if len(cfg.RealtimeOutboxWorker.EventTypes) != 3 || cfg.RealtimeOutboxWorker.EventTypes[0] != "WalletRedPacketCreated" {
|
||||
t.Fatalf("realtime outbox event types mismatch: %+v", cfg.RealtimeOutboxWorker.EventTypes)
|
||||
}
|
||||
}
|
||||
@ -120,6 +120,34 @@ type DebitGiftCommand struct {
|
||||
TargetAgencyOwnerUserID int64
|
||||
}
|
||||
|
||||
// DebitGiftTargetCommand 是批量送礼中单个接收方的账务语义。
|
||||
type DebitGiftTargetCommand struct {
|
||||
// CommandID 是单个目标交易的幂等键;同一批量送礼必须为每个目标派生独立值。
|
||||
CommandID string
|
||||
// TargetUserID 是本次入账 GIFT_POINT 的用户。
|
||||
TargetUserID int64
|
||||
// TargetIsHost 只能由 gateway 注入,批量目标之间不能共享该身份快照。
|
||||
TargetIsHost bool
|
||||
// TargetHostRegionID 是该目标主播身份所属区域。
|
||||
TargetHostRegionID int64
|
||||
// TargetAgencyOwnerUserID 是该目标主播当前代理 owner 收款快照。
|
||||
TargetAgencyOwnerUserID int64
|
||||
}
|
||||
|
||||
// BatchDebitGiftCommand 在一个钱包事务中结算同一 sender 对多个 target 的同款礼物。
|
||||
type BatchDebitGiftCommand struct {
|
||||
AppCode string
|
||||
CommandID string
|
||||
RoomID string
|
||||
SenderUserID int64
|
||||
GiftID string
|
||||
GiftCount int32
|
||||
PriceVersion string
|
||||
RegionID int64
|
||||
SenderRegionID int64
|
||||
Targets []DebitGiftTargetCommand
|
||||
}
|
||||
|
||||
// Receipt 是账务命令落账后的稳定回执。
|
||||
type Receipt struct {
|
||||
BillingReceiptID string
|
||||
@ -138,6 +166,19 @@ type Receipt struct {
|
||||
HostPeriodCycleKey string
|
||||
}
|
||||
|
||||
// BatchGiftTargetReceipt 保留批量送礼里每个目标的独立交易回执。
|
||||
type BatchGiftTargetReceipt struct {
|
||||
TargetUserID int64
|
||||
CommandID string
|
||||
Receipt Receipt
|
||||
}
|
||||
|
||||
// BatchGiftReceipt 同时返回聚合展示值和逐目标事实回执。
|
||||
type BatchGiftReceipt struct {
|
||||
Aggregate Receipt
|
||||
Targets []BatchGiftTargetReceipt
|
||||
}
|
||||
|
||||
// HostSalarySettlementBatchCommand 是 cron 或测试触发结算批处理的最小输入。
|
||||
type HostSalarySettlementBatchCommand struct {
|
||||
AppCode string
|
||||
|
||||
@ -396,6 +396,29 @@ type EquipUserResourceCommand struct {
|
||||
EntitlementID string
|
||||
}
|
||||
|
||||
type UnequipUserResourceCommand struct {
|
||||
AppCode string
|
||||
UserID int64
|
||||
ResourceType string
|
||||
}
|
||||
|
||||
type UnequipUserResourceResult struct {
|
||||
ResourceType string
|
||||
Unequipped bool
|
||||
UpdatedAtMS int64
|
||||
}
|
||||
|
||||
type BatchGetUserEquippedResourcesQuery struct {
|
||||
AppCode string
|
||||
UserIDs []int64
|
||||
ResourceTypes []string
|
||||
}
|
||||
|
||||
type UserEquippedResources struct {
|
||||
UserID int64
|
||||
Resources []UserResourceEntitlement
|
||||
}
|
||||
|
||||
type ListResourceGrantsQuery struct {
|
||||
AppCode string
|
||||
TargetUserID int64
|
||||
|
||||
@ -199,6 +199,24 @@ func (s *Service) EquipUserResource(ctx context.Context, command resourcedomain.
|
||||
return s.repository.EquipUserResource(ctx, command)
|
||||
}
|
||||
|
||||
func (s *Service) UnequipUserResource(ctx context.Context, command resourcedomain.UnequipUserResourceCommand) (resourcedomain.UnequipUserResourceResult, error) {
|
||||
if s.repository == nil {
|
||||
return resourcedomain.UnequipUserResourceResult{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||
}
|
||||
command.AppCode = appcode.Normalize(command.AppCode)
|
||||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||||
return s.repository.UnequipUserResource(ctx, command)
|
||||
}
|
||||
|
||||
func (s *Service) BatchGetUserEquippedResources(ctx context.Context, query resourcedomain.BatchGetUserEquippedResourcesQuery) ([]resourcedomain.UserEquippedResources, error) {
|
||||
if s.repository == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||
}
|
||||
query.AppCode = appcode.Normalize(query.AppCode)
|
||||
ctx = appcode.WithContext(ctx, query.AppCode)
|
||||
return s.repository.BatchGetUserEquippedResources(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error) {
|
||||
if s.repository == nil {
|
||||
return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||
|
||||
@ -19,6 +19,7 @@ const walletBadgeGrantMetric = "wallet_badge_grant"
|
||||
// Repository 隔离账务存储,扣费必须由 repository 保证余额和幂等。
|
||||
type Repository interface {
|
||||
DebitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error)
|
||||
BatchDebitGift(ctx context.Context, command ledger.BatchDebitGiftCommand) (ledger.BatchGiftReceipt, error)
|
||||
GetBalances(ctx context.Context, userID int64, assetTypes []string) ([]ledger.AssetBalance, error)
|
||||
AdminCreditAsset(ctx context.Context, command ledger.AdminCreditAssetCommand) (ledger.AssetBalance, string, error)
|
||||
AdminCreditCoinSellerStock(ctx context.Context, command ledger.CoinSellerStockCreditCommand) (ledger.CoinSellerStockCreditReceipt, error)
|
||||
@ -75,6 +76,8 @@ type Repository interface {
|
||||
GrantResourceGroup(ctx context.Context, command resourcedomain.GrantResourceGroupCommand) (resourcedomain.ResourceGrant, error)
|
||||
ListUserResources(ctx context.Context, query resourcedomain.ListUserResourcesQuery) ([]resourcedomain.UserResourceEntitlement, error)
|
||||
EquipUserResource(ctx context.Context, command resourcedomain.EquipUserResourceCommand) (resourcedomain.UserResourceEntitlement, error)
|
||||
UnequipUserResource(ctx context.Context, command resourcedomain.UnequipUserResourceCommand) (resourcedomain.UnequipUserResourceResult, error)
|
||||
BatchGetUserEquippedResources(ctx context.Context, query resourcedomain.BatchGetUserEquippedResourcesQuery) ([]resourcedomain.UserEquippedResources, error)
|
||||
ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error)
|
||||
ListResourceShopItems(ctx context.Context, query resourcedomain.ListResourceShopItemsQuery) ([]resourcedomain.ResourceShopItem, int64, error)
|
||||
UpsertResourceShopItems(ctx context.Context, command resourcedomain.ResourceShopItemsCommand) ([]resourcedomain.ResourceShopItem, error)
|
||||
@ -142,6 +145,40 @@ func (s *Service) DebitGift(ctx context.Context, command ledger.DebitGiftCommand
|
||||
return s.repository.DebitGift(ctx, command)
|
||||
}
|
||||
|
||||
// BatchDebitGift 校验多目标送礼扣费命令,并要求 repository 在同一事务中完成全部目标结算。
|
||||
func (s *Service) BatchDebitGift(ctx context.Context, command ledger.BatchDebitGiftCommand) (ledger.BatchGiftReceipt, error) {
|
||||
if command.CommandID == "" || command.RoomID == "" || command.SenderUserID <= 0 || command.GiftID == "" {
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "billing command is incomplete")
|
||||
}
|
||||
if command.GiftCount <= 0 {
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift_count must be positive")
|
||||
}
|
||||
if len(command.Targets) == 0 {
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift targets are required")
|
||||
}
|
||||
seen := make(map[int64]struct{}, len(command.Targets))
|
||||
for _, target := range command.Targets {
|
||||
if target.CommandID == "" || target.TargetUserID <= 0 {
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift target is incomplete")
|
||||
}
|
||||
if _, exists := seen[target.TargetUserID]; exists {
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift target is duplicated")
|
||||
}
|
||||
seen[target.TargetUserID] = struct{}{}
|
||||
if target.TargetIsHost && target.TargetHostRegionID <= 0 {
|
||||
// 主播工资入账必须带该目标自己的主播区域,不能复用房间区域或其他目标快照。
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "target_host_region_id is required")
|
||||
}
|
||||
}
|
||||
if s.repository == nil {
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||
}
|
||||
command.AppCode = appcode.Normalize(command.AppCode)
|
||||
ctx = appcode.WithContext(ctx, command.AppCode)
|
||||
|
||||
return s.repository.BatchDebitGift(ctx, command)
|
||||
}
|
||||
|
||||
// ProcessHostSalarySettlementBatch 执行主播/代理工资结算批处理;日结和半月结不清空周期钻石,月底只做逻辑清算标记。
|
||||
func (s *Service) ProcessHostSalarySettlementBatch(ctx context.Context, command ledger.HostSalarySettlementBatchCommand) (ledger.HostSalarySettlementBatchResult, error) {
|
||||
if s.repository == nil {
|
||||
|
||||
@ -89,6 +89,77 @@ func TestDebitGiftUsesServerPriceAndCreditsGiftPoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatchDebitGiftSettlesAllTargetsAtomically 验证多目标送礼一次扣总额,并为每个接收方写独立收礼事实。
|
||||
func TestBatchDebitGiftSettlesAllTargetsAtomically(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
repository.SetBalance(10001, 100)
|
||||
repository.SetGiftPrice("rose", "v9", 7, 3, 11)
|
||||
svc := walletservice.New(repository)
|
||||
|
||||
receipt, err := svc.BatchDebitGift(context.Background(), ledger.BatchDebitGiftCommand{
|
||||
CommandID: "cmd-gift-batch",
|
||||
RoomID: "room-1",
|
||||
SenderUserID: 10001,
|
||||
GiftID: "rose",
|
||||
GiftCount: 2,
|
||||
PriceVersion: "v9",
|
||||
SenderRegionID: 1001,
|
||||
Targets: []ledger.DebitGiftTargetCommand{
|
||||
{CommandID: "cmd-gift-batch:target:10002", TargetUserID: 10002},
|
||||
{CommandID: "cmd-gift-batch:target:10003", TargetUserID: 10003},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BatchDebitGift failed: %v", err)
|
||||
}
|
||||
|
||||
if receipt.Aggregate.CoinSpent != 28 || receipt.Aggregate.GiftPointAdded != 12 || receipt.Aggregate.HeatValue != 44 || receipt.Aggregate.BalanceAfter != 72 || len(receipt.Targets) != 2 {
|
||||
t.Fatalf("batch aggregate mismatch: %+v", receipt)
|
||||
}
|
||||
for _, userID := range []int64{10002, 10003} {
|
||||
balances, err := svc.GetBalances(context.Background(), userID, []string{ledger.AssetGiftPoint})
|
||||
if err != nil {
|
||||
t.Fatalf("GetBalances target %d failed: %v", userID, err)
|
||||
}
|
||||
if balanceAmount(balances, ledger.AssetGiftPoint) != 6 {
|
||||
t.Fatalf("target %d GIFT_POINT mismatch: %+v", userID, balances)
|
||||
}
|
||||
}
|
||||
balances, err := svc.GetBalances(context.Background(), 10001, []string{ledger.AssetCoin})
|
||||
if err != nil {
|
||||
t.Fatalf("GetBalances sender failed: %v", err)
|
||||
}
|
||||
if balanceAmount(balances, ledger.AssetCoin) != 72 {
|
||||
t.Fatalf("sender COIN balance mismatch: %+v", balances)
|
||||
}
|
||||
if got := repository.CountRows("wallet_transactions", "command_id IN (?, ?)", "cmd-gift-batch:target:10002", "cmd-gift-batch:target:10003"); got != 2 {
|
||||
t.Fatalf("batch gift should write one transaction per target, got %d", got)
|
||||
}
|
||||
if got := repository.CountRows("wallet_entries", "transaction_id IN (?, ?)", receipt.Targets[0].Receipt.TransactionID, receipt.Targets[1].Receipt.TransactionID); got != 4 {
|
||||
t.Fatalf("batch gift should write sender and target entries per target, got %d", got)
|
||||
}
|
||||
|
||||
again, err := svc.BatchDebitGift(context.Background(), ledger.BatchDebitGiftCommand{
|
||||
CommandID: "cmd-gift-batch",
|
||||
RoomID: "room-1",
|
||||
SenderUserID: 10001,
|
||||
GiftID: "rose",
|
||||
GiftCount: 2,
|
||||
PriceVersion: "v9",
|
||||
SenderRegionID: 1001,
|
||||
Targets: []ledger.DebitGiftTargetCommand{
|
||||
{CommandID: "cmd-gift-batch:target:10002", TargetUserID: 10002},
|
||||
{CommandID: "cmd-gift-batch:target:10003", TargetUserID: 10003},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BatchDebitGift replay failed: %v", err)
|
||||
}
|
||||
if again.Aggregate.CoinSpent != receipt.Aggregate.CoinSpent || again.Targets[0].Receipt.TransactionID != receipt.Targets[0].Receipt.TransactionID || repository.CountRows("wallet_transactions", "command_id IN (?, ?)", "cmd-gift-batch:target:10002", "cmd-gift-batch:target:10003") != 2 {
|
||||
t.Fatalf("batch idempotency mismatch: first=%+v again=%+v", receipt, again)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDebitGiftCreditsHostPeriodDiamondsOnlyForHost 验证只有 gateway 确认的 active 主播会增加工资周期钻石。
|
||||
func TestDebitGiftCreditsHostPeriodDiamondsOnlyForHost(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
@ -1141,6 +1212,67 @@ func TestRedPacketLifecycleUsesRealMySQL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalletOutboxClaimSeparatesRealtimeEventTypes(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
repository.SetBalance(19101, 1000)
|
||||
svc := walletservice.New(repository)
|
||||
if _, err := svc.UpdateRedPacketConfig(context.Background(), ledger.RedPacketConfig{
|
||||
AppCode: "lalu",
|
||||
Enabled: true,
|
||||
CountTiers: []int32{5},
|
||||
AmountTiers: []int64{10},
|
||||
DelayedOpenSeconds: 1,
|
||||
ExpireSeconds: 2,
|
||||
DailySendLimit: 5,
|
||||
UpdatedByAdminID: 9001,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdateRedPacketConfig failed: %v", err)
|
||||
}
|
||||
receipt, err := svc.CreateRedPacket(context.Background(), ledger.RedPacketCreateCommand{
|
||||
AppCode: "lalu",
|
||||
CommandID: "cmd-red-packet-realtime-outbox",
|
||||
SenderUserID: 19101,
|
||||
RoomID: "room-191",
|
||||
RegionID: 191,
|
||||
PacketType: ledger.RedPacketTypeNormal,
|
||||
TotalAmount: 10,
|
||||
PacketCount: 5,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRedPacket failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||
realtimeTypes := []string{"WalletRedPacketCreated", "WalletRedPacketClaimed", "WalletRedPacketRefunded"}
|
||||
realtimeRecords, err := repository.ClaimPendingWalletOutboxFiltered(ctx, "wallet-realtime-outbox-test", 10, time.Now().Add(time.Minute).UnixMilli(), mysqlstorage.WalletOutboxClaimFilter{
|
||||
IncludeEventTypes: realtimeTypes,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimPendingWalletOutboxFiltered realtime failed: %v", err)
|
||||
}
|
||||
if len(realtimeRecords) != 1 || realtimeRecords[0].EventType != "WalletRedPacketCreated" {
|
||||
t.Fatalf("realtime claim should only pick red packet fact, got %+v", realtimeRecords)
|
||||
}
|
||||
|
||||
normalRecords, err := repository.ClaimPendingWalletOutboxFiltered(ctx, "wallet-normal-outbox-test", 10, time.Now().Add(time.Minute).UnixMilli(), mysqlstorage.WalletOutboxClaimFilter{
|
||||
ExcludeEventTypes: realtimeTypes,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimPendingWalletOutboxFiltered normal failed: %v", err)
|
||||
}
|
||||
if len(normalRecords) == 0 {
|
||||
t.Fatalf("normal claim should still pick non-realtime wallet facts for packet %s", receipt.Packet.PacketID)
|
||||
}
|
||||
for _, record := range normalRecords {
|
||||
if record.EventType == "WalletRedPacketCreated" || record.EventType == "WalletRedPacketClaimed" || record.EventType == "WalletRedPacketRefunded" {
|
||||
t.Fatalf("normal claim must exclude realtime red packet fact: %+v", normalRecords)
|
||||
}
|
||||
}
|
||||
if got := repository.CountRows("wallet_outbox", "event_id = ? AND status = ? AND worker_id = ?", realtimeRecords[0].EventID, "delivering", "wallet-realtime-outbox-test"); got != 1 {
|
||||
t.Fatalf("realtime fact should be locked by realtime worker, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalletOutboxClaimUsesRealMySQLStatusFlow(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
repository.SetAssetBalance(30001, ledger.AssetCoinSellerCoin, 1000)
|
||||
@ -2465,6 +2597,86 @@ func TestEquipUserResourceRequiresActiveEquipableEntitlement(t *testing.T) {
|
||||
if got := repository.CountRows("user_resource_equipment", "user_id = ? AND resource_type = ?", int64(43001), resourcedomain.TypeBadge); got != 1 {
|
||||
t.Fatalf("equipment should keep one active badge, got %d", got)
|
||||
}
|
||||
equippedResources, err := svc.BatchGetUserEquippedResources(ctx, resourcedomain.BatchGetUserEquippedResourcesQuery{
|
||||
UserIDs: []int64{43001},
|
||||
ResourceTypes: []string{resourcedomain.TypeBadge},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch get equipped resources failed: %v", err)
|
||||
}
|
||||
if len(equippedResources) != 1 || len(equippedResources[0].Resources) != 1 || equippedResources[0].Resources[0].EntitlementID != equipped.EntitlementID {
|
||||
t.Fatalf("batch equipped resources mismatch: %+v", equippedResources)
|
||||
}
|
||||
unequipped, err := svc.UnequipUserResource(ctx, resourcedomain.UnequipUserResourceCommand{
|
||||
UserID: 43001,
|
||||
ResourceType: resourcedomain.TypeBadge,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unequip badge resource failed: %v", err)
|
||||
}
|
||||
if !unequipped.Unequipped || unequipped.ResourceType != resourcedomain.TypeBadge {
|
||||
t.Fatalf("unequip result mismatch: %+v", unequipped)
|
||||
}
|
||||
if got := repository.CountRows("user_resource_equipment", "user_id = ? AND resource_type = ?", int64(43001), resourcedomain.TypeBadge); got != 0 {
|
||||
t.Fatalf("equipment should be removed after unequip, got %d", got)
|
||||
}
|
||||
emptyUnequip, err := svc.UnequipUserResource(ctx, resourcedomain.UnequipUserResourceCommand{
|
||||
UserID: 43001,
|
||||
ResourceType: resourcedomain.TypeBadge,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("repeat unequip badge resource failed: %v", err)
|
||||
}
|
||||
if emptyUnequip.Unequipped {
|
||||
t.Fatalf("repeat unequip should be idempotent false: %+v", emptyUnequip)
|
||||
}
|
||||
|
||||
vehicleResource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||
ResourceCode: "vehicle_expiring",
|
||||
ResourceType: resourcedomain.TypeVehicle,
|
||||
Name: "Expiring Vehicle",
|
||||
Status: resourcedomain.StatusActive,
|
||||
Grantable: true,
|
||||
GrantStrategy: resourcedomain.GrantStrategySetActiveFlag,
|
||||
UsageScopes: []string{"room_entry"},
|
||||
OperatorUserID: 90001,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create vehicle resource failed: %v", err)
|
||||
}
|
||||
vehicleGrant, err := svc.GrantResource(ctx, resourcedomain.GrantResourceCommand{
|
||||
CommandID: "cmd-grant-expiring-vehicle",
|
||||
TargetUserID: 43001,
|
||||
ResourceID: vehicleResource.ResourceID,
|
||||
Quantity: 1,
|
||||
DurationMS: int64(time.Hour / time.Millisecond),
|
||||
Reason: "test expired equipment",
|
||||
OperatorUserID: 90001,
|
||||
GrantSource: resourcedomain.GrantSourceAdmin,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("grant vehicle resource failed: %v", err)
|
||||
}
|
||||
if _, err := svc.EquipUserResource(ctx, resourcedomain.EquipUserResourceCommand{
|
||||
UserID: 43001,
|
||||
ResourceID: vehicleResource.ResourceID,
|
||||
}); err != nil {
|
||||
t.Fatalf("equip vehicle resource failed: %v", err)
|
||||
}
|
||||
repository.ExpireEntitlement(vehicleGrant.Items[0].EntitlementID, time.Now().Add(-time.Second).UnixMilli())
|
||||
expiredEquipped, err := svc.BatchGetUserEquippedResources(ctx, resourcedomain.BatchGetUserEquippedResourcesQuery{
|
||||
UserIDs: []int64{43001},
|
||||
ResourceTypes: []string{resourcedomain.TypeVehicle},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch get expired equipped resources failed: %v", err)
|
||||
}
|
||||
if len(expiredEquipped) != 1 || len(expiredEquipped[0].Resources) != 0 {
|
||||
t.Fatalf("expired equipment should be filtered: %+v", expiredEquipped)
|
||||
}
|
||||
if got := repository.CountRows("user_resource_equipment", "user_id = ? AND resource_type = ?", int64(43001), resourcedomain.TypeVehicle); got != 0 {
|
||||
t.Fatalf("expired equipment should be pruned, got %d", got)
|
||||
}
|
||||
|
||||
giftResource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||
ResourceCode: "gift_not_equipable",
|
||||
|
||||
@ -68,6 +68,14 @@ type WalletOutboxRecord struct {
|
||||
LockUntilMS int64
|
||||
}
|
||||
|
||||
// WalletOutboxClaimFilter narrows one MQ fanout lane by event type.
|
||||
type WalletOutboxClaimFilter struct {
|
||||
// IncludeEventTypes lets a realtime worker own only the configured UI event facts.
|
||||
IncludeEventTypes []string
|
||||
// ExcludeEventTypes lets the generic wallet worker skip facts owned by the realtime lane.
|
||||
ExcludeEventTypes []string
|
||||
}
|
||||
|
||||
// Open 创建 MySQL 连接池并做启动 ping。
|
||||
func Open(ctx context.Context, dsn string) (*Repository, error) {
|
||||
if strings.TrimSpace(dsn) == "" {
|
||||
@ -322,6 +330,257 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
||||
return receiptFromGiftMetadata(transactionID, metadata), nil
|
||||
}
|
||||
|
||||
// BatchDebitGift 在一个 MySQL 事务内完成多目标送礼;任一目标失败时整批回滚,避免房间命令和账务出现部分成功。
|
||||
func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDebitGiftCommand) (ledger.BatchGiftReceipt, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
ctx = contextWithCommandApp(ctx, command.AppCode)
|
||||
command.AppCode = appcode.FromContext(ctx)
|
||||
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
defer func() {
|
||||
// Commit 成功后 Rollback 会返回 sql.ErrTxDone;忽略它可保证所有早退路径释放事务。
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
nowMs := time.Now().UnixMilli()
|
||||
if command.RegionID < 0 {
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "region_id is invalid")
|
||||
}
|
||||
if command.SenderRegionID < 0 {
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "sender_region_id is invalid")
|
||||
}
|
||||
giftConfig, err := r.resolveActiveGiftConfig(ctx, tx, command.GiftID, command.RegionID)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
price, err := r.resolveGiftPrice(ctx, tx, command.GiftID, command.PriceVersion, nowMs)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
chargeAmount, err := checkedMul(price.CoinPrice, int64(command.GiftCount))
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
giftPointAdded, err := checkedMul(price.GiftPointAmount, int64(command.GiftCount))
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
heatValue, err := checkedMul(price.HeatValue, int64(command.GiftCount))
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
totalChargeAmount, err := checkedMul(chargeAmount, int64(len(command.Targets)))
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
|
||||
// 批量命令的每个目标都有独立 wallet command_id;幂等重试必须是“全部已存在”或“全部未存在”,不接受半批历史。
|
||||
existing := make(map[string]ledger.BatchGiftTargetReceipt, len(command.Targets))
|
||||
for _, target := range command.Targets {
|
||||
targetCommand := debitGiftCommandFromBatchTarget(command, target)
|
||||
requestHash := debitRequestHash(targetCommand)
|
||||
txRow, exists, err := r.lookupTransaction(ctx, tx, target.CommandID, requestHash, bizTypeGiftDebit)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
receipt, err := r.receiptForGiftTransaction(ctx, tx, txRow.TransactionID)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
existing[target.CommandID] = ledger.BatchGiftTargetReceipt{
|
||||
TargetUserID: target.TargetUserID,
|
||||
CommandID: target.CommandID,
|
||||
Receipt: receipt,
|
||||
}
|
||||
}
|
||||
if len(existing) > 0 {
|
||||
if len(existing) != len(command.Targets) {
|
||||
// 同一个 room 命令不应该只存在部分 wallet 子交易;直接 fail-close,等待人工核对账本。
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.IdempotencyConflict, "wallet batch command idempotency conflict")
|
||||
}
|
||||
targetReceipts := make([]ledger.BatchGiftTargetReceipt, 0, len(command.Targets))
|
||||
for _, target := range command.Targets {
|
||||
targetReceipts = append(targetReceipts, existing[target.CommandID])
|
||||
}
|
||||
aggregate, err := aggregateGiftReceipts(targetReceipts)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
return ledger.BatchGiftReceipt{Aggregate: aggregate, Targets: targetReceipts}, nil
|
||||
}
|
||||
|
||||
sender, err := r.lockAccount(ctx, tx, command.SenderUserID, price.ChargeAssetType, totalChargeAmount == 0, nowMs)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
if sender.AvailableAmount < totalChargeAmount {
|
||||
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
|
||||
}
|
||||
|
||||
targetAccounts, err := r.lockGiftTargetAccounts(ctx, tx, command.Targets, nowMs)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
|
||||
anyHostTarget := false
|
||||
for _, target := range command.Targets {
|
||||
if target.TargetIsHost {
|
||||
anyHostTarget = true
|
||||
break
|
||||
}
|
||||
}
|
||||
hostRatio := giftDiamondRatioSnapshot{Percent: "0.00", PPM: 0}
|
||||
hostRatioRegionID := int64(0)
|
||||
if anyHostTarget {
|
||||
hostRatio, hostRatioRegionID, err = r.resolveGiftDiamondRatio(ctx, tx, command.AppCode, command.SenderRegionID, giftConfig.GiftTypeCode)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
}
|
||||
|
||||
events := make([]walletOutboxEvent, 0, len(command.Targets)*4)
|
||||
targetReceipts := make([]ledger.BatchGiftTargetReceipt, 0, len(command.Targets))
|
||||
for _, target := range command.Targets {
|
||||
hostPeriodDiamondAdded := int64(0)
|
||||
hostPeriodCycleKey := ""
|
||||
giftDiamondRatioPercent := "0.00"
|
||||
giftDiamondRatioRegionID := int64(0)
|
||||
if target.TargetIsHost {
|
||||
// 每个目标只使用自己的 host 快照;不能把某个主播的 agency/region 套给批量里的其他用户。
|
||||
hostPeriodDiamondAdded, err = giftDiamondAmount(chargeAmount, hostRatio.PPM)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
giftDiamondRatioPercent = hostRatio.Percent
|
||||
giftDiamondRatioRegionID = hostRatioRegionID
|
||||
hostPeriodCycleKey = hostPeriodCycleKeyFromMS(nowMs)
|
||||
}
|
||||
|
||||
transactionID := transactionID(command.AppCode, target.CommandID)
|
||||
senderAfter := sender.AvailableAmount - chargeAmount
|
||||
metadata := giftMetadata{
|
||||
AppCode: command.AppCode,
|
||||
GiftID: command.GiftID,
|
||||
GiftName: giftConfig.Name,
|
||||
ResourceID: giftConfig.ResourceID,
|
||||
ResourceSnapshot: mustJSON(giftConfig.Resource),
|
||||
GiftTypeCode: giftConfig.GiftTypeCode,
|
||||
PresentationJSON: giftConfig.PresentationJSON,
|
||||
SortOrder: giftConfig.SortOrder,
|
||||
GiftCount: command.GiftCount,
|
||||
PriceVersion: price.PriceVersion,
|
||||
ChargeAssetType: price.ChargeAssetType,
|
||||
ChargeAmount: chargeAmount,
|
||||
CoinSpent: chargeAmount,
|
||||
GiftPointAdded: giftPointAdded,
|
||||
HeatValue: heatValue,
|
||||
BalanceAfter: senderAfter,
|
||||
BillingReceipt: billingReceiptID(command.AppCode, target.CommandID),
|
||||
SenderUserID: command.SenderUserID,
|
||||
SenderRegionID: command.SenderRegionID,
|
||||
TargetUserID: target.TargetUserID,
|
||||
TargetIsHost: target.TargetIsHost,
|
||||
TargetHostRegionID: target.TargetHostRegionID,
|
||||
TargetAgencyOwnerUserID: target.TargetAgencyOwnerUserID,
|
||||
HostPeriodDiamondAdded: hostPeriodDiamondAdded,
|
||||
GiftDiamondRatioPercent: giftDiamondRatioPercent,
|
||||
GiftDiamondRatioRegionID: giftDiamondRatioRegionID,
|
||||
HostPeriodCycleKey: hostPeriodCycleKey,
|
||||
RoomID: command.RoomID,
|
||||
CoinPrice: price.CoinPrice,
|
||||
GiftPointAmount: price.GiftPointAmount,
|
||||
HeatUnitValue: price.HeatValue,
|
||||
}
|
||||
if err := r.insertTransaction(ctx, tx, transactionID, target.CommandID, bizTypeGiftDebit, debitRequestHash(debitGiftCommandFromBatchTarget(command, target)), fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
if err := r.applyAccountDelta(ctx, tx, sender, -chargeAmount, 0, nowMs); err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
events = append(events, balanceChangedEvent(transactionID, target.CommandID, command.SenderUserID, price.ChargeAssetType, -chargeAmount, 0, senderAfter, sender.FrozenAmount, sender.Version+1, metadata, nowMs))
|
||||
sender.AvailableAmount = senderAfter
|
||||
sender.Version++
|
||||
|
||||
targetAccount := targetAccounts[target.TargetUserID]
|
||||
if err := r.applyAccountDelta(ctx, tx, targetAccount, giftPointAdded, 0, nowMs); err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
targetAfter := targetAccount.AvailableAmount + giftPointAdded
|
||||
events = append(events, balanceChangedEvent(transactionID, target.CommandID, target.TargetUserID, ledger.AssetGiftPoint, giftPointAdded, 0, targetAfter, targetAccount.FrozenAmount, targetAccount.Version+1, metadata, nowMs))
|
||||
targetAccount.AvailableAmount = targetAfter
|
||||
targetAccount.Version++
|
||||
targetAccounts[target.TargetUserID] = targetAccount
|
||||
|
||||
if err := r.insertEntry(ctx, tx, walletEntry{
|
||||
TransactionID: transactionID,
|
||||
UserID: command.SenderUserID,
|
||||
AssetType: price.ChargeAssetType,
|
||||
AvailableDelta: -chargeAmount,
|
||||
FrozenDelta: 0,
|
||||
AvailableAfter: senderAfter,
|
||||
FrozenAfter: sender.FrozenAmount,
|
||||
CounterpartyUserID: target.TargetUserID,
|
||||
RoomID: command.RoomID,
|
||||
CreatedAtMS: nowMs,
|
||||
}); err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
if err := r.insertEntry(ctx, tx, walletEntry{
|
||||
TransactionID: transactionID,
|
||||
UserID: target.TargetUserID,
|
||||
AssetType: ledger.AssetGiftPoint,
|
||||
AvailableDelta: giftPointAdded,
|
||||
FrozenDelta: 0,
|
||||
AvailableAfter: targetAfter,
|
||||
FrozenAfter: targetAccount.FrozenAmount,
|
||||
CounterpartyUserID: command.SenderUserID,
|
||||
RoomID: command.RoomID,
|
||||
CreatedAtMS: nowMs,
|
||||
}); err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
if hostPeriodDiamondAdded > 0 {
|
||||
hostPeriodDiamondAfter, hostPeriodDiamondVersion, err := r.creditHostPeriodDiamonds(ctx, tx, transactionID, target.CommandID, metadata, nowMs)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
metadata.HostPeriodDiamondAfter = hostPeriodDiamondAfter
|
||||
metadata.HostPeriodDiamondVersion = hostPeriodDiamondVersion
|
||||
}
|
||||
|
||||
events = append(events, giftDebitedEvent(transactionID, target.CommandID, command.SenderUserID, price.ChargeAssetType, -chargeAmount, 0, metadata, nowMs))
|
||||
if hostPeriodDiamondAdded > 0 {
|
||||
events = append(events, hostPeriodDiamondCreditedEvent(transactionID, target.CommandID, metadata, nowMs))
|
||||
}
|
||||
targetReceipts = append(targetReceipts, ledger.BatchGiftTargetReceipt{
|
||||
TargetUserID: target.TargetUserID,
|
||||
CommandID: target.CommandID,
|
||||
Receipt: receiptFromGiftMetadata(transactionID, metadata),
|
||||
})
|
||||
}
|
||||
|
||||
if err := r.insertWalletOutbox(ctx, tx, events); err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
|
||||
aggregate, err := aggregateGiftReceipts(targetReceipts)
|
||||
if err != nil {
|
||||
return ledger.BatchGiftReceipt{}, err
|
||||
}
|
||||
return ledger.BatchGiftReceipt{Aggregate: aggregate, Targets: targetReceipts}, nil
|
||||
}
|
||||
|
||||
// GetBalances 读取用户多资产余额;指定资产不存在时返回 0 投影,便于客户端稳定渲染。
|
||||
func (r *Repository) GetBalances(ctx context.Context, userID int64, assetTypes []string) ([]ledger.AssetBalance, error) {
|
||||
if r == nil || r.db == nil {
|
||||
@ -1642,6 +1901,11 @@ func (r *Repository) insertWalletOutbox(ctx context.Context, tx *sql.Tx, events
|
||||
|
||||
// ClaimPendingWalletOutbox atomically claims committed wallet facts for MQ fanout.
|
||||
func (r *Repository) ClaimPendingWalletOutbox(ctx context.Context, workerID string, limit int, lockUntilMS int64) ([]WalletOutboxRecord, error) {
|
||||
return r.ClaimPendingWalletOutboxFiltered(ctx, workerID, limit, lockUntilMS, WalletOutboxClaimFilter{})
|
||||
}
|
||||
|
||||
// ClaimPendingWalletOutboxFiltered atomically claims committed wallet facts for one MQ fanout lane.
|
||||
func (r *Repository) ClaimPendingWalletOutboxFiltered(ctx context.Context, workerID string, limit int, lockUntilMS int64, filter WalletOutboxClaimFilter) ([]WalletOutboxRecord, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
@ -1661,6 +1925,8 @@ func (r *Repository) ClaimPendingWalletOutbox(ctx context.Context, workerID stri
|
||||
}
|
||||
|
||||
appCode := appcode.FromContext(ctx)
|
||||
eventFilterSQL, eventFilterArgs := walletOutboxEventFilterSQL(filter)
|
||||
pendingIndex, claimIndex := walletOutboxClaimIndexes(filter)
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1673,45 +1939,21 @@ func (r *Repository) ClaimPendingWalletOutbox(ctx context.Context, workerID stri
|
||||
args []any
|
||||
}{
|
||||
{
|
||||
query: `
|
||||
SELECT app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type,
|
||||
available_delta, frozen_delta, COALESCE(CAST(payload AS CHAR), '{}'), created_at_ms,
|
||||
retry_count, COALESCE(last_error, ''), COALESCE(worker_id, ''), COALESCE(lock_until_ms, 0)
|
||||
FROM wallet_outbox FORCE INDEX (idx_wallet_outbox_pending)
|
||||
WHERE app_code = ? AND status = ?
|
||||
query: walletOutboxClaimQuery(pendingIndex, `
|
||||
AND next_retry_at_ms IS NULL
|
||||
AND lock_until_ms IS NULL
|
||||
ORDER BY created_at_ms ASC, event_id ASC
|
||||
LIMIT ?
|
||||
FOR UPDATE SKIP LOCKED`,
|
||||
args: []any{appCode, outboxStatusPending},
|
||||
AND lock_until_ms IS NULL`, eventFilterSQL),
|
||||
args: append([]any{appCode, outboxStatusPending}, eventFilterArgs...),
|
||||
},
|
||||
{
|
||||
query: `
|
||||
SELECT app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type,
|
||||
available_delta, frozen_delta, COALESCE(CAST(payload AS CHAR), '{}'), created_at_ms,
|
||||
retry_count, COALESCE(last_error, ''), COALESCE(worker_id, ''), COALESCE(lock_until_ms, 0)
|
||||
FROM wallet_outbox FORCE INDEX (idx_wallet_outbox_pending)
|
||||
WHERE app_code = ? AND status = ?
|
||||
query: walletOutboxClaimQuery(pendingIndex, `
|
||||
AND next_retry_at_ms <= ?
|
||||
AND lock_until_ms IS NULL
|
||||
ORDER BY created_at_ms ASC, event_id ASC
|
||||
LIMIT ?
|
||||
FOR UPDATE SKIP LOCKED`,
|
||||
args: []any{appCode, outboxStatusRetryable, nowMS},
|
||||
AND lock_until_ms IS NULL`, eventFilterSQL),
|
||||
args: append([]any{appCode, outboxStatusRetryable, nowMS}, eventFilterArgs...),
|
||||
},
|
||||
{
|
||||
query: `
|
||||
SELECT app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type,
|
||||
available_delta, frozen_delta, COALESCE(CAST(payload AS CHAR), '{}'), created_at_ms,
|
||||
retry_count, COALESCE(last_error, ''), COALESCE(worker_id, ''), COALESCE(lock_until_ms, 0)
|
||||
FROM wallet_outbox FORCE INDEX (idx_wallet_outbox_claim)
|
||||
WHERE app_code = ? AND status = ?
|
||||
AND lock_until_ms <= ?
|
||||
ORDER BY created_at_ms ASC, event_id ASC
|
||||
LIMIT ?
|
||||
FOR UPDATE SKIP LOCKED`,
|
||||
args: []any{appCode, outboxStatusDelivering, nowMS},
|
||||
query: walletOutboxClaimQuery(claimIndex, `
|
||||
AND lock_until_ms <= ?`, eventFilterSQL),
|
||||
args: append([]any{appCode, outboxStatusDelivering, nowMS}, eventFilterArgs...),
|
||||
},
|
||||
}
|
||||
for _, branch := range claimBranches {
|
||||
@ -1752,6 +1994,73 @@ func (r *Repository) ClaimPendingWalletOutbox(ctx context.Context, workerID stri
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func walletOutboxClaimQuery(indexName string, statusCondition string, eventFilterSQL string) string {
|
||||
return `
|
||||
SELECT app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type,
|
||||
available_delta, frozen_delta, COALESCE(CAST(payload AS CHAR), '{}'), created_at_ms,
|
||||
retry_count, COALESCE(last_error, ''), COALESCE(worker_id, ''), COALESCE(lock_until_ms, 0)
|
||||
FROM wallet_outbox FORCE INDEX (` + indexName + `)
|
||||
WHERE app_code = ? AND status = ?` + statusCondition + eventFilterSQL + `
|
||||
ORDER BY created_at_ms ASC, event_id ASC
|
||||
LIMIT ?
|
||||
FOR UPDATE SKIP LOCKED`
|
||||
}
|
||||
|
||||
func walletOutboxEventFilterSQL(filter WalletOutboxClaimFilter) (string, []any) {
|
||||
include := normalizeWalletOutboxEventTypes(filter.IncludeEventTypes)
|
||||
if len(include) > 0 {
|
||||
return " AND event_type IN (" + walletOutboxPlaceholders(len(include)) + ")", walletOutboxEventArgs(include)
|
||||
}
|
||||
exclude := normalizeWalletOutboxEventTypes(filter.ExcludeEventTypes)
|
||||
if len(exclude) > 0 {
|
||||
return " AND event_type NOT IN (" + walletOutboxPlaceholders(len(exclude)) + ")", walletOutboxEventArgs(exclude)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func walletOutboxClaimIndexes(filter WalletOutboxClaimFilter) (string, string) {
|
||||
if len(normalizeWalletOutboxEventTypes(filter.IncludeEventTypes)) > 0 {
|
||||
return "idx_wallet_outbox_event_pending", "idx_wallet_outbox_event_claim"
|
||||
}
|
||||
return "idx_wallet_outbox_pending", "idx_wallet_outbox_claim"
|
||||
}
|
||||
|
||||
func normalizeWalletOutboxEventTypes(values []string) []string {
|
||||
normalized := make([]string, 0, len(values))
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[value]; exists {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
normalized = append(normalized, value)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func walletOutboxPlaceholders(count int) string {
|
||||
if count <= 0 {
|
||||
return ""
|
||||
}
|
||||
placeholders := make([]string, count)
|
||||
for index := range placeholders {
|
||||
placeholders[index] = "?"
|
||||
}
|
||||
return strings.Join(placeholders, ",")
|
||||
}
|
||||
|
||||
func walletOutboxEventArgs(values []string) []any {
|
||||
args := make([]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
args = append(args, value)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func queryWalletOutboxRecords(ctx context.Context, tx *sql.Tx, query string, args ...any) ([]WalletOutboxRecord, error) {
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
@ -2434,6 +2743,100 @@ func checkedAdd(left int64, right int64) (int64, error) {
|
||||
return left + right, nil
|
||||
}
|
||||
|
||||
func debitGiftCommandFromBatchTarget(command ledger.BatchDebitGiftCommand, target ledger.DebitGiftTargetCommand) ledger.DebitGiftCommand {
|
||||
// 批量目标最终仍落成独立 gift_debit 交易;复用单目标 hash 可以保证账务语义变化时照样触发幂等冲突。
|
||||
return ledger.DebitGiftCommand{
|
||||
AppCode: command.AppCode,
|
||||
CommandID: target.CommandID,
|
||||
RoomID: command.RoomID,
|
||||
SenderUserID: command.SenderUserID,
|
||||
TargetUserID: target.TargetUserID,
|
||||
GiftID: command.GiftID,
|
||||
GiftCount: command.GiftCount,
|
||||
PriceVersion: command.PriceVersion,
|
||||
RegionID: command.RegionID,
|
||||
SenderRegionID: command.SenderRegionID,
|
||||
TargetIsHost: target.TargetIsHost,
|
||||
TargetHostRegionID: target.TargetHostRegionID,
|
||||
TargetAgencyOwnerUserID: target.TargetAgencyOwnerUserID,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) lockGiftTargetAccounts(ctx context.Context, tx *sql.Tx, targets []ledger.DebitGiftTargetCommand, nowMs int64) (map[int64]walletAccount, error) {
|
||||
ids := make([]int64, 0, len(targets))
|
||||
seen := make(map[int64]struct{}, len(targets))
|
||||
for _, target := range targets {
|
||||
if _, exists := seen[target.TargetUserID]; exists {
|
||||
continue
|
||||
}
|
||||
seen[target.TargetUserID] = struct{}{}
|
||||
ids = append(ids, target.TargetUserID)
|
||||
}
|
||||
sort.Slice(ids, func(left, right int) bool {
|
||||
return ids[left] < ids[right]
|
||||
})
|
||||
accounts := make(map[int64]walletAccount, len(ids))
|
||||
for _, targetUserID := range ids {
|
||||
// 固定按 user_id 升序锁目标账户,降低并发批量送礼互相等待时形成死锁的概率。
|
||||
account, err := r.lockAccount(ctx, tx, targetUserID, ledger.AssetGiftPoint, true, nowMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accounts[targetUserID] = account
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func aggregateGiftReceipts(targets []ledger.BatchGiftTargetReceipt) (ledger.Receipt, error) {
|
||||
aggregate := ledger.Receipt{}
|
||||
billingReceiptIDs := make([]string, 0, len(targets))
|
||||
transactionIDs := make([]string, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
receipt := target.Receipt
|
||||
if receipt.BillingReceiptID != "" {
|
||||
billingReceiptIDs = append(billingReceiptIDs, receipt.BillingReceiptID)
|
||||
}
|
||||
if receipt.TransactionID != "" {
|
||||
transactionIDs = append(transactionIDs, receipt.TransactionID)
|
||||
}
|
||||
var err error
|
||||
if aggregate.CoinSpent, err = checkedAdd(aggregate.CoinSpent, receipt.CoinSpent); err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
if aggregate.ChargeAmount, err = checkedAdd(aggregate.ChargeAmount, receipt.ChargeAmount); err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
if aggregate.GiftPointAdded, err = checkedAdd(aggregate.GiftPointAdded, receipt.GiftPointAdded); err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
if aggregate.HeatValue, err = checkedAdd(aggregate.HeatValue, receipt.HeatValue); err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
if aggregate.HostPeriodDiamondAdded, err = checkedAdd(aggregate.HostPeriodDiamondAdded, receipt.HostPeriodDiamondAdded); err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
if aggregate.ChargeAssetType == "" {
|
||||
aggregate.ChargeAssetType = receipt.ChargeAssetType
|
||||
} else if receipt.ChargeAssetType != "" && aggregate.ChargeAssetType != receipt.ChargeAssetType {
|
||||
aggregate.ChargeAssetType = giftWallChargeAssetMixed
|
||||
}
|
||||
if aggregate.GiftTypeCode == "" {
|
||||
aggregate.GiftTypeCode = receipt.GiftTypeCode
|
||||
}
|
||||
if aggregate.PriceVersion == "" {
|
||||
aggregate.PriceVersion = receipt.PriceVersion
|
||||
}
|
||||
if aggregate.HostPeriodCycleKey == "" {
|
||||
aggregate.HostPeriodCycleKey = receipt.HostPeriodCycleKey
|
||||
}
|
||||
// sender 账后余额随目标顺序逐笔递减;聚合响应返回最后一笔后的余额,表示整批完成后的余额。
|
||||
aggregate.BalanceAfter = receipt.BalanceAfter
|
||||
}
|
||||
aggregate.BillingReceiptID = strings.Join(billingReceiptIDs, ",")
|
||||
aggregate.TransactionID = strings.Join(transactionIDs, ",")
|
||||
return aggregate, nil
|
||||
}
|
||||
|
||||
func hostPeriodCycleKeyFromMS(ms int64) string {
|
||||
// 工资周期用 UTC 月份锚定,避免服务节点本地时区差异导致月底礼物落到不同周期。
|
||||
return time.UnixMilli(ms).UTC().Format("2006-01")
|
||||
|
||||
@ -814,6 +814,10 @@ func (r *Repository) ListUserResources(ctx context.Context, query resourcedomain
|
||||
if query.UserID <= 0 {
|
||||
return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||||
}
|
||||
nowMs := time.Now().UnixMilli()
|
||||
// equipment 表只记录“当前佩戴指针”,真正的有效期仍以 entitlement 为准。
|
||||
// 所以背包列表读取前先清理当前用户已失效的佩戴指针,避免过期道具继续显示 equipped=true。
|
||||
pruneResourceTypes := []string{}
|
||||
where := `WHERE e.app_code = ? AND e.user_id = ?`
|
||||
args := []any{appcode.FromContext(ctx), query.UserID}
|
||||
if strings.TrimSpace(query.ResourceType) != "" {
|
||||
@ -821,11 +825,18 @@ func (r *Repository) ListUserResources(ctx context.Context, query resourcedomain
|
||||
if !resourcedomain.ValidResourceType(resourceType) {
|
||||
return nil, xerr.New(xerr.InvalidArgument, "resource_type is invalid")
|
||||
}
|
||||
// 只有可佩戴资源才可能在 equipment 表中留下指针;礼物/金币等非佩戴资源不参与清理。
|
||||
if isEquipableResourceType(resourceType) {
|
||||
pruneResourceTypes = []string{resourceType}
|
||||
}
|
||||
where += ` AND r.resource_type = ?`
|
||||
args = append(args, resourceType)
|
||||
}
|
||||
if err := r.pruneExpiredUserResourceEquipment(ctx, []int64{query.UserID}, pruneResourceTypes, nowMs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if query.ActiveOnly {
|
||||
nowMs := time.Now().UnixMilli()
|
||||
// App 背包只需要当前可用权益;后台或排障读取可关闭 ActiveOnly 看历史/过期记录。
|
||||
where += ` AND e.status = 'active' AND e.effective_at_ms <= ? AND (e.expires_at_ms = 0 OR e.expires_at_ms > ?) AND e.remaining_quantity > 0`
|
||||
args = append(args, nowMs, nowMs)
|
||||
}
|
||||
@ -904,6 +915,147 @@ func (r *Repository) EquipUserResource(ctx context.Context, command resourcedoma
|
||||
return entitlement, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UnequipUserResource(ctx context.Context, command resourcedomain.UnequipUserResourceCommand) (resourcedomain.UnequipUserResourceResult, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return resourcedomain.UnequipUserResourceResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
ctx = contextWithCommandApp(ctx, command.AppCode)
|
||||
command.AppCode = appcode.FromContext(ctx)
|
||||
command.ResourceType = resourcedomain.NormalizeResourceType(command.ResourceType)
|
||||
if command.UserID <= 0 || command.ResourceType == "" {
|
||||
return resourcedomain.UnequipUserResourceResult{}, xerr.New(xerr.InvalidArgument, "user_id and resource_type are required")
|
||||
}
|
||||
if !resourcedomain.ValidResourceType(command.ResourceType) || !isEquipableResourceType(command.ResourceType) {
|
||||
return resourcedomain.UnequipUserResourceResult{}, xerr.New(xerr.InvalidArgument, "resource_type cannot be equipped")
|
||||
}
|
||||
|
||||
// 取消佩戴要和 outbox 事件在同一事务内提交:
|
||||
// UI 投影可以监听 UserResourceChanged,但不能在 equipment 删除失败时收到“已卸下”的假事件。
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return resourcedomain.UnequipUserResourceResult{}, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
nowMs := time.Now().UnixMilli()
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM user_resource_equipment
|
||||
WHERE app_code = ? AND user_id = ? AND resource_type = ?`,
|
||||
command.AppCode, command.UserID, command.ResourceType,
|
||||
)
|
||||
if err != nil {
|
||||
return resourcedomain.UnequipUserResourceResult{}, err
|
||||
}
|
||||
// RowsAffected=0 表示用户本来就没佩戴该类型;这个场景按幂等取消处理,不抛业务错误。
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return resourcedomain.UnequipUserResourceResult{}, err
|
||||
}
|
||||
if affected > 0 {
|
||||
// resource_id 对“取消某一类佩戴”没有单一值,payload 用 resource_type 表示变更维度。
|
||||
eventCommandID := fmt.Sprintf("unequip:%d:%s:%d", command.UserID, command.ResourceType, time.Now().UnixNano())
|
||||
if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
|
||||
resourceOutboxEvent("UserResourceChanged", eventCommandID, command.UserID, 0, map[string]any{
|
||||
"action": "unequip",
|
||||
"app_code": command.AppCode,
|
||||
"user_id": command.UserID,
|
||||
"resource_type": command.ResourceType,
|
||||
"updated_at_ms": nowMs,
|
||||
"event_command": eventCommandID,
|
||||
}, nowMs),
|
||||
}); err != nil {
|
||||
return resourcedomain.UnequipUserResourceResult{}, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return resourcedomain.UnequipUserResourceResult{}, err
|
||||
}
|
||||
return resourcedomain.UnequipUserResourceResult{
|
||||
ResourceType: command.ResourceType,
|
||||
Unequipped: affected > 0,
|
||||
UpdatedAtMS: nowMs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) BatchGetUserEquippedResources(ctx context.Context, query resourcedomain.BatchGetUserEquippedResourcesQuery) ([]resourcedomain.UserEquippedResources, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
ctx = contextWithCommandApp(ctx, query.AppCode)
|
||||
userIDs := compactPositiveInt64s(query.UserIDs)
|
||||
if len(userIDs) == 0 {
|
||||
return []resourcedomain.UserEquippedResources{}, nil
|
||||
}
|
||||
// 批量查询只允许可佩戴类型,避免把背包里不可展示为装扮的资源混进房间资料。
|
||||
resourceTypes, err := normalizeEquipableResourceTypes(query.ResourceTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nowMs := time.Now().UnixMilli()
|
||||
// 先清理再查,保证调用方不需要理解“equipment 只是指针”的存储细节。
|
||||
if err := r.pruneExpiredUserResourceEquipment(ctx, userIDs, resourceTypes, nowMs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args := append([]any{appcode.FromContext(ctx)}, int64AnyArgs(userIDs)...)
|
||||
args = append(args, nowMs, nowMs)
|
||||
typeFilter := ""
|
||||
if len(resourceTypes) > 0 {
|
||||
// resourceTypes 为空表示查询所有可佩戴类型;非空时只返回调用方关心的展示维度。
|
||||
typeFilter = ` AND eq.resource_type IN (` + placeholders(len(resourceTypes)) + `)`
|
||||
args = append(args, stringAnyArgs(resourceTypes)...)
|
||||
}
|
||||
// 从 equipment 反查 entitlement/resource,而不是从 entitlement 扫描 equipped 标记:
|
||||
// 这样每个 user_id + resource_type 只会返回当前佩戴的那一个有效权益。
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT e.app_code, e.entitlement_id, e.user_id, e.resource_id, e.status,
|
||||
e.quantity, e.remaining_quantity, e.effective_at_ms, e.expires_at_ms,
|
||||
e.source_grant_id, e.created_at_ms, e.updated_at_ms,
|
||||
TRUE AS equipped,
|
||||
`+resourceColumnsWithAlias("r")+`
|
||||
FROM user_resource_equipment eq
|
||||
JOIN user_resource_entitlements e ON e.app_code = eq.app_code
|
||||
AND e.user_id = eq.user_id
|
||||
AND e.resource_id = eq.resource_id
|
||||
AND e.entitlement_id = eq.entitlement_id
|
||||
JOIN resources r ON r.app_code = e.app_code AND r.resource_id = e.resource_id
|
||||
WHERE eq.app_code = ? AND eq.user_id IN (`+placeholders(len(userIDs))+`)
|
||||
AND e.status = 'active'
|
||||
AND e.effective_at_ms <= ? AND (e.expires_at_ms = 0 OR e.expires_at_ms > ?)
|
||||
AND e.remaining_quantity > 0
|
||||
AND r.status = 'active'`+typeFilter+`
|
||||
ORDER BY eq.user_id ASC, eq.updated_at_ms DESC`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
grouped := make(map[int64][]resourcedomain.UserResourceEntitlement, len(userIDs))
|
||||
for rows.Next() {
|
||||
item, err := scanUserResourceEntitlement(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Equipped = true
|
||||
grouped[item.UserID] = append(grouped[item.UserID], item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回顺序按请求 user_ids 保持稳定,调用方可以直接和入参用户列表对齐。
|
||||
result := make([]resourcedomain.UserEquippedResources, 0, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
result = append(result, resourcedomain.UserEquippedResources{
|
||||
UserID: userID,
|
||||
Resources: grouped[userID],
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
@ -2742,6 +2894,14 @@ func int64AnyArgs(values []int64) []any {
|
||||
return args
|
||||
}
|
||||
|
||||
func stringAnyArgs(values []string) []any {
|
||||
args := make([]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
args = append(args, value)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func compactPositiveInt64s(values []int64) []int64 {
|
||||
seen := make(map[int64]struct{}, len(values))
|
||||
out := make([]int64, 0, len(values))
|
||||
@ -2758,6 +2918,72 @@ func compactPositiveInt64s(values []int64) []int64 {
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeEquipableResourceTypes(values []string) ([]string, error) {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
out := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
resourceType := resourcedomain.NormalizeResourceType(value)
|
||||
if resourceType == "" {
|
||||
// 空类型不限制查询范围,保持 batch API 可以表达“查全部可佩戴资源”。
|
||||
continue
|
||||
}
|
||||
if !resourcedomain.ValidResourceType(resourceType) || !isEquipableResourceType(resourceType) {
|
||||
return nil, xerr.New(xerr.InvalidArgument, "resource_type cannot be equipped")
|
||||
}
|
||||
if _, exists := seen[resourceType]; exists {
|
||||
// 去重后拼 IN (...),避免无意义扩大 SQL 参数和破坏调用方顺序稳定性。
|
||||
continue
|
||||
}
|
||||
seen[resourceType] = struct{}{}
|
||||
out = append(out, resourceType)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Repository) pruneExpiredUserResourceEquipment(ctx context.Context, userIDs []int64, resourceTypes []string, nowMs int64) error {
|
||||
userIDs = compactPositiveInt64s(userIDs)
|
||||
if r == nil || r.db == nil || len(userIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
// 清理函数和查询函数复用同一套类型校验,避免查询拒绝的类型被清理 SQL 接受。
|
||||
resourceTypes, err := normalizeEquipableResourceTypes(resourceTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args := append([]any{appcode.FromContext(ctx)}, int64AnyArgs(userIDs)...)
|
||||
args = append(args, nowMs, nowMs)
|
||||
typeFilter := ""
|
||||
if len(resourceTypes) > 0 {
|
||||
typeFilter = ` AND eq.resource_type IN (` + placeholders(len(resourceTypes)) + `)`
|
||||
args = append(args, stringAnyArgs(resourceTypes)...)
|
||||
}
|
||||
// 删除条件覆盖三类 stale 指针:
|
||||
// 1. entitlement/resource 被删或不可用;
|
||||
// 2. entitlement 未生效、过期、剩余次数耗尽;
|
||||
// 3. resource 被下架。查询端随后只会看到仍可展示的佩戴。
|
||||
_, err = r.db.ExecContext(ctx, `
|
||||
DELETE eq
|
||||
FROM user_resource_equipment eq
|
||||
LEFT JOIN user_resource_entitlements e ON e.app_code = eq.app_code
|
||||
AND e.user_id = eq.user_id
|
||||
AND e.resource_id = eq.resource_id
|
||||
AND e.entitlement_id = eq.entitlement_id
|
||||
LEFT JOIN resources r ON r.app_code = eq.app_code AND r.resource_id = eq.resource_id
|
||||
WHERE eq.app_code = ? AND eq.user_id IN (`+placeholders(len(userIDs))+`)
|
||||
AND (
|
||||
e.entitlement_id IS NULL
|
||||
OR e.status <> 'active'
|
||||
OR e.effective_at_ms > ?
|
||||
OR (e.expires_at_ms <> 0 AND e.expires_at_ms <= ?)
|
||||
OR e.remaining_quantity <= 0
|
||||
OR r.resource_id IS NULL
|
||||
OR r.status <> 'active'
|
||||
)`+typeFilter,
|
||||
args...,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func mapResourceWriteError(err error) error {
|
||||
if isMySQLDuplicateError(err) {
|
||||
return xerr.New(xerr.Conflict, "resource_code already exists")
|
||||
|
||||
@ -61,6 +61,20 @@ func (r *Repository) ActiveResourceEntitlement(userID int64, resourceID int64) (
|
||||
return expiresAtMS, quantity, remainingQuantity
|
||||
}
|
||||
|
||||
// ExpireEntitlement moves a granted entitlement into the past for expiry-path assertions.
|
||||
func (r *Repository) ExpireEntitlement(entitlementID string, expiresAtMS int64) {
|
||||
r.t.Helper()
|
||||
|
||||
if _, err := r.schema.DB.ExecContext(context.Background(), `
|
||||
UPDATE user_resource_entitlements
|
||||
SET expires_at_ms = ?, updated_at_ms = ?
|
||||
WHERE entitlement_id = ?`,
|
||||
expiresAtMS, time.Now().UnixMilli(), entitlementID,
|
||||
); err != nil {
|
||||
r.t.Fatalf("expire entitlement failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// HostSalarySettlementIDs returns the persisted settlement IDs for one host/cycle in creation order.
|
||||
func (r *Repository) HostSalarySettlementIDs(userID int64, cycleKey string) []string {
|
||||
r.t.Helper()
|
||||
|
||||
@ -269,6 +269,45 @@ func (s *Server) EquipUserResource(ctx context.Context, req *walletv1.EquipUserR
|
||||
return &walletv1.EquipUserResourceResponse{Resource: userResourceToProto(item)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) UnequipUserResource(ctx context.Context, req *walletv1.UnequipUserResourceRequest) (*walletv1.UnequipUserResourceResponse, error) {
|
||||
result, err := s.svc.UnequipUserResource(ctx, resourcedomain.UnequipUserResourceCommand{
|
||||
AppCode: req.GetAppCode(),
|
||||
UserID: req.GetUserId(),
|
||||
ResourceType: req.GetResourceType(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &walletv1.UnequipUserResourceResponse{
|
||||
ResourceType: result.ResourceType,
|
||||
Unequipped: result.Unequipped,
|
||||
UpdatedAtMs: result.UpdatedAtMS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) BatchGetUserEquippedResources(ctx context.Context, req *walletv1.BatchGetUserEquippedResourcesRequest) (*walletv1.BatchGetUserEquippedResourcesResponse, error) {
|
||||
items, err := s.svc.BatchGetUserEquippedResources(ctx, resourcedomain.BatchGetUserEquippedResourcesQuery{
|
||||
AppCode: req.GetAppCode(),
|
||||
UserIDs: req.GetUserIds(),
|
||||
ResourceTypes: req.GetResourceTypes(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &walletv1.BatchGetUserEquippedResourcesResponse{Users: make([]*walletv1.UserEquippedResources, 0, len(items))}
|
||||
for _, item := range items {
|
||||
user := &walletv1.UserEquippedResources{
|
||||
UserId: item.UserID,
|
||||
Resources: make([]*walletv1.UserResourceEntitlement, 0, len(item.Resources)),
|
||||
}
|
||||
for _, resource := range item.Resources {
|
||||
user.Resources = append(user.Resources, userResourceToProto(resource))
|
||||
}
|
||||
resp.Users = append(resp.Users, user)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListResourceGrants(ctx context.Context, req *walletv1.ListResourceGrantsRequest) (*walletv1.ListResourceGrantsResponse, error) {
|
||||
items, total, err := s.svc.ListResourceGrants(ctx, resourcedomain.ListResourceGrantsQuery{
|
||||
AppCode: req.GetAppCode(),
|
||||
|
||||
@ -46,6 +46,52 @@ func (s *Server) DebitGift(ctx context.Context, req *walletv1.DebitGiftRequest)
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
|
||||
return debitGiftResponseFromReceipt(receipt), nil
|
||||
}
|
||||
|
||||
// BatchDebitGift 只负责批量目标 protobuf 转换;同事务扣费和幂等由 wallet service/repository 保证。
|
||||
func (s *Server) BatchDebitGift(ctx context.Context, req *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||
targets := make([]ledger.DebitGiftTargetCommand, 0, len(req.GetTargets()))
|
||||
for _, target := range req.GetTargets() {
|
||||
targets = append(targets, ledger.DebitGiftTargetCommand{
|
||||
CommandID: target.GetCommandId(),
|
||||
TargetUserID: target.GetTargetUserId(),
|
||||
TargetIsHost: target.GetTargetIsHost(),
|
||||
TargetHostRegionID: target.GetTargetHostRegionId(),
|
||||
TargetAgencyOwnerUserID: target.GetTargetAgencyOwnerUserId(),
|
||||
})
|
||||
}
|
||||
receipt, err := s.svc.BatchDebitGift(ctx, ledger.BatchDebitGiftCommand{
|
||||
AppCode: req.GetAppCode(),
|
||||
CommandID: req.GetCommandId(),
|
||||
RoomID: req.GetRoomId(),
|
||||
SenderUserID: req.GetSenderUserId(),
|
||||
GiftID: req.GetGiftId(),
|
||||
GiftCount: req.GetGiftCount(),
|
||||
PriceVersion: req.GetPriceVersion(),
|
||||
RegionID: req.GetRegionId(),
|
||||
SenderRegionID: req.GetSenderRegionId(),
|
||||
Targets: targets,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
response := &walletv1.BatchDebitGiftResponse{
|
||||
Aggregate: debitGiftResponseFromReceipt(receipt.Aggregate),
|
||||
Receipts: make([]*walletv1.BatchDebitGiftReceipt, 0, len(receipt.Targets)),
|
||||
}
|
||||
for _, target := range receipt.Targets {
|
||||
response.Receipts = append(response.Receipts, &walletv1.BatchDebitGiftReceipt{
|
||||
TargetUserId: target.TargetUserID,
|
||||
CommandId: target.CommandID,
|
||||
Billing: debitGiftResponseFromReceipt(target.Receipt),
|
||||
})
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func debitGiftResponseFromReceipt(receipt ledger.Receipt) *walletv1.DebitGiftResponse {
|
||||
return &walletv1.DebitGiftResponse{
|
||||
BillingReceiptId: receipt.BillingReceiptID,
|
||||
BalanceAfter: receipt.BalanceAfter,
|
||||
@ -59,7 +105,7 @@ func (s *Server) DebitGift(ctx context.Context, req *walletv1.DebitGiftRequest)
|
||||
PriceVersion: receipt.PriceVersion,
|
||||
HostPeriodDiamondAdded: receipt.HostPeriodDiamondAdded,
|
||||
HostPeriodCycleKey: receipt.HostPeriodCycleKey,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetBalances 返回用户多资产余额投影。
|
||||
|
||||
158
tools/grant-registration-level-badges/main.go
Normal file
158
tools/grant-registration-level-badges/main.go
Normal file
@ -0,0 +1,158 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
)
|
||||
|
||||
const registrationLevelBadgeReason = "growth_level_registration_badge"
|
||||
|
||||
func main() {
|
||||
var appCode string
|
||||
var activityDSN string
|
||||
var userDSN string
|
||||
var walletAddr string
|
||||
var dryRun bool
|
||||
var onlyUserID int64
|
||||
flag.StringVar(&appCode, "app_code", "lalu", "app_code to backfill")
|
||||
flag.StringVar(&activityDSN, "activity_dsn", "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=UTC", "activity mysql dsn")
|
||||
flag.StringVar(&userDSN, "user_dsn", "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=UTC", "user mysql dsn")
|
||||
flag.StringVar(&walletAddr, "wallet_addr", "127.0.0.1:13004", "wallet-service grpc address")
|
||||
flag.BoolVar(&dryRun, "dry_run", true, "print planned grants without calling wallet-service")
|
||||
flag.Int64Var(&onlyUserID, "user_id", 0, "optional single user_id")
|
||||
flag.Parse()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
activityDB, err := sql.Open("mysql", activityDSN)
|
||||
if err != nil {
|
||||
log.Fatalf("open activity db: %v", err)
|
||||
}
|
||||
defer activityDB.Close()
|
||||
userDB, err := sql.Open("mysql", userDSN)
|
||||
if err != nil {
|
||||
log.Fatalf("open user db: %v", err)
|
||||
}
|
||||
defer userDB.Close()
|
||||
|
||||
badges, err := levelOneBadgeResources(ctx, activityDB, appCode)
|
||||
if err != nil {
|
||||
log.Fatalf("query level badges: %v", err)
|
||||
}
|
||||
users, err := targetUsers(ctx, userDB, appCode, onlyUserID)
|
||||
if err != nil {
|
||||
log.Fatalf("query users: %v", err)
|
||||
}
|
||||
|
||||
tracks := []string{"wealth", "game", "charm"}
|
||||
for _, track := range tracks {
|
||||
if badges[track] <= 0 {
|
||||
log.Printf("skip track=%s: level 1 long badge is not configured", track)
|
||||
}
|
||||
}
|
||||
if dryRun {
|
||||
log.Printf("dry_run=true app_code=%s users=%d badges=%v", appCode, len(users), badges)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(walletAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
log.Fatalf("dial wallet-service: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
wallet := walletv1.NewWalletServiceClient(conn)
|
||||
|
||||
total := 0
|
||||
for _, userID := range users {
|
||||
for _, track := range tracks {
|
||||
resourceID := badges[track]
|
||||
if resourceID <= 0 {
|
||||
continue
|
||||
}
|
||||
// wallet-service 用 command_id 做完整请求幂等;同一个用户同一轨道重复执行只会返回首次发放结果。
|
||||
commandID := fmt.Sprintf("registration_level_badges:%s:%d:%s", appCode, userID, track)
|
||||
_, err := wallet.GrantResource(ctx, &walletv1.GrantResourceRequest{
|
||||
CommandId: commandID,
|
||||
AppCode: appCode,
|
||||
TargetUserId: userID,
|
||||
ResourceId: resourceID,
|
||||
Quantity: 1,
|
||||
DurationMs: 0,
|
||||
Reason: registrationLevelBadgeReason,
|
||||
OperatorUserId: userID,
|
||||
GrantSource: "growth_level",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("grant failed user_id=%d track=%s resource_id=%d: %v", userID, track, resourceID, err)
|
||||
}
|
||||
total++
|
||||
}
|
||||
}
|
||||
log.Printf("done app_code=%s users=%d grant_calls=%d", appCode, len(users), total)
|
||||
}
|
||||
|
||||
func levelOneBadgeResources(ctx context.Context, db *sql.DB, appCode string) (map[string]int64, error) {
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT track,
|
||||
COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(display_config_json, '$.long_badge_resource_id')) AS SIGNED), 0) AS resource_id
|
||||
FROM growth_level_rules
|
||||
WHERE app_code = ? AND status = 'active' AND level = 1
|
||||
ORDER BY track ASC`, appCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
result := map[string]int64{}
|
||||
for rows.Next() {
|
||||
var track string
|
||||
var resourceID int64
|
||||
if err := rows.Scan(&track, &resourceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
track = strings.ToLower(strings.TrimSpace(track))
|
||||
if resourceID > 0 {
|
||||
result[track] = resourceID
|
||||
}
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func targetUsers(ctx context.Context, db *sql.DB, appCode string, onlyUserID int64) ([]int64, error) {
|
||||
if onlyUserID > 0 {
|
||||
return []int64{onlyUserID}, nil
|
||||
}
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT user_id
|
||||
FROM users
|
||||
WHERE app_code = ? AND status = 'active'
|
||||
ORDER BY user_id ASC`, appCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
users := make([]int64, 0)
|
||||
for rows.Next() {
|
||||
var userID int64
|
||||
if err := rows.Scan(&userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users = append(users, userID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(users, func(i, j int) bool { return users[i] < users[j] })
|
||||
return users, nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user