主播结算相关和房间锁房
This commit is contained in:
parent
8b2181dee7
commit
82dfb9238f
@ -875,11 +875,13 @@ func (x *RoomChatEnabledChanged) GetEnabled() bool {
|
|||||||
|
|
||||||
// RoomPasswordChanged 表达房间入房密码状态变更,不携带明文或哈希。
|
// RoomPasswordChanged 表达房间入房密码状态变更,不携带明文或哈希。
|
||||||
type RoomPasswordChanged struct {
|
type RoomPasswordChanged struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
|
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
|
||||||
Locked bool `protobuf:"varint,2,opt,name=locked,proto3" json:"locked,omitempty"`
|
Locked bool `protobuf:"varint,2,opt,name=locked,proto3" json:"locked,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// visible_region_id 是房间当前可见区域,activity-service 用它把锁房变化投到区域播报群。
|
||||||
sizeCache protoimpl.SizeCache
|
VisibleRegionId int64 `protobuf:"varint,3,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomPasswordChanged) Reset() {
|
func (x *RoomPasswordChanged) Reset() {
|
||||||
@ -926,6 +928,13 @@ func (x *RoomPasswordChanged) GetLocked() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *RoomPasswordChanged) GetVisibleRegionId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.VisibleRegionId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// RoomAdminChanged 表达房间管理员集合变更。
|
// RoomAdminChanged 表达房间管理员集合变更。
|
||||||
type RoomAdminChanged struct {
|
type RoomAdminChanged struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
@ -2001,10 +2010,11 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
|
|||||||
"\x06locked\x18\x03 \x01(\bR\x06locked\"V\n" +
|
"\x06locked\x18\x03 \x01(\bR\x06locked\"V\n" +
|
||||||
"\x16RoomChatEnabledChanged\x12\"\n" +
|
"\x16RoomChatEnabledChanged\x12\"\n" +
|
||||||
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12\x18\n" +
|
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12\x18\n" +
|
||||||
"\aenabled\x18\x02 \x01(\bR\aenabled\"Q\n" +
|
"\aenabled\x18\x02 \x01(\bR\aenabled\"}\n" +
|
||||||
"\x13RoomPasswordChanged\x12\"\n" +
|
"\x13RoomPasswordChanged\x12\"\n" +
|
||||||
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12\x16\n" +
|
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12\x16\n" +
|
||||||
"\x06locked\x18\x02 \x01(\bR\x06locked\"v\n" +
|
"\x06locked\x18\x02 \x01(\bR\x06locked\x12*\n" +
|
||||||
|
"\x11visible_region_id\x18\x03 \x01(\x03R\x0fvisibleRegionId\"v\n" +
|
||||||
"\x10RoomAdminChanged\x12\"\n" +
|
"\x10RoomAdminChanged\x12\"\n" +
|
||||||
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12$\n" +
|
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12$\n" +
|
||||||
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x18\n" +
|
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x18\n" +
|
||||||
|
|||||||
@ -112,6 +112,8 @@ message RoomChatEnabledChanged {
|
|||||||
message RoomPasswordChanged {
|
message RoomPasswordChanged {
|
||||||
int64 actor_user_id = 1;
|
int64 actor_user_id = 1;
|
||||||
bool locked = 2;
|
bool locked = 2;
|
||||||
|
// visible_region_id 是房间当前可见区域,activity-service 用它把锁房变化投到区域播报群。
|
||||||
|
int64 visible_region_id = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomAdminChanged 表达房间管理员集合变更。
|
// RoomAdminChanged 表达房间管理员集合变更。
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -65,6 +65,8 @@ message SeatState {
|
|||||||
bool mic_muted = 9;
|
bool mic_muted = 9;
|
||||||
// seat_status 是服务端派生的展示状态:empty/locked/occupied/publishing/muted。
|
// seat_status 是服务端派生的展示状态:empty/locked/occupied/publishing/muted。
|
||||||
string seat_status = 10;
|
string seat_status = 10;
|
||||||
|
// mic_heartbeat_at_ms 是当前 mic_session 最近一次服务端接受麦上心跳的 UTC epoch ms。
|
||||||
|
int64 mic_heartbeat_at_ms = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RankItem 表达房间内本地礼物榜项目。
|
// RankItem 表达房间内本地礼物榜项目。
|
||||||
@ -632,6 +634,23 @@ message ConfirmMicPublishingResponse {
|
|||||||
RoomSnapshot room = 3;
|
RoomSnapshot room = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MicHeartbeatRequest 刷新当前 publishing 麦位会话的服务端麦上心跳。
|
||||||
|
message MicHeartbeatRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
// target_user_id 为空时默认刷新 actor_user_id 自己的麦上会话。
|
||||||
|
int64 target_user_id = 2;
|
||||||
|
// mic_session_id 必须匹配当前麦位上的会话,避免旧心跳续住新会话。
|
||||||
|
string mic_session_id = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MicHeartbeatResponse 返回刷新后的麦位和房间快照。
|
||||||
|
message MicHeartbeatResponse {
|
||||||
|
CommandResult result = 1;
|
||||||
|
int32 seat_no = 2;
|
||||||
|
RoomSnapshot room = 3;
|
||||||
|
int64 mic_heartbeat_at_ms = 4;
|
||||||
|
}
|
||||||
|
|
||||||
// SetMicMuteRequest 修改麦位上的服务端可见静音态。
|
// SetMicMuteRequest 修改麦位上的服务端可见静音态。
|
||||||
message SetMicMuteRequest {
|
message SetMicMuteRequest {
|
||||||
RequestMeta meta = 1;
|
RequestMeta meta = 1;
|
||||||
@ -1089,6 +1108,7 @@ service RoomCommandService {
|
|||||||
rpc MicDown(MicDownRequest) returns (MicDownResponse);
|
rpc MicDown(MicDownRequest) returns (MicDownResponse);
|
||||||
rpc ChangeMicSeat(ChangeMicSeatRequest) returns (ChangeMicSeatResponse);
|
rpc ChangeMicSeat(ChangeMicSeatRequest) returns (ChangeMicSeatResponse);
|
||||||
rpc ConfirmMicPublishing(ConfirmMicPublishingRequest) returns (ConfirmMicPublishingResponse);
|
rpc ConfirmMicPublishing(ConfirmMicPublishingRequest) returns (ConfirmMicPublishingResponse);
|
||||||
|
rpc MicHeartbeat(MicHeartbeatRequest) returns (MicHeartbeatResponse);
|
||||||
rpc SetMicMute(SetMicMuteRequest) returns (SetMicMuteResponse);
|
rpc SetMicMute(SetMicMuteRequest) returns (SetMicMuteResponse);
|
||||||
rpc ApplyRTCEvent(ApplyRTCEventRequest) returns (ApplyRTCEventResponse);
|
rpc ApplyRTCEvent(ApplyRTCEventRequest) returns (ApplyRTCEventResponse);
|
||||||
rpc SetMicSeatLock(SetMicSeatLockRequest) returns (SetMicSeatLockResponse);
|
rpc SetMicSeatLock(SetMicSeatLockRequest) returns (SetMicSeatLockResponse);
|
||||||
|
|||||||
@ -37,6 +37,7 @@ const (
|
|||||||
RoomCommandService_MicDown_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicDown"
|
RoomCommandService_MicDown_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicDown"
|
||||||
RoomCommandService_ChangeMicSeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/ChangeMicSeat"
|
RoomCommandService_ChangeMicSeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/ChangeMicSeat"
|
||||||
RoomCommandService_ConfirmMicPublishing_FullMethodName = "/hyapp.room.v1.RoomCommandService/ConfirmMicPublishing"
|
RoomCommandService_ConfirmMicPublishing_FullMethodName = "/hyapp.room.v1.RoomCommandService/ConfirmMicPublishing"
|
||||||
|
RoomCommandService_MicHeartbeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicHeartbeat"
|
||||||
RoomCommandService_SetMicMute_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicMute"
|
RoomCommandService_SetMicMute_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicMute"
|
||||||
RoomCommandService_ApplyRTCEvent_FullMethodName = "/hyapp.room.v1.RoomCommandService/ApplyRTCEvent"
|
RoomCommandService_ApplyRTCEvent_FullMethodName = "/hyapp.room.v1.RoomCommandService/ApplyRTCEvent"
|
||||||
RoomCommandService_SetMicSeatLock_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicSeatLock"
|
RoomCommandService_SetMicSeatLock_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicSeatLock"
|
||||||
@ -76,6 +77,7 @@ type RoomCommandServiceClient interface {
|
|||||||
MicDown(ctx context.Context, in *MicDownRequest, opts ...grpc.CallOption) (*MicDownResponse, error)
|
MicDown(ctx context.Context, in *MicDownRequest, opts ...grpc.CallOption) (*MicDownResponse, error)
|
||||||
ChangeMicSeat(ctx context.Context, in *ChangeMicSeatRequest, opts ...grpc.CallOption) (*ChangeMicSeatResponse, error)
|
ChangeMicSeat(ctx context.Context, in *ChangeMicSeatRequest, opts ...grpc.CallOption) (*ChangeMicSeatResponse, error)
|
||||||
ConfirmMicPublishing(ctx context.Context, in *ConfirmMicPublishingRequest, opts ...grpc.CallOption) (*ConfirmMicPublishingResponse, error)
|
ConfirmMicPublishing(ctx context.Context, in *ConfirmMicPublishingRequest, opts ...grpc.CallOption) (*ConfirmMicPublishingResponse, error)
|
||||||
|
MicHeartbeat(ctx context.Context, in *MicHeartbeatRequest, opts ...grpc.CallOption) (*MicHeartbeatResponse, error)
|
||||||
SetMicMute(ctx context.Context, in *SetMicMuteRequest, opts ...grpc.CallOption) (*SetMicMuteResponse, error)
|
SetMicMute(ctx context.Context, in *SetMicMuteRequest, opts ...grpc.CallOption) (*SetMicMuteResponse, error)
|
||||||
ApplyRTCEvent(ctx context.Context, in *ApplyRTCEventRequest, opts ...grpc.CallOption) (*ApplyRTCEventResponse, error)
|
ApplyRTCEvent(ctx context.Context, in *ApplyRTCEventRequest, opts ...grpc.CallOption) (*ApplyRTCEventResponse, error)
|
||||||
SetMicSeatLock(ctx context.Context, in *SetMicSeatLockRequest, opts ...grpc.CallOption) (*SetMicSeatLockResponse, error)
|
SetMicSeatLock(ctx context.Context, in *SetMicSeatLockRequest, opts ...grpc.CallOption) (*SetMicSeatLockResponse, error)
|
||||||
@ -279,6 +281,16 @@ func (c *roomCommandServiceClient) ConfirmMicPublishing(ctx context.Context, in
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *roomCommandServiceClient) MicHeartbeat(ctx context.Context, in *MicHeartbeatRequest, opts ...grpc.CallOption) (*MicHeartbeatResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(MicHeartbeatResponse)
|
||||||
|
err := c.cc.Invoke(ctx, RoomCommandService_MicHeartbeat_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *roomCommandServiceClient) SetMicMute(ctx context.Context, in *SetMicMuteRequest, opts ...grpc.CallOption) (*SetMicMuteResponse, error) {
|
func (c *roomCommandServiceClient) SetMicMute(ctx context.Context, in *SetMicMuteRequest, opts ...grpc.CallOption) (*SetMicMuteResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(SetMicMuteResponse)
|
out := new(SetMicMuteResponse)
|
||||||
@ -433,6 +445,7 @@ type RoomCommandServiceServer interface {
|
|||||||
MicDown(context.Context, *MicDownRequest) (*MicDownResponse, error)
|
MicDown(context.Context, *MicDownRequest) (*MicDownResponse, error)
|
||||||
ChangeMicSeat(context.Context, *ChangeMicSeatRequest) (*ChangeMicSeatResponse, error)
|
ChangeMicSeat(context.Context, *ChangeMicSeatRequest) (*ChangeMicSeatResponse, error)
|
||||||
ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error)
|
ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error)
|
||||||
|
MicHeartbeat(context.Context, *MicHeartbeatRequest) (*MicHeartbeatResponse, error)
|
||||||
SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error)
|
SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error)
|
||||||
ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error)
|
ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error)
|
||||||
SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error)
|
SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error)
|
||||||
@ -510,6 +523,9 @@ func (UnimplementedRoomCommandServiceServer) ChangeMicSeat(context.Context, *Cha
|
|||||||
func (UnimplementedRoomCommandServiceServer) ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error) {
|
func (UnimplementedRoomCommandServiceServer) ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method ConfirmMicPublishing not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ConfirmMicPublishing not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedRoomCommandServiceServer) MicHeartbeat(context.Context, *MicHeartbeatRequest) (*MicHeartbeatResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method MicHeartbeat not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedRoomCommandServiceServer) SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error) {
|
func (UnimplementedRoomCommandServiceServer) SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method SetMicMute not implemented")
|
return nil, status.Error(codes.Unimplemented, "method SetMicMute not implemented")
|
||||||
}
|
}
|
||||||
@ -894,6 +910,24 @@ func _RoomCommandService_ConfirmMicPublishing_Handler(srv interface{}, ctx conte
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _RoomCommandService_MicHeartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(MicHeartbeatRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(RoomCommandServiceServer).MicHeartbeat(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: RoomCommandService_MicHeartbeat_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(RoomCommandServiceServer).MicHeartbeat(ctx, req.(*MicHeartbeatRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _RoomCommandService_SetMicMute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _RoomCommandService_SetMicMute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(SetMicMuteRequest)
|
in := new(SetMicMuteRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -1207,6 +1241,10 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ConfirmMicPublishing",
|
MethodName: "ConfirmMicPublishing",
|
||||||
Handler: _RoomCommandService_ConfirmMicPublishing_Handler,
|
Handler: _RoomCommandService_ConfirmMicPublishing_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "MicHeartbeat",
|
||||||
|
Handler: _RoomCommandService_MicHeartbeat_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "SetMicMute",
|
MethodName: "SetMicMute",
|
||||||
Handler: _RoomCommandService_SetMicMute_Handler,
|
Handler: _RoomCommandService_SetMicMute_Handler,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -104,6 +104,66 @@ message GetBalancesResponse {
|
|||||||
repeated AssetBalance balances = 1;
|
repeated AssetBalance balances = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message HostSalaryPolicyLevel {
|
||||||
|
int32 level_no = 1;
|
||||||
|
int64 required_diamonds = 2;
|
||||||
|
int64 host_salary_usd_minor = 3;
|
||||||
|
int64 host_coin_reward = 4;
|
||||||
|
int64 agency_salary_usd_minor = 5;
|
||||||
|
string status = 6;
|
||||||
|
int32 sort_order = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HostSalaryPolicy {
|
||||||
|
uint64 policy_id = 1;
|
||||||
|
string name = 2;
|
||||||
|
int64 region_id = 3;
|
||||||
|
string status = 4;
|
||||||
|
string settlement_mode = 5;
|
||||||
|
string settlement_trigger_mode = 6;
|
||||||
|
string gift_coin_to_diamond_ratio = 7;
|
||||||
|
string residual_diamond_to_usd_rate = 8;
|
||||||
|
int64 effective_from_ms = 9;
|
||||||
|
int64 effective_to_ms = 10;
|
||||||
|
repeated HostSalaryPolicyLevel levels = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetActiveHostSalaryPolicyRequest {
|
||||||
|
string request_id = 1;
|
||||||
|
string app_code = 2;
|
||||||
|
int64 region_id = 3;
|
||||||
|
string settlement_mode = 4;
|
||||||
|
string settlement_trigger_mode = 5;
|
||||||
|
int64 now_ms = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetActiveHostSalaryPolicyResponse {
|
||||||
|
bool found = 1;
|
||||||
|
HostSalaryPolicy policy = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HostSalaryProgress {
|
||||||
|
int64 host_user_id = 1;
|
||||||
|
string cycle_key = 2;
|
||||||
|
int64 region_id = 3;
|
||||||
|
int64 agency_owner_user_id = 4;
|
||||||
|
int64 total_diamonds = 5;
|
||||||
|
int64 gift_diamond_total = 6;
|
||||||
|
int64 updated_at_ms = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetHostSalaryProgressRequest {
|
||||||
|
string request_id = 1;
|
||||||
|
string app_code = 2;
|
||||||
|
int64 host_user_id = 3;
|
||||||
|
string cycle_key = 4;
|
||||||
|
int64 now_ms = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetHostSalaryProgressResponse {
|
||||||
|
HostSalaryProgress progress = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// AdminCreditAssetRequest 是后台手动入账/调账的最小审计命令。
|
// AdminCreditAssetRequest 是后台手动入账/调账的最小审计命令。
|
||||||
message AdminCreditAssetRequest {
|
message AdminCreditAssetRequest {
|
||||||
string command_id = 1;
|
string command_id = 1;
|
||||||
@ -1437,6 +1497,8 @@ service WalletService {
|
|||||||
rpc DebitGift(DebitGiftRequest) returns (DebitGiftResponse);
|
rpc DebitGift(DebitGiftRequest) returns (DebitGiftResponse);
|
||||||
rpc BatchDebitGift(BatchDebitGiftRequest) returns (BatchDebitGiftResponse);
|
rpc BatchDebitGift(BatchDebitGiftRequest) returns (BatchDebitGiftResponse);
|
||||||
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
|
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
|
||||||
|
rpc GetActiveHostSalaryPolicy(GetActiveHostSalaryPolicyRequest) returns (GetActiveHostSalaryPolicyResponse);
|
||||||
|
rpc GetHostSalaryProgress(GetHostSalaryProgressRequest) returns (GetHostSalaryProgressResponse);
|
||||||
rpc AdminCreditAsset(AdminCreditAssetRequest) returns (AdminCreditAssetResponse);
|
rpc AdminCreditAsset(AdminCreditAssetRequest) returns (AdminCreditAssetResponse);
|
||||||
rpc AdminCreditCoinSellerStock(AdminCreditCoinSellerStockRequest) returns (AdminCreditCoinSellerStockResponse);
|
rpc AdminCreditCoinSellerStock(AdminCreditCoinSellerStockRequest) returns (AdminCreditCoinSellerStockResponse);
|
||||||
rpc TransferCoinFromSeller(TransferCoinFromSellerRequest) returns (TransferCoinFromSellerResponse);
|
rpc TransferCoinFromSeller(TransferCoinFromSellerRequest) returns (TransferCoinFromSellerResponse);
|
||||||
|
|||||||
@ -204,6 +204,8 @@ const (
|
|||||||
WalletService_DebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitGift"
|
WalletService_DebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitGift"
|
||||||
WalletService_BatchDebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchDebitGift"
|
WalletService_BatchDebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchDebitGift"
|
||||||
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
|
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
|
||||||
|
WalletService_GetActiveHostSalaryPolicy_FullMethodName = "/hyapp.wallet.v1.WalletService/GetActiveHostSalaryPolicy"
|
||||||
|
WalletService_GetHostSalaryProgress_FullMethodName = "/hyapp.wallet.v1.WalletService/GetHostSalaryProgress"
|
||||||
WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset"
|
WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset"
|
||||||
WalletService_AdminCreditCoinSellerStock_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditCoinSellerStock"
|
WalletService_AdminCreditCoinSellerStock_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditCoinSellerStock"
|
||||||
WalletService_TransferCoinFromSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferCoinFromSeller"
|
WalletService_TransferCoinFromSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferCoinFromSeller"
|
||||||
@ -274,6 +276,8 @@ type WalletServiceClient interface {
|
|||||||
DebitGift(ctx context.Context, in *DebitGiftRequest, opts ...grpc.CallOption) (*DebitGiftResponse, error)
|
DebitGift(ctx context.Context, in *DebitGiftRequest, opts ...grpc.CallOption) (*DebitGiftResponse, error)
|
||||||
BatchDebitGift(ctx context.Context, in *BatchDebitGiftRequest, opts ...grpc.CallOption) (*BatchDebitGiftResponse, error)
|
BatchDebitGift(ctx context.Context, in *BatchDebitGiftRequest, opts ...grpc.CallOption) (*BatchDebitGiftResponse, error)
|
||||||
GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error)
|
GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error)
|
||||||
|
GetActiveHostSalaryPolicy(ctx context.Context, in *GetActiveHostSalaryPolicyRequest, opts ...grpc.CallOption) (*GetActiveHostSalaryPolicyResponse, error)
|
||||||
|
GetHostSalaryProgress(ctx context.Context, in *GetHostSalaryProgressRequest, opts ...grpc.CallOption) (*GetHostSalaryProgressResponse, error)
|
||||||
AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error)
|
AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error)
|
||||||
AdminCreditCoinSellerStock(ctx context.Context, in *AdminCreditCoinSellerStockRequest, opts ...grpc.CallOption) (*AdminCreditCoinSellerStockResponse, error)
|
AdminCreditCoinSellerStock(ctx context.Context, in *AdminCreditCoinSellerStockRequest, opts ...grpc.CallOption) (*AdminCreditCoinSellerStockResponse, error)
|
||||||
TransferCoinFromSeller(ctx context.Context, in *TransferCoinFromSellerRequest, opts ...grpc.CallOption) (*TransferCoinFromSellerResponse, error)
|
TransferCoinFromSeller(ctx context.Context, in *TransferCoinFromSellerRequest, opts ...grpc.CallOption) (*TransferCoinFromSellerResponse, error)
|
||||||
@ -373,6 +377,26 @@ func (c *walletServiceClient) GetBalances(ctx context.Context, in *GetBalancesRe
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *walletServiceClient) GetActiveHostSalaryPolicy(ctx context.Context, in *GetActiveHostSalaryPolicyRequest, opts ...grpc.CallOption) (*GetActiveHostSalaryPolicyResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetActiveHostSalaryPolicyResponse)
|
||||||
|
err := c.cc.Invoke(ctx, WalletService_GetActiveHostSalaryPolicy_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *walletServiceClient) GetHostSalaryProgress(ctx context.Context, in *GetHostSalaryProgressRequest, opts ...grpc.CallOption) (*GetHostSalaryProgressResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetHostSalaryProgressResponse)
|
||||||
|
err := c.cc.Invoke(ctx, WalletService_GetHostSalaryProgress_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *walletServiceClient) AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error) {
|
func (c *walletServiceClient) AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(AdminCreditAssetResponse)
|
out := new(AdminCreditAssetResponse)
|
||||||
@ -972,6 +996,8 @@ type WalletServiceServer interface {
|
|||||||
DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error)
|
DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error)
|
||||||
BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error)
|
BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error)
|
||||||
GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error)
|
GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error)
|
||||||
|
GetActiveHostSalaryPolicy(context.Context, *GetActiveHostSalaryPolicyRequest) (*GetActiveHostSalaryPolicyResponse, error)
|
||||||
|
GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error)
|
||||||
AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error)
|
AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error)
|
||||||
AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error)
|
AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error)
|
||||||
TransferCoinFromSeller(context.Context, *TransferCoinFromSellerRequest) (*TransferCoinFromSellerResponse, error)
|
TransferCoinFromSeller(context.Context, *TransferCoinFromSellerRequest) (*TransferCoinFromSellerResponse, error)
|
||||||
@ -1050,6 +1076,12 @@ func (UnimplementedWalletServiceServer) BatchDebitGift(context.Context, *BatchDe
|
|||||||
func (UnimplementedWalletServiceServer) GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error) {
|
func (UnimplementedWalletServiceServer) GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetBalances not implemented")
|
return nil, status.Error(codes.Unimplemented, "method GetBalances not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedWalletServiceServer) GetActiveHostSalaryPolicy(context.Context, *GetActiveHostSalaryPolicyRequest) (*GetActiveHostSalaryPolicyResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetActiveHostSalaryPolicy not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedWalletServiceServer) GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error) {
|
||||||
|
return nil, status.Error(codes.Unimplemented, "method GetHostSalaryProgress not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedWalletServiceServer) AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error) {
|
func (UnimplementedWalletServiceServer) AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method AdminCreditAsset not implemented")
|
return nil, status.Error(codes.Unimplemented, "method AdminCreditAsset not implemented")
|
||||||
}
|
}
|
||||||
@ -1302,6 +1334,42 @@ func _WalletService_GetBalances_Handler(srv interface{}, ctx context.Context, de
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _WalletService_GetActiveHostSalaryPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetActiveHostSalaryPolicyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(WalletServiceServer).GetActiveHostSalaryPolicy(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: WalletService_GetActiveHostSalaryPolicy_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(WalletServiceServer).GetActiveHostSalaryPolicy(ctx, req.(*GetActiveHostSalaryPolicyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _WalletService_GetHostSalaryProgress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetHostSalaryProgressRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(WalletServiceServer).GetHostSalaryProgress(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: WalletService_GetHostSalaryProgress_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(WalletServiceServer).GetHostSalaryProgress(ctx, req.(*GetHostSalaryProgressRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _WalletService_AdminCreditAsset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _WalletService_AdminCreditAsset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(AdminCreditAssetRequest)
|
in := new(AdminCreditAssetRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -2383,6 +2451,14 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "GetBalances",
|
MethodName: "GetBalances",
|
||||||
Handler: _WalletService_GetBalances_Handler,
|
Handler: _WalletService_GetBalances_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetActiveHostSalaryPolicy",
|
||||||
|
Handler: _WalletService_GetActiveHostSalaryPolicy_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetHostSalaryProgress",
|
||||||
|
Handler: _WalletService_GetHostSalaryProgress_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "AdminCreditAsset",
|
MethodName: "AdminCreditAsset",
|
||||||
Handler: _WalletService_AdminCreditAsset_Handler,
|
Handler: _WalletService_AdminCreditAsset_Handler,
|
||||||
|
|||||||
88
docs/Flutter麦上心跳接口.md
Normal file
88
docs/Flutter麦上心跳接口.md
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# Flutter 麦上心跳接口
|
||||||
|
|
||||||
|
## 地址
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/rooms/mic/heartbeat
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 参数
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `room_id` | string | 是 | 房间 ID。 |
|
||||||
|
| `command_id` | string | 是 | 本次心跳命令 ID。同一次请求重试复用同一个值,下一次心跳换新的值。 |
|
||||||
|
| `target_user_id` | int64 | 否 | 传 `0` 或不传表示当前登录用户自己。 |
|
||||||
|
| `mic_session_id` | string | 是 | 当前麦位会话 ID,必须使用上麦返回的 `mic_session_id`。 |
|
||||||
|
|
||||||
|
请求示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"room_id": "lalu_xxx",
|
||||||
|
"command_id": "cmd_mic_heartbeat_lalu_xxx_123_001",
|
||||||
|
"target_user_id": 0,
|
||||||
|
"mic_session_id": "mic_xxx"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 返回值
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "OK",
|
||||||
|
"request_id": "req_xxx",
|
||||||
|
"data": {
|
||||||
|
"result": {
|
||||||
|
"applied": true,
|
||||||
|
"room_version": 18,
|
||||||
|
"server_time_ms": 1778000005000
|
||||||
|
},
|
||||||
|
"seat_no": 1,
|
||||||
|
"mic_heartbeat_at_ms": 1778000005000,
|
||||||
|
"room": {
|
||||||
|
"room_id": "lalu_xxx",
|
||||||
|
"version": 18,
|
||||||
|
"mic_seats": [
|
||||||
|
{
|
||||||
|
"seat_no": 1,
|
||||||
|
"user_id": 123,
|
||||||
|
"publish_state": "publishing",
|
||||||
|
"mic_session_id": "mic_xxx",
|
||||||
|
"mic_heartbeat_at_ms": 1778000005000,
|
||||||
|
"seat_status": "publishing"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `data.seat_no` | 当前用户所在麦位。 |
|
||||||
|
| `data.mic_heartbeat_at_ms` | 服务端接受本次麦上心跳的时间,UTC epoch milliseconds。 |
|
||||||
|
| `data.room` | 刷新后的房间快照。 |
|
||||||
|
| `data.room.mic_seats[].mic_heartbeat_at_ms` | 麦位最近一次服务端接受心跳的时间。 |
|
||||||
|
| `data.room.mic_seats[].publish_state` | `publishing` 表示当前会话已确认发流,可以继续心跳。 |
|
||||||
|
| `data.room.mic_seats[].seat_status` | 展示状态,常见值为 `publishing`、`muted`、`occupied`、`empty`、`locked`。 |
|
||||||
|
|
||||||
|
## 相关 IM
|
||||||
|
|
||||||
|
| IM | 是否新增 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 无 | 否 | 麦上心跳不广播独立 IM。 |
|
||||||
|
| `room_mic_publish_confirmed` | 已有 | 发流确认成功后进入 `publishing`,Flutter 收到后可以开始麦上心跳。 |
|
||||||
|
| `room_mic_down` | 已有 | 用户下麦、离房、被踢、发流超时或 RTC 停止后释放麦位,Flutter 收到后停止麦上心跳。 |
|
||||||
|
|
||||||
|
## Flutter 调用规则
|
||||||
|
|
||||||
|
1. `POST /api/v1/rooms/mic/up` 成功后保存 `mic_session_id`。
|
||||||
|
2. RTC 发流成功后调用 `POST /api/v1/rooms/mic/publishing/confirm`。
|
||||||
|
3. 确认成功且当前麦位 `publish_state=publishing` 后,周期调用 `POST /api/v1/rooms/mic/heartbeat`。
|
||||||
|
4. `pending_publish` 状态不能调用麦上心跳开始计时。
|
||||||
|
5. 返回 `mic session mismatch` 时,本地保存的是旧 `mic_session_id`,需要刷新房间详情。
|
||||||
|
6. 收到 `room_mic_down` 或接口返回用户不在麦上时,停止本地麦上心跳。
|
||||||
@ -1,60 +1,45 @@
|
|||||||
# 房间锁房接口 Flutter 对接
|
# 房间锁房 Flutter 对接
|
||||||
|
|
||||||
本文描述 Flutter App 对接房间锁房能力的 HTTP 接口。房间密码由 `room-service` 只保存 bcrypt 哈希;客户端只提交明文用于本次设置或进房校验,任何列表、搜索、详情、房间消息和响应体都不会返回明文或哈希。
|
## 地址
|
||||||
|
|
||||||
## 接口地址
|
本地:
|
||||||
|
|
||||||
`POST /api/v1/rooms/password`
|
|
||||||
|
|
||||||
`POST /api/v1/rooms/join`
|
|
||||||
|
|
||||||
`GET /api/v1/rooms`
|
|
||||||
|
|
||||||
`GET /api/v1/rooms/feeds`
|
|
||||||
|
|
||||||
本地开发地址:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
http://127.0.0.1:13000
|
http://127.0.0.1:13000
|
||||||
```
|
```
|
||||||
|
|
||||||
## 请求方法
|
接口:
|
||||||
|
|
||||||
| 方法 | 接口 | 说明 |
|
| 方法 | 地址 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `POST` | `/api/v1/rooms/password` | 房主设置或清空房间入房密码。 |
|
| `POST` | `/api/v1/rooms/password` | 房主设置或清空房间密码 |
|
||||||
| `POST` | `/api/v1/rooms/join` | 进入房间;带锁房间需要传 `password`。 |
|
| `POST` | `/api/v1/rooms/join` | 进入房间;锁房时必须传密码 |
|
||||||
| `GET` | `/api/v1/rooms` | 公共房间列表和搜索;房间卡片返回 `locked`。 |
|
| `GET` | `/api/v1/rooms` | 房间列表和搜索,返回 `locked` |
|
||||||
| `GET` | `/api/v1/rooms/feeds` | Mine 页房间流;房间卡片返回 `locked`。 |
|
| `GET` | `/api/v1/rooms/feeds` | Mine 页房间流,返回 `locked` |
|
||||||
|
|
||||||
## 请求头
|
请求头:
|
||||||
|
|
||||||
| Header | 必填 | 模拟值 | 说明 |
|
| Header | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Authorization` | 是 | `Bearer <access_token>` |
|
||||||
|
| `X-App-Code` | 否 | App 编码,例如 `lalu` |
|
||||||
|
|
||||||
|
## 设置房间密码
|
||||||
|
|
||||||
|
`POST /api/v1/rooms/password`
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
| 字段 | 必填 | 类型 | 说明 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `Authorization` | 是 | `Bearer <access_token>` | 登录 access token;服务端从 token 读取当前用户 ID。 |
|
| `room_id` | 是 | string | 房间 ID |
|
||||||
| `X-App-Code` | 否 | `lalu` | App 编码;登录态接口最终以 token 内的 `app_code` 为准。 |
|
| `command_id` | 是 | string | 本次设置动作的幂等键,同一次重试复用 |
|
||||||
|
| `locked` | 是 | bool | `true` 设置密码,`false` 清空密码 |
|
||||||
|
| `password` | `locked=true` 必填 | string | 房间密码,服务端 trim 后最长 64 字符;`locked=false` 时忽略 |
|
||||||
|
|
||||||
## 设置或清空房间密码
|
请求:
|
||||||
|
|
||||||
只有房主可以调用。房管、主持人和普通观众调用会返回 `PERMISSION_DENIED`。
|
|
||||||
|
|
||||||
请求体:
|
|
||||||
|
|
||||||
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
|
||||||
| `command_id` | 是 | string | `cmd_room_password_room_10001_1710000000` | 房间命令幂等键;同一次点击重试必须复用。 |
|
|
||||||
| `locked` | 是 | bool | `true` | `true` 表示设置密码,`false` 表示清空密码。 |
|
|
||||||
| `password` | `locked=true` 时必填 | string | `1234` | 入房密码;服务端 trim 后最长 64 个字符。`locked=false` 时忽略。 |
|
|
||||||
|
|
||||||
设置密码请求示例:
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/v1/rooms/password
|
|
||||||
Authorization: Bearer <access_token>
|
|
||||||
X-App-Code: lalu
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
"room_id": "room_10001",
|
"room_id": "room_10001",
|
||||||
"command_id": "cmd_room_password_room_10001_1710000000",
|
"command_id": "cmd_room_password_room_10001_1710000000",
|
||||||
@ -63,22 +48,7 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
清空密码请求示例:
|
返回:
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/v1/rooms/password
|
|
||||||
Authorization: Bearer <access_token>
|
|
||||||
X-App-Code: lalu
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"room_id": "room_10001",
|
|
||||||
"command_id": "cmd_room_password_room_10001_1710000100",
|
|
||||||
"locked": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -114,42 +84,56 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
字段说明:
|
返回字段:
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 说明 |
|
||||||
| --- | --- | --- |
|
|
||||||
| `data.result.applied` | bool | 本次命令是否产生状态变更;重复提交相同命令可能为 `false`。 |
|
|
||||||
| `data.result.room_version` | int64 | 命令完成后房间版本。 |
|
|
||||||
| `data.room.locked` | bool | 最新锁房状态;`true` 表示后续新用户进房需要密码。 |
|
|
||||||
| `data.room.version` | int64 | 最新房间快照版本;客户端本地房间状态以更大版本覆盖旧状态。 |
|
|
||||||
| `data.seats` | array | 当前座位列表;锁房接口成功后可直接复用该房间快照刷新页面。 |
|
|
||||||
|
|
||||||
## 带密码进入房间
|
|
||||||
|
|
||||||
客户端进入房间前先看房间卡片或详情里的 `locked`:
|
|
||||||
|
|
||||||
| `locked` | 客户端行为 |
|
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `false` | 直接调用 JoinRoom,`password` 可不传或传空字符串。 |
|
| `data.result.applied` | 本次命令是否改变房间状态 |
|
||||||
| `true` | 先弹出密码输入,再调用 JoinRoom 并传 `password`。 |
|
| `data.result.room_version` | 房间最新版本 |
|
||||||
|
| `data.room.locked` | 最新锁房状态 |
|
||||||
|
| `data.room.version` | 房间快照版本 |
|
||||||
|
| `data.seats` | 当前麦位列表 |
|
||||||
|
|
||||||
请求体:
|
## 清空房间密码
|
||||||
|
|
||||||
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
`POST /api/v1/rooms/password`
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
|
||||||
| `command_id` | 是 | string | `cmd_join_room_10001_20001_1710000000` | 进房命令幂等键;同一次进房重试必须复用。 |
|
|
||||||
| `role` | 否 | string | `audience` | 当前固定传 `audience`。 |
|
|
||||||
| `password` | 锁房时必填 | string | `1234` | 只用于本次进房校验,服务端不保存明文。 |
|
|
||||||
|
|
||||||
请求示例:
|
参数:
|
||||||
|
|
||||||
```http
|
| 字段 | 必填 | 类型 | 说明 |
|
||||||
POST /api/v1/rooms/join
|
| --- | --- | --- | --- |
|
||||||
Authorization: Bearer <access_token>
|
| `room_id` | 是 | string | 房间 ID |
|
||||||
X-App-Code: lalu
|
| `command_id` | 是 | string | 本次清空动作的幂等键 |
|
||||||
Content-Type: application/json
|
| `locked` | 是 | bool | 固定传 `false` |
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"command_id": "cmd_room_unlock_room_10001_1710000100",
|
||||||
|
"locked": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回同设置密码接口,`data.room.locked=false`。
|
||||||
|
|
||||||
|
## 进入锁房房间
|
||||||
|
|
||||||
|
`POST /api/v1/rooms/join`
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
| 字段 | 必填 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `room_id` | 是 | string | 房间 ID |
|
||||||
|
| `command_id` | 是 | string | 本次进房动作的幂等键,同一次重试复用 |
|
||||||
|
| `role` | 否 | string | 当前传 `audience` |
|
||||||
|
| `password` | 锁房时必填 | string | 本次入房密码 |
|
||||||
|
|
||||||
|
请求:
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
"room_id": "room_10001",
|
"room_id": "room_10001",
|
||||||
"command_id": "cmd_join_room_10001_20001_1710000000",
|
"command_id": "cmd_join_room_10001_20001_1710000000",
|
||||||
@ -158,7 +142,7 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
成功响应只展示锁房相关字段:
|
返回:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -196,35 +180,33 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
返回字段:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `data.room.locked` | 当前房间是否锁房 |
|
||||||
|
| `data.viewer.user_id` | 当前用户 ID |
|
||||||
|
| `data.im.group_id` | JoinRoom 成功后需要加入的腾讯 IM 房间群 |
|
||||||
|
| `data.im.need_join_group` | `true` 时 Flutter 调腾讯 IM `joinGroup` |
|
||||||
|
| `data.rtc.available` | 是否拿到 RTC token |
|
||||||
|
|
||||||
进房规则:
|
进房规则:
|
||||||
|
|
||||||
| 场景 | 结果 |
|
| 场景 | 结果 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 房间未锁 | 不校验 `password`。 |
|
| 未锁房 | `password` 可不传 |
|
||||||
| 房间已锁且密码正确 | JoinRoom 成功,客户端再调用腾讯 IM `joinGroup` 和 RTC `enterRoom`。 |
|
| 已锁房且密码正确 | JoinRoom 成功,再加入 IM 房间群 |
|
||||||
| 房间已锁且密码错误或缺失 | 返回 `403 PERMISSION_DENIED`,不会创建 presence,不会返回 RTC token。 |
|
| 已锁房且密码错误或缺失 | 返回 `403 PERMISSION_DENIED`,不要加入 IM 房间群 |
|
||||||
| 房主进入自己的房间 | 不需要密码。 |
|
| 房主进自己房间 | 不需要密码 |
|
||||||
| 用户已经在房间内 | 再次 JoinRoom 按幂等恢复当前房间态,不会因为后续锁房被踢出。 |
|
| 已在房间内重复 JoinRoom | 非房主仍要按当前最新密码校验;密码错误返回 `403 PERMISSION_DENIED` |
|
||||||
|
|
||||||
## 房间列表和搜索 locked 字段
|
## 房间列表 locked 字段
|
||||||
|
|
||||||
公共列表和搜索使用同一个接口:
|
`GET /api/v1/rooms?tab=hot&query=100001`
|
||||||
|
|
||||||
```http
|
`GET /api/v1/rooms/feeds?tab=followed`
|
||||||
GET /api/v1/rooms?tab=hot&query=100001
|
|
||||||
Authorization: Bearer <access_token>
|
|
||||||
X-App-Code: lalu
|
|
||||||
```
|
|
||||||
|
|
||||||
Mine 页房间流:
|
返回:
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/v1/rooms/feeds?tab=followed
|
|
||||||
Authorization: Bearer <access_token>
|
|
||||||
X-App-Code: lalu
|
|
||||||
```
|
|
||||||
|
|
||||||
列表响应示例:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -236,18 +218,10 @@ X-App-Code: lalu
|
|||||||
{
|
{
|
||||||
"room_id": "room_10001",
|
"room_id": "room_10001",
|
||||||
"im_group_id": "room_10001",
|
"im_group_id": "room_10001",
|
||||||
"owner_user_id": "10001",
|
|
||||||
"title": "Live Room",
|
"title": "Live Room",
|
||||||
"cover_url": "https://cdn.example/room.png",
|
|
||||||
"mode": "voice",
|
|
||||||
"status": "active",
|
|
||||||
"locked": true,
|
"locked": true,
|
||||||
"heat": 1000,
|
|
||||||
"online_count": 12,
|
"online_count": 12,
|
||||||
"seat_count": 15,
|
|
||||||
"occupied_seat_count": 2,
|
|
||||||
"visible_region_id": 1,
|
"visible_region_id": 1,
|
||||||
"app_code": "lalu",
|
|
||||||
"room_short_id": "100001"
|
"room_short_id": "100001"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -256,17 +230,87 @@ X-App-Code: lalu
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
字段说明:
|
字段:
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- |
|
||||||
| `rooms[].locked` | bool | 房间是否需要入房密码;搜索结果和普通列表一致。 |
|
| `rooms[].locked` | 是否锁房;`true` 时进房前弹密码输入 |
|
||||||
| `rooms[].im_group_id` | string | 腾讯 IM 群 ID;只用于 JoinRoom 成功后的 `joinGroup`,不能绕过 JoinRoom。 |
|
| `rooms[].im_group_id` | 房间 IM 群 ID,只能在 JoinRoom 成功后使用 |
|
||||||
| `next_cursor` | string | 下一页游标;为空表示没有下一页。 |
|
| `rooms[].visible_region_id` | 房间区域 |
|
||||||
|
|
||||||
## 错误处理
|
## 相关 IM
|
||||||
|
|
||||||
失败返回仍使用统一 envelope:
|
### JoinRoom 成功后加入房间群
|
||||||
|
|
||||||
|
接口 `POST /api/v1/rooms/join` 成功后,Flutter 使用:
|
||||||
|
|
||||||
|
```text
|
||||||
|
data.im.group_id
|
||||||
|
```
|
||||||
|
|
||||||
|
调用腾讯 IM SDK 加入房间群。
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
| 场景 | Flutter 行为 |
|
||||||
|
| --- | --- |
|
||||||
|
| JoinRoom 成功 | 调腾讯 IM `joinGroup(data.im.group_id)` |
|
||||||
|
| JoinRoom 返回 `PERMISSION_DENIED` | 不调用 `joinGroup` |
|
||||||
|
| JoinRoom 返回 `ROOM_CLOSED` 或 `NOT_FOUND` | 不调用 `joinGroup`,刷新列表 |
|
||||||
|
|
||||||
|
### 房间密码变更区域通知
|
||||||
|
|
||||||
|
房主设置或清空密码后,服务端会发区域播报 IM,不再发房间群 IM。
|
||||||
|
|
||||||
|
Flutter 需要监听用户所在区域播报群的自定义消息:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_id": "room_password_changed:evt_room_xxx",
|
||||||
|
"broadcast_type": "room_password_changed",
|
||||||
|
"scope": "region",
|
||||||
|
"app_code": "lalu",
|
||||||
|
"region_id": 1,
|
||||||
|
"room_id": "room_10001",
|
||||||
|
"actor_user_id": 10001,
|
||||||
|
"locked": true,
|
||||||
|
"sent_at_ms": 1710000000456,
|
||||||
|
"room_version": 12,
|
||||||
|
"source_event_id": "evt_room_xxx",
|
||||||
|
"action": {
|
||||||
|
"type": "refresh_room",
|
||||||
|
"room_id": "room_10001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `broadcast_type` | 固定 `room_password_changed` |
|
||||||
|
| `scope` | 固定 `region` |
|
||||||
|
| `region_id` | 区域 ID |
|
||||||
|
| `room_id` | 发生变化的房间 |
|
||||||
|
| `locked` | 最新锁房状态 |
|
||||||
|
| `room_version` | 房间版本;本地只用更大的版本覆盖旧状态 |
|
||||||
|
| `action.type` | 固定 `refresh_room` |
|
||||||
|
|
||||||
|
处理:
|
||||||
|
|
||||||
|
| 当前页面 | 行为 |
|
||||||
|
| --- | --- |
|
||||||
|
| 房间列表页 | 更新对应房间卡片 `locked` |
|
||||||
|
| 房间详情页 | 如果 `room_id` 相同,刷新房间快照或更新本地 `locked` |
|
||||||
|
| 密码弹窗打开中 | 收到同房间更新后,按最新 `locked` 状态决定是否关闭弹窗 |
|
||||||
|
|
||||||
|
### 房间群系统消息
|
||||||
|
|
||||||
|
`room_password_changed` 不走房间群 IM。房间内用户刷新锁房状态依赖区域播报或接口返回的 `room.locked`。
|
||||||
|
|
||||||
|
## 错误返回
|
||||||
|
|
||||||
|
失败统一返回:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -276,108 +320,13 @@ X-App-Code: lalu
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| HTTP 状态码 | `code` | 典型场景 | 处理方式 |
|
常见错误:
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `400` | `INVALID_ARGUMENT` | `room_id`、`command_id` 缺失;设置锁房时 `password` 为空或超过 64 字符。 | 客户端修正参数,不自动重试。 |
|
|
||||||
| `401` | `UNAUTHORIZED` | token 缺失、无效、过期或会话失效。 | 重新登录。 |
|
|
||||||
| `403` | `PROFILE_REQUIRED` | 用户资料未完成。 | 跳转资料补全流程。 |
|
|
||||||
| `403` | `PERMISSION_DENIED` | 非房主设置密码;进锁房密码错误;用户被 ban。 | 设置密码时隐藏入口或提示无权限;进房时提示密码错误。 |
|
|
||||||
| `404` | `NOT_FOUND` | 房间不存在。 | 刷新列表或退出房间页。 |
|
|
||||||
| `409` | `ROOM_CLOSED` | 房间已关闭。 | 刷新列表或退出房间页。 |
|
|
||||||
| `409` | `CONFLICT` | 同一个 `command_id` 被不同请求体复用。 | 为新的用户动作生成新的 `command_id`。 |
|
|
||||||
| `502` | `UPSTREAM_ERROR` | 内部服务暂不可用。 | 可提示稍后重试。 |
|
|
||||||
|
|
||||||
幂等规则:
|
| HTTP | code | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
| 场景 | 返回 |
|
| `400` | `INVALID_ARGUMENT` | 参数缺失、设置密码时密码为空或超过 64 字符 |
|
||||||
| --- | --- |
|
| `401` | `UNAUTHORIZED` | token 无效或过期 |
|
||||||
| 同一个 `command_id` 重试同一次设置密码 | 返回同一命令结果,不重复变更房间。 |
|
| `403` | `PERMISSION_DENIED` | 非房主设置密码、进房密码错误、用户被 ban |
|
||||||
| 同一个 `command_id` 换另一个密码 | `409 CONFLICT`。 |
|
| `404` | `NOT_FOUND` | 房间不存在 |
|
||||||
| 重复清空已解锁房间 | `200 OK`,`data.room.locked=false`。 |
|
| `409` | `ROOM_CLOSED` | 房间已关闭 |
|
||||||
| 同一个用户重复 JoinRoom | 返回当前房间首屏数据,不重复创建 presence。 |
|
| `409` | `CONFLICT` | 同一个 `command_id` 被不同请求体复用 |
|
||||||
|
|
||||||
## Flutter 解析示例
|
|
||||||
|
|
||||||
```dart
|
|
||||||
class RoomCommandResult {
|
|
||||||
RoomCommandResult({
|
|
||||||
required this.applied,
|
|
||||||
required this.roomVersion,
|
|
||||||
required this.serverTimeMs,
|
|
||||||
});
|
|
||||||
|
|
||||||
final bool applied;
|
|
||||||
final int roomVersion;
|
|
||||||
final int serverTimeMs;
|
|
||||||
|
|
||||||
factory RoomCommandResult.fromJson(Map<String, dynamic> json) {
|
|
||||||
return RoomCommandResult(
|
|
||||||
applied: json['applied'] as bool? ?? false,
|
|
||||||
roomVersion: (json['room_version'] as num?)?.toInt() ?? 0,
|
|
||||||
serverTimeMs: (json['server_time_ms'] as num?)?.toInt() ?? 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RoomCard {
|
|
||||||
RoomCard({
|
|
||||||
required this.roomId,
|
|
||||||
required this.imGroupId,
|
|
||||||
required this.title,
|
|
||||||
required this.locked,
|
|
||||||
required this.onlineCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String roomId;
|
|
||||||
final String imGroupId;
|
|
||||||
final String title;
|
|
||||||
final bool locked;
|
|
||||||
final int onlineCount;
|
|
||||||
|
|
||||||
factory RoomCard.fromJson(Map<String, dynamic> json) {
|
|
||||||
return RoomCard(
|
|
||||||
roomId: json['room_id'] as String? ?? '',
|
|
||||||
imGroupId: json['im_group_id'] as String? ?? '',
|
|
||||||
title: json['title'] as String? ?? '',
|
|
||||||
locked: json['locked'] as bool? ?? false,
|
|
||||||
onlineCount: (json['online_count'] as num?)?.toInt() ?? 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RoomLockResult {
|
|
||||||
RoomLockResult({
|
|
||||||
required this.result,
|
|
||||||
required this.room,
|
|
||||||
required this.serverTimeMs,
|
|
||||||
});
|
|
||||||
|
|
||||||
final RoomCommandResult result;
|
|
||||||
final RoomCard room;
|
|
||||||
final int serverTimeMs;
|
|
||||||
|
|
||||||
factory RoomLockResult.fromJson(Map<String, dynamic> json) {
|
|
||||||
return RoomLockResult(
|
|
||||||
result: RoomCommandResult.fromJson(
|
|
||||||
json['result'] as Map<String, dynamic>? ?? const {},
|
|
||||||
),
|
|
||||||
room: RoomCard.fromJson(
|
|
||||||
json['room'] as Map<String, dynamic>? ?? const {},
|
|
||||||
),
|
|
||||||
serverTimeMs: (json['server_time_ms'] as num?)?.toInt() ?? 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
调用时先解析外层 envelope:只有 `code == "OK"` 时读取 `data`;其他 `code` 按登录态、参数错误、权限错误或服务错误处理,并把 `request_id` 写入客户端日志。
|
|
||||||
|
|
||||||
客户端生成 `command_id` 时按一次用户动作生成一次,例如:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
String newRoomCommandId(String action, String roomId, String userId) {
|
|
||||||
return 'cmd_${action}_${roomId}_${userId}_${DateTime.now().millisecondsSinceEpoch}';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
同一次 HTTP 重试复用原 `command_id`;用户重新输入密码或再次点击设置按钮时生成新的 `command_id`。
|
|
||||||
|
|||||||
@ -687,6 +687,46 @@ Rules:
|
|||||||
- `room_version` 和 `event_time_ms` 用于丢弃旧事件,避免旧 audience/leave 事件清理新会话。
|
- `room_version` 和 `event_time_ms` 用于丢弃旧事件,避免旧 audience/leave 事件清理新会话。
|
||||||
- 成功后麦位 `publish_state=publishing`。
|
- 成功后麦位 `publish_state=publishing`。
|
||||||
|
|
||||||
|
### Mic Heartbeat
|
||||||
|
|
||||||
|
地址:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/rooms/mic/heartbeat
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"room_id": "lalu_xxx",
|
||||||
|
"command_id": "cmd_mic_heartbeat_<room_id>_<user_id>_<nonce>",
|
||||||
|
"target_user_id": 0,
|
||||||
|
"mic_session_id": "mic_xxx"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回值:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": {},
|
||||||
|
"seat_no": 1,
|
||||||
|
"mic_heartbeat_at_ms": 1778000005000,
|
||||||
|
"room": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
相关 IM:
|
||||||
|
|
||||||
|
- 无独立 IM。该接口只刷新当前 `publishing` 麦位会话的服务端心跳时间和房间 presence。
|
||||||
|
- `target_user_id=0` 表示当前登录用户自己。
|
||||||
|
- `mic_session_id` 必须等于当前麦位会话。
|
||||||
|
- `pending_publish` 不能调用该接口开始计时,必须先调用 `POST /api/v1/rooms/mic/publishing/confirm`。
|
||||||
|
- 重试同一次心跳复用同一个 `command_id`;下一次心跳必须换新的 `command_id`。
|
||||||
|
|
||||||
### Mic Down
|
### Mic Down
|
||||||
|
|
||||||
```http
|
```http
|
||||||
|
|||||||
@ -16,6 +16,8 @@ const (
|
|||||||
TypeRedPacket = "red_packet"
|
TypeRedPacket = "red_packet"
|
||||||
// TypeRoomTreasure 是语音房宝箱满能量后的开箱倒计时播报。
|
// TypeRoomTreasure 是语音房宝箱满能量后的开箱倒计时播报。
|
||||||
TypeRoomTreasure = "room_treasure"
|
TypeRoomTreasure = "room_treasure"
|
||||||
|
// TypeRoomPasswordChanged 是锁房状态变化的区域通知,客户端用它刷新房间锁状态入口。
|
||||||
|
TypeRoomPasswordChanged = "room_password_changed"
|
||||||
// TypeLuckyGiftBigWin 是幸运礼物中奖区域飘屏;当前默认 1 倍及以上即进入播报 outbox。
|
// TypeLuckyGiftBigWin 是幸运礼物中奖区域飘屏;当前默认 1 倍及以上即进入播报 outbox。
|
||||||
TypeLuckyGiftBigWin = "lucky_gift_big_win"
|
TypeLuckyGiftBigWin = "lucky_gift_big_win"
|
||||||
|
|
||||||
|
|||||||
@ -298,7 +298,7 @@ func (s *Service) ProcessPendingBroadcasts(ctx context.Context, options WorkerOp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleRoomEvent 把已提交的 room outbox 事实转换为服务端播报。
|
// HandleRoomEvent 把已提交的 room outbox 事实转换为服务端播报。
|
||||||
// 只有 RoomGiftSent 且满足区域和礼物价值阈值时才生成区域播报,避免把展示策略侵入 Room Cell 主链路。
|
// 礼物、宝箱和锁房只使用 room outbox 中的稳定事实,避免把展示策略侵入 Room Cell 主链路。
|
||||||
func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.EventEnvelope) (broadcastdomain.ConsumeRoomEventResult, error) {
|
func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.EventEnvelope) (broadcastdomain.ConsumeRoomEventResult, error) {
|
||||||
if envelope == nil {
|
if envelope == nil {
|
||||||
return broadcastdomain.ConsumeRoomEventResult{}, xerr.New(xerr.InvalidArgument, "room event envelope is required")
|
return broadcastdomain.ConsumeRoomEventResult{}, xerr.New(xerr.InvalidArgument, "room event envelope is required")
|
||||||
@ -308,8 +308,11 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
|
|||||||
if envelope.GetEventType() == "RoomTreasureCountdownStarted" {
|
if envelope.GetEventType() == "RoomTreasureCountdownStarted" {
|
||||||
return s.handleRoomTreasureCountdown(eventCtx, envelope)
|
return s.handleRoomTreasureCountdown(eventCtx, envelope)
|
||||||
}
|
}
|
||||||
|
if envelope.GetEventType() == "RoomPasswordChanged" {
|
||||||
|
return s.handleRoomPasswordChanged(eventCtx, envelope)
|
||||||
|
}
|
||||||
if envelope.GetEventType() != "RoomGiftSent" {
|
if envelope.GetEventType() != "RoomGiftSent" {
|
||||||
// 当前只消费礼物和房间宝箱事实;未来红包或运营事件应显式增加事件类型分支和测试。
|
// 当前只消费礼物、房间宝箱和锁房事实;未来运营事件应显式增加事件类型分支和测试。
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
var gift roomeventsv1.RoomGiftSent
|
var gift roomeventsv1.RoomGiftSent
|
||||||
@ -343,6 +346,38 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) handleRoomPasswordChanged(ctx context.Context, envelope *roomeventsv1.EventEnvelope) (broadcastdomain.ConsumeRoomEventResult, error) {
|
||||||
|
result := broadcastdomain.ConsumeRoomEventResult{EventID: envelope.GetEventId(), Status: broadcastdomain.StatusSkipped}
|
||||||
|
var event roomeventsv1.RoomPasswordChanged
|
||||||
|
if err := proto.Unmarshal(envelope.GetBody(), &event); err != nil {
|
||||||
|
return broadcastdomain.ConsumeRoomEventResult{}, err
|
||||||
|
}
|
||||||
|
if event.GetVisibleRegionId() <= 0 {
|
||||||
|
// 区域通知必须有明确区域,不能把无区域房间退化成全局播报扩大影响面。
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
broadcastEventID := roomPasswordChangedBroadcastEventID(envelope)
|
||||||
|
payloadJSON, err := roomPasswordChangedPayload(envelope, &event, broadcastEventID, s.now().UTC().UnixMilli())
|
||||||
|
if err != nil {
|
||||||
|
return broadcastdomain.ConsumeRoomEventResult{}, err
|
||||||
|
}
|
||||||
|
published, err := s.PublishRegionBroadcast(ctx, PublishInput{
|
||||||
|
EventID: broadcastEventID,
|
||||||
|
BroadcastType: broadcastdomain.TypeRoomPasswordChanged,
|
||||||
|
RegionID: event.GetVisibleRegionId(),
|
||||||
|
PayloadJSON: payloadJSON,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return broadcastdomain.ConsumeRoomEventResult{}, err
|
||||||
|
}
|
||||||
|
return broadcastdomain.ConsumeRoomEventResult{
|
||||||
|
EventID: envelope.GetEventId(),
|
||||||
|
Status: published.Status,
|
||||||
|
BroadcastEventID: published.EventID,
|
||||||
|
BroadcastCreated: published.Created,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) handleRoomTreasureCountdown(ctx context.Context, envelope *roomeventsv1.EventEnvelope) (broadcastdomain.ConsumeRoomEventResult, error) {
|
func (s *Service) handleRoomTreasureCountdown(ctx context.Context, envelope *roomeventsv1.EventEnvelope) (broadcastdomain.ConsumeRoomEventResult, error) {
|
||||||
result := broadcastdomain.ConsumeRoomEventResult{EventID: envelope.GetEventId(), Status: broadcastdomain.StatusSkipped}
|
result := broadcastdomain.ConsumeRoomEventResult{EventID: envelope.GetEventId(), Status: broadcastdomain.StatusSkipped}
|
||||||
var treasure roomeventsv1.RoomTreasureCountdownStarted
|
var treasure roomeventsv1.RoomTreasureCountdownStarted
|
||||||
@ -519,6 +554,32 @@ func roomTreasureBroadcastEventID(envelope *roomeventsv1.EventEnvelope, treasure
|
|||||||
return "room_treasure_broadcast:" + envelope.GetEventId()
|
return "room_treasure_broadcast:" + envelope.GetEventId()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func roomPasswordChangedBroadcastEventID(envelope *roomeventsv1.EventEnvelope) string {
|
||||||
|
return "room_password_changed:" + envelope.GetEventId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomPasswordChangedPayload(envelope *roomeventsv1.EventEnvelope, event *roomeventsv1.RoomPasswordChanged, eventID string, sentAtMS int64) (string, error) {
|
||||||
|
payload := map[string]any{
|
||||||
|
"event_id": eventID,
|
||||||
|
"broadcast_type": broadcastdomain.TypeRoomPasswordChanged,
|
||||||
|
"scope": broadcastdomain.ScopeRegion,
|
||||||
|
"app_code": envelope.GetAppCode(),
|
||||||
|
"region_id": event.GetVisibleRegionId(),
|
||||||
|
"room_id": envelope.GetRoomId(),
|
||||||
|
"actor_user_id": event.GetActorUserId(),
|
||||||
|
"locked": event.GetLocked(),
|
||||||
|
"sent_at_ms": sentAtMS,
|
||||||
|
"room_version": envelope.GetRoomVersion(),
|
||||||
|
"source_event_id": envelope.GetEventId(),
|
||||||
|
"action": map[string]any{
|
||||||
|
"type": "refresh_room",
|
||||||
|
"room_id": envelope.GetRoomId(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(payload)
|
||||||
|
return string(encoded), err
|
||||||
|
}
|
||||||
|
|
||||||
func roomTreasurePayload(envelope *roomeventsv1.EventEnvelope, treasure *roomeventsv1.RoomTreasureCountdownStarted, eventID string, sentAtMS int64) (string, error) {
|
func roomTreasurePayload(envelope *roomeventsv1.EventEnvelope, treasure *roomeventsv1.RoomTreasureCountdownStarted, eventID string, sentAtMS int64) (string, error) {
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"event_id": eventID,
|
"event_id": eventID,
|
||||||
|
|||||||
@ -56,6 +56,40 @@ func TestHandleRoomGiftSentCreatesRegionBroadcast(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleRoomPasswordChangedCreatesRegionBroadcast(t *testing.T) {
|
||||||
|
repository := newFakeRepository()
|
||||||
|
service := New(Config{NodeID: "node-a"}, repository, nil, nil)
|
||||||
|
service.SetClock(func() time.Time { return time.UnixMilli(1_700_000_001_000) })
|
||||||
|
envelope := mustPasswordChangedEnvelope(t, &roomeventsv1.RoomPasswordChanged{
|
||||||
|
ActorUserId: 42,
|
||||||
|
Locked: true,
|
||||||
|
VisibleRegionId: 1001,
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := service.HandleRoomEvent(appcode.WithContext(context.Background(), "lalu"), envelope)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("HandleRoomEvent password changed failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.Status != broadcastdomain.StatusPending || !result.BroadcastCreated || result.BroadcastEventID != "room_password_changed:"+envelope.GetEventId() {
|
||||||
|
t.Fatalf("unexpected password broadcast result: %+v", result)
|
||||||
|
}
|
||||||
|
record := repository.records[result.BroadcastEventID]
|
||||||
|
if record.GroupID != "hy_lalu_bc_r_1001" || record.Scope != broadcastdomain.ScopeRegion || record.BroadcastType != broadcastdomain.TypeRoomPasswordChanged {
|
||||||
|
t.Fatalf("unexpected password broadcast record: %+v", record)
|
||||||
|
}
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(record.PayloadJSON), &payload); err != nil {
|
||||||
|
t.Fatalf("password payload is not json: %v", err)
|
||||||
|
}
|
||||||
|
if payload["room_id"] != "room-1001" || payload["scope"] != "region" || payload["locked"] != true || payload["actor_user_id"] != float64(42) {
|
||||||
|
t.Fatalf("password payload mismatch: %+v", payload)
|
||||||
|
}
|
||||||
|
action, ok := payload["action"].(map[string]any)
|
||||||
|
if !ok || action["type"] != "refresh_room" || action["room_id"] != "room-1001" {
|
||||||
|
t.Fatalf("password payload action mismatch: %+v", payload["action"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleRoomGiftSentSkipsNonBroadcastCases(t *testing.T) {
|
func TestHandleRoomGiftSentSkipsNonBroadcastCases(t *testing.T) {
|
||||||
service := New(Config{SuperGiftMinValue: 1000}, newFakeRepository(), nil, nil)
|
service := New(Config{SuperGiftMinValue: 1000}, newFakeRepository(), nil, nil)
|
||||||
belowThreshold := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{GiftValue: 999, VisibleRegionId: 1001})
|
belowThreshold := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{GiftValue: 999, VisibleRegionId: 1001})
|
||||||
@ -301,6 +335,23 @@ func mustGiftEnvelope(t *testing.T, gift *roomeventsv1.RoomGiftSent) *roomevents
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustPasswordChangedEnvelope(t *testing.T, event *roomeventsv1.RoomPasswordChanged) *roomeventsv1.EventEnvelope {
|
||||||
|
t.Helper()
|
||||||
|
body, err := proto.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal password changed failed: %v", err)
|
||||||
|
}
|
||||||
|
return &roomeventsv1.EventEnvelope{
|
||||||
|
EventId: "evt-room-password-1",
|
||||||
|
RoomId: "room-1001",
|
||||||
|
EventType: "RoomPasswordChanged",
|
||||||
|
RoomVersion: 8,
|
||||||
|
OccurredAtMs: 1_700_000_001_000,
|
||||||
|
AppCode: "lalu",
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func redPacketPayloadData(t *testing.T, payloadJSON string) map[string]any {
|
func redPacketPayloadData(t *testing.T, payloadJSON string) map[string]any {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ type RoomClient interface {
|
|||||||
MicDown(ctx context.Context, req *roomv1.MicDownRequest) (*roomv1.MicDownResponse, error)
|
MicDown(ctx context.Context, req *roomv1.MicDownRequest) (*roomv1.MicDownResponse, error)
|
||||||
ChangeMicSeat(ctx context.Context, req *roomv1.ChangeMicSeatRequest) (*roomv1.ChangeMicSeatResponse, error)
|
ChangeMicSeat(ctx context.Context, req *roomv1.ChangeMicSeatRequest) (*roomv1.ChangeMicSeatResponse, error)
|
||||||
ConfirmMicPublishing(ctx context.Context, req *roomv1.ConfirmMicPublishingRequest) (*roomv1.ConfirmMicPublishingResponse, error)
|
ConfirmMicPublishing(ctx context.Context, req *roomv1.ConfirmMicPublishingRequest) (*roomv1.ConfirmMicPublishingResponse, error)
|
||||||
|
MicHeartbeat(ctx context.Context, req *roomv1.MicHeartbeatRequest) (*roomv1.MicHeartbeatResponse, error)
|
||||||
SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error)
|
SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error)
|
||||||
ApplyRTCEvent(ctx context.Context, req *roomv1.ApplyRTCEventRequest) (*roomv1.ApplyRTCEventResponse, error)
|
ApplyRTCEvent(ctx context.Context, req *roomv1.ApplyRTCEventRequest) (*roomv1.ApplyRTCEventResponse, error)
|
||||||
SetMicSeatLock(ctx context.Context, req *roomv1.SetMicSeatLockRequest) (*roomv1.SetMicSeatLockResponse, error)
|
SetMicSeatLock(ctx context.Context, req *roomv1.SetMicSeatLockRequest) (*roomv1.SetMicSeatLockResponse, error)
|
||||||
@ -132,6 +133,10 @@ func (c *grpcRoomClient) ConfirmMicPublishing(ctx context.Context, req *roomv1.C
|
|||||||
return c.client.ConfirmMicPublishing(ctx, req)
|
return c.client.ConfirmMicPublishing(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *grpcRoomClient) MicHeartbeat(ctx context.Context, req *roomv1.MicHeartbeatRequest) (*roomv1.MicHeartbeatResponse, error) {
|
||||||
|
return c.client.MicHeartbeat(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *grpcRoomClient) SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error) {
|
func (c *grpcRoomClient) SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error) {
|
||||||
return c.client.SetMicMute(ctx, req)
|
return c.client.SetMicMute(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import (
|
|||||||
// WalletClient 抽象 gateway 对 wallet-service 余额查询能力的依赖。
|
// WalletClient 抽象 gateway 对 wallet-service 余额查询能力的依赖。
|
||||||
type WalletClient interface {
|
type WalletClient interface {
|
||||||
GetBalances(ctx context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error)
|
GetBalances(ctx context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error)
|
||||||
|
GetActiveHostSalaryPolicy(ctx context.Context, req *walletv1.GetActiveHostSalaryPolicyRequest) (*walletv1.GetActiveHostSalaryPolicyResponse, error)
|
||||||
|
GetHostSalaryProgress(ctx context.Context, req *walletv1.GetHostSalaryProgressRequest) (*walletv1.GetHostSalaryProgressResponse, error)
|
||||||
GetWalletOverview(ctx context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error)
|
GetWalletOverview(ctx context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error)
|
||||||
GetWalletValueSummary(ctx context.Context, req *walletv1.GetWalletValueSummaryRequest) (*walletv1.GetWalletValueSummaryResponse, error)
|
GetWalletValueSummary(ctx context.Context, req *walletv1.GetWalletValueSummaryRequest) (*walletv1.GetWalletValueSummaryResponse, error)
|
||||||
GetUserGiftWall(ctx context.Context, req *walletv1.GetUserGiftWallRequest) (*walletv1.GetUserGiftWallResponse, error)
|
GetUserGiftWall(ctx context.Context, req *walletv1.GetUserGiftWallRequest) (*walletv1.GetUserGiftWallResponse, error)
|
||||||
@ -54,6 +56,14 @@ func (c *grpcWalletClient) GetBalances(ctx context.Context, req *walletv1.GetBal
|
|||||||
return c.client.GetBalances(ctx, req)
|
return c.client.GetBalances(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *grpcWalletClient) GetActiveHostSalaryPolicy(ctx context.Context, req *walletv1.GetActiveHostSalaryPolicyRequest) (*walletv1.GetActiveHostSalaryPolicyResponse, error) {
|
||||||
|
return c.client.GetActiveHostSalaryPolicy(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcWalletClient) GetHostSalaryProgress(ctx context.Context, req *walletv1.GetHostSalaryProgressRequest) (*walletv1.GetHostSalaryProgressResponse, error) {
|
||||||
|
return c.client.GetHostSalaryProgress(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *grpcWalletClient) GetWalletOverview(ctx context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error) {
|
func (c *grpcWalletClient) GetWalletOverview(ctx context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error) {
|
||||||
return c.client.GetWalletOverview(ctx, req)
|
return c.client.GetWalletOverview(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,6 +72,8 @@ type UserHandlers struct {
|
|||||||
GetMyRoleSummary http.HandlerFunc
|
GetMyRoleSummary http.HandlerFunc
|
||||||
SearchHostAgencies http.HandlerFunc
|
SearchHostAgencies http.HandlerFunc
|
||||||
ApplyToHostAgency http.HandlerFunc
|
ApplyToHostAgency http.HandlerFunc
|
||||||
|
GetHostCenterAgency http.HandlerFunc
|
||||||
|
GetHostCenterPlatformPolicy http.HandlerFunc
|
||||||
GetAgencyCenterOverview http.HandlerFunc
|
GetAgencyCenterOverview http.HandlerFunc
|
||||||
ListAgencyCenterHosts http.HandlerFunc
|
ListAgencyCenterHosts http.HandlerFunc
|
||||||
ListAgencyCenterApplications http.HandlerFunc
|
ListAgencyCenterApplications http.HandlerFunc
|
||||||
@ -127,6 +129,7 @@ type RoomHandlers struct {
|
|||||||
MicDown http.HandlerFunc
|
MicDown http.HandlerFunc
|
||||||
ChangeMicSeat http.HandlerFunc
|
ChangeMicSeat http.HandlerFunc
|
||||||
ConfirmMicPublishing http.HandlerFunc
|
ConfirmMicPublishing http.HandlerFunc
|
||||||
|
MicHeartbeat http.HandlerFunc
|
||||||
SetMicMute http.HandlerFunc
|
SetMicMute http.HandlerFunc
|
||||||
SetMicSeatLock http.HandlerFunc
|
SetMicSeatLock http.HandlerFunc
|
||||||
SetChatEnabled http.HandlerFunc
|
SetChatEnabled http.HandlerFunc
|
||||||
@ -300,6 +303,8 @@ func (r routes) registerUserRoutes() {
|
|||||||
r.profile("/users/me/role-summary", "", h.GetMyRoleSummary)
|
r.profile("/users/me/role-summary", "", h.GetMyRoleSummary)
|
||||||
r.profile("/host/agencies/search", http.MethodGet, h.SearchHostAgencies)
|
r.profile("/host/agencies/search", http.MethodGet, h.SearchHostAgencies)
|
||||||
r.profile("/host/agency-applications", http.MethodPost, h.ApplyToHostAgency)
|
r.profile("/host/agency-applications", http.MethodPost, h.ApplyToHostAgency)
|
||||||
|
r.profile("/host-center/agency", http.MethodGet, h.GetHostCenterAgency)
|
||||||
|
r.profile("/host-center/platform-policy", http.MethodGet, h.GetHostCenterPlatformPolicy)
|
||||||
r.profile("/agency-center/overview", http.MethodGet, h.GetAgencyCenterOverview)
|
r.profile("/agency-center/overview", http.MethodGet, h.GetAgencyCenterOverview)
|
||||||
r.profile("/agency-center/hosts", http.MethodGet, h.ListAgencyCenterHosts)
|
r.profile("/agency-center/hosts", http.MethodGet, h.ListAgencyCenterHosts)
|
||||||
r.profile("/agency-center/applications", http.MethodGet, h.ListAgencyCenterApplications)
|
r.profile("/agency-center/applications", http.MethodGet, h.ListAgencyCenterApplications)
|
||||||
@ -357,6 +362,7 @@ func (r routes) registerRoomRoutes() {
|
|||||||
r.profile("/rooms/mic/down", http.MethodPost, h.MicDown)
|
r.profile("/rooms/mic/down", http.MethodPost, h.MicDown)
|
||||||
r.profile("/rooms/mic/change", http.MethodPost, h.ChangeMicSeat)
|
r.profile("/rooms/mic/change", http.MethodPost, h.ChangeMicSeat)
|
||||||
r.profile("/rooms/mic/publishing/confirm", http.MethodPost, h.ConfirmMicPublishing)
|
r.profile("/rooms/mic/publishing/confirm", http.MethodPost, h.ConfirmMicPublishing)
|
||||||
|
r.profile("/rooms/mic/heartbeat", http.MethodPost, h.MicHeartbeat)
|
||||||
r.profile("/rooms/mic/mute", http.MethodPost, h.SetMicMute)
|
r.profile("/rooms/mic/mute", http.MethodPost, h.SetMicMute)
|
||||||
r.profile("/rooms/mic/lock", http.MethodPost, h.SetMicSeatLock)
|
r.profile("/rooms/mic/lock", http.MethodPost, h.SetMicSeatLock)
|
||||||
r.profile("/rooms/chat/enabled", http.MethodPost, h.SetChatEnabled)
|
r.profile("/rooms/chat/enabled", http.MethodPost, h.SetChatEnabled)
|
||||||
|
|||||||
@ -44,6 +44,7 @@ type fakeRoomClient struct {
|
|||||||
lastMicDown *roomv1.MicDownRequest
|
lastMicDown *roomv1.MicDownRequest
|
||||||
lastChangeMicSeat *roomv1.ChangeMicSeatRequest
|
lastChangeMicSeat *roomv1.ChangeMicSeatRequest
|
||||||
lastConfirmMic *roomv1.ConfirmMicPublishingRequest
|
lastConfirmMic *roomv1.ConfirmMicPublishingRequest
|
||||||
|
lastMicHeartbeat *roomv1.MicHeartbeatRequest
|
||||||
lastSetMicMute *roomv1.SetMicMuteRequest
|
lastSetMicMute *roomv1.SetMicMuteRequest
|
||||||
lastRTCEvent *roomv1.ApplyRTCEventRequest
|
lastRTCEvent *roomv1.ApplyRTCEventRequest
|
||||||
lastMicSeatLock *roomv1.SetMicSeatLockRequest
|
lastMicSeatLock *roomv1.SetMicSeatLockRequest
|
||||||
@ -203,6 +204,11 @@ func (f *fakeRoomClient) ConfirmMicPublishing(_ context.Context, req *roomv1.Con
|
|||||||
return &roomv1.ConfirmMicPublishingResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 7}}, nil
|
return &roomv1.ConfirmMicPublishingResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 7}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeRoomClient) MicHeartbeat(_ context.Context, req *roomv1.MicHeartbeatRequest) (*roomv1.MicHeartbeatResponse, error) {
|
||||||
|
f.lastMicHeartbeat = req
|
||||||
|
return &roomv1.MicHeartbeatResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 8}, SeatNo: 2, MicHeartbeatAtMs: 1_700_000_001_000}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeRoomClient) SetMicMute(_ context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error) {
|
func (f *fakeRoomClient) SetMicMute(_ context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error) {
|
||||||
f.lastSetMicMute = req
|
f.lastSetMicMute = req
|
||||||
return &roomv1.SetMicMuteResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 7}}, nil
|
return &roomv1.SetMicMuteResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 7}}, nil
|
||||||
@ -425,63 +431,69 @@ type fakeUserHostClient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fakeWalletClient struct {
|
type fakeWalletClient struct {
|
||||||
last *walletv1.GetBalancesRequest
|
last *walletv1.GetBalancesRequest
|
||||||
balanceRequests []*walletv1.GetBalancesRequest
|
balanceRequests []*walletv1.GetBalancesRequest
|
||||||
resp *walletv1.GetBalancesResponse
|
resp *walletv1.GetBalancesResponse
|
||||||
balancesByUserID map[int64]*walletv1.GetBalancesResponse
|
balancesByUserID map[int64]*walletv1.GetBalancesResponse
|
||||||
err error
|
err error
|
||||||
lastOverview *walletv1.GetWalletOverviewRequest
|
lastHostSalaryPolicy *walletv1.GetActiveHostSalaryPolicyRequest
|
||||||
overviewResp *walletv1.GetWalletOverviewResponse
|
hostSalaryPolicyResp *walletv1.GetActiveHostSalaryPolicyResponse
|
||||||
overviewErr error
|
hostSalaryPolicyErr error
|
||||||
lastValueSummary *walletv1.GetWalletValueSummaryRequest
|
lastHostSalaryProgress *walletv1.GetHostSalaryProgressRequest
|
||||||
valueSummaryResp *walletv1.GetWalletValueSummaryResponse
|
hostSalaryProgressResp *walletv1.GetHostSalaryProgressResponse
|
||||||
valueSummaryErr error
|
hostSalaryProgressErr error
|
||||||
lastGiftWall *walletv1.GetUserGiftWallRequest
|
lastOverview *walletv1.GetWalletOverviewRequest
|
||||||
giftWallResp *walletv1.GetUserGiftWallResponse
|
overviewResp *walletv1.GetWalletOverviewResponse
|
||||||
lastRechargeProducts *walletv1.ListRechargeProductsRequest
|
overviewErr error
|
||||||
rechargeProductsResp *walletv1.ListRechargeProductsResponse
|
lastValueSummary *walletv1.GetWalletValueSummaryRequest
|
||||||
lastGoogleConfirm *walletv1.ConfirmGooglePaymentRequest
|
valueSummaryResp *walletv1.GetWalletValueSummaryResponse
|
||||||
googleConfirmResp *walletv1.ConfirmGooglePaymentResponse
|
valueSummaryErr error
|
||||||
lastDiamondExchange *walletv1.GetDiamondExchangeConfigRequest
|
lastGiftWall *walletv1.GetUserGiftWallRequest
|
||||||
diamondExchangeResp *walletv1.GetDiamondExchangeConfigResponse
|
giftWallResp *walletv1.GetUserGiftWallResponse
|
||||||
lastTransactions *walletv1.ListWalletTransactionsRequest
|
lastRechargeProducts *walletv1.ListRechargeProductsRequest
|
||||||
transactionsResp *walletv1.ListWalletTransactionsResponse
|
rechargeProductsResp *walletv1.ListRechargeProductsResponse
|
||||||
lastVipPackages *walletv1.ListVipPackagesRequest
|
lastGoogleConfirm *walletv1.ConfirmGooglePaymentRequest
|
||||||
vipPackagesResp *walletv1.ListVipPackagesResponse
|
googleConfirmResp *walletv1.ConfirmGooglePaymentResponse
|
||||||
lastMyVip *walletv1.GetMyVipRequest
|
lastDiamondExchange *walletv1.GetDiamondExchangeConfigRequest
|
||||||
myVipResp *walletv1.GetMyVipResponse
|
diamondExchangeResp *walletv1.GetDiamondExchangeConfigResponse
|
||||||
vipErr error
|
lastTransactions *walletv1.ListWalletTransactionsRequest
|
||||||
lastPurchaseVip *walletv1.PurchaseVipRequest
|
transactionsResp *walletv1.ListWalletTransactionsResponse
|
||||||
purchaseVipResp *walletv1.PurchaseVipResponse
|
lastVipPackages *walletv1.ListVipPackagesRequest
|
||||||
lastTransfer *walletv1.TransferCoinFromSellerRequest
|
vipPackagesResp *walletv1.ListVipPackagesResponse
|
||||||
transferResp *walletv1.TransferCoinFromSellerResponse
|
lastMyVip *walletv1.GetMyVipRequest
|
||||||
transferErr error
|
myVipResp *walletv1.GetMyVipResponse
|
||||||
lastListResources *walletv1.ListResourcesRequest
|
vipErr error
|
||||||
listResourcesResp *walletv1.ListResourcesResponse
|
lastPurchaseVip *walletv1.PurchaseVipRequest
|
||||||
lastGetResource *walletv1.GetResourceRequest
|
purchaseVipResp *walletv1.PurchaseVipResponse
|
||||||
resourcesByID map[int64]*walletv1.Resource
|
lastTransfer *walletv1.TransferCoinFromSellerRequest
|
||||||
lastGetResourceGroup *walletv1.GetResourceGroupRequest
|
transferResp *walletv1.TransferCoinFromSellerResponse
|
||||||
resourceGroupsByID map[int64]*walletv1.ResourceGroup
|
transferErr error
|
||||||
lastResourceShop *walletv1.ListResourceShopItemsRequest
|
lastListResources *walletv1.ListResourcesRequest
|
||||||
resourceShopResp *walletv1.ListResourceShopItemsResponse
|
listResourcesResp *walletv1.ListResourcesResponse
|
||||||
lastPurchaseShop *walletv1.PurchaseResourceShopItemRequest
|
lastGetResource *walletv1.GetResourceRequest
|
||||||
purchaseShopResp *walletv1.PurchaseResourceShopItemResponse
|
resourcesByID map[int64]*walletv1.Resource
|
||||||
lastListGiftConfigs *walletv1.ListGiftConfigsRequest
|
lastGetResourceGroup *walletv1.GetResourceGroupRequest
|
||||||
listGiftConfigsResp *walletv1.ListGiftConfigsResponse
|
resourceGroupsByID map[int64]*walletv1.ResourceGroup
|
||||||
lastListGiftTypes *walletv1.ListGiftTypeConfigsRequest
|
lastResourceShop *walletv1.ListResourceShopItemsRequest
|
||||||
listGiftTypesResp *walletv1.ListGiftTypeConfigsResponse
|
resourceShopResp *walletv1.ListResourceShopItemsResponse
|
||||||
lastGrantResource *walletv1.GrantResourceRequest
|
lastPurchaseShop *walletv1.PurchaseResourceShopItemRequest
|
||||||
grantResourceResp *walletv1.ResourceGrantResponse
|
purchaseShopResp *walletv1.PurchaseResourceShopItemResponse
|
||||||
grantResourceErr error
|
lastListGiftConfigs *walletv1.ListGiftConfigsRequest
|
||||||
lastBatchEquipped *walletv1.BatchGetUserEquippedResourcesRequest
|
listGiftConfigsResp *walletv1.ListGiftConfigsResponse
|
||||||
batchEquippedRequests []*walletv1.BatchGetUserEquippedResourcesRequest
|
lastListGiftTypes *walletv1.ListGiftTypeConfigsRequest
|
||||||
batchEquippedResp *walletv1.BatchGetUserEquippedResourcesResponse
|
listGiftTypesResp *walletv1.ListGiftTypeConfigsResponse
|
||||||
lastListRedPackets *walletv1.ListRedPacketsRequest
|
lastGrantResource *walletv1.GrantResourceRequest
|
||||||
listRedPacketsResp *walletv1.ListRedPacketsResponse
|
grantResourceResp *walletv1.ResourceGrantResponse
|
||||||
lastGetRedPacket *walletv1.GetRedPacketRequest
|
grantResourceErr error
|
||||||
getRedPacketResp *walletv1.GetRedPacketResponse
|
lastBatchEquipped *walletv1.BatchGetUserEquippedResourcesRequest
|
||||||
lastClaimRedPacket *walletv1.ClaimRedPacketRequest
|
batchEquippedRequests []*walletv1.BatchGetUserEquippedResourcesRequest
|
||||||
claimRedPacketResp *walletv1.ClaimRedPacketResponse
|
batchEquippedResp *walletv1.BatchGetUserEquippedResourcesResponse
|
||||||
|
lastListRedPackets *walletv1.ListRedPacketsRequest
|
||||||
|
listRedPacketsResp *walletv1.ListRedPacketsResponse
|
||||||
|
lastGetRedPacket *walletv1.GetRedPacketRequest
|
||||||
|
getRedPacketResp *walletv1.GetRedPacketResponse
|
||||||
|
lastClaimRedPacket *walletv1.ClaimRedPacketRequest
|
||||||
|
claimRedPacketResp *walletv1.ClaimRedPacketResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeMessageInboxClient struct {
|
type fakeMessageInboxClient struct {
|
||||||
@ -1212,6 +1224,28 @@ func (f *fakeWalletClient) GetBalances(_ context.Context, req *walletv1.GetBalan
|
|||||||
return &walletv1.GetBalancesResponse{}, nil
|
return &walletv1.GetBalancesResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeWalletClient) GetActiveHostSalaryPolicy(_ context.Context, req *walletv1.GetActiveHostSalaryPolicyRequest) (*walletv1.GetActiveHostSalaryPolicyResponse, error) {
|
||||||
|
f.lastHostSalaryPolicy = req
|
||||||
|
if f.hostSalaryPolicyErr != nil {
|
||||||
|
return nil, f.hostSalaryPolicyErr
|
||||||
|
}
|
||||||
|
if f.hostSalaryPolicyResp != nil {
|
||||||
|
return f.hostSalaryPolicyResp, nil
|
||||||
|
}
|
||||||
|
return &walletv1.GetActiveHostSalaryPolicyResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeWalletClient) GetHostSalaryProgress(_ context.Context, req *walletv1.GetHostSalaryProgressRequest) (*walletv1.GetHostSalaryProgressResponse, error) {
|
||||||
|
f.lastHostSalaryProgress = req
|
||||||
|
if f.hostSalaryProgressErr != nil {
|
||||||
|
return nil, f.hostSalaryProgressErr
|
||||||
|
}
|
||||||
|
if f.hostSalaryProgressResp != nil {
|
||||||
|
return f.hostSalaryProgressResp, nil
|
||||||
|
}
|
||||||
|
return &walletv1.GetHostSalaryProgressResponse{Progress: &walletv1.HostSalaryProgress{HostUserId: req.GetHostUserId()}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeWalletClient) GetWalletOverview(_ context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error) {
|
func (f *fakeWalletClient) GetWalletOverview(_ context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error) {
|
||||||
f.lastOverview = req
|
f.lastOverview = req
|
||||||
if f.overviewErr != nil {
|
if f.overviewErr != nil {
|
||||||
@ -1704,6 +1738,15 @@ func TestRoomCommandsPropagateClientCommandID(t *testing.T) {
|
|||||||
return client.lastConfirmMic.GetMeta()
|
return client.lastConfirmMic.GetMeta()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "mic_heartbeat",
|
||||||
|
path: "/api/v1/rooms/mic/heartbeat",
|
||||||
|
body: `{"room_id":"room-1","command_id":"cmd-mic-heartbeat-1","mic_session_id":"mic-1"}`,
|
||||||
|
commandID: "cmd-mic-heartbeat-1",
|
||||||
|
extractMeta: func(client *fakeRoomClient) *roomv1.RequestMeta {
|
||||||
|
return client.lastMicHeartbeat.GetMeta()
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "mic_lock",
|
name: "mic_lock",
|
||||||
path: "/api/v1/rooms/mic/lock",
|
path: "/api/v1/rooms/mic/lock",
|
||||||
@ -3263,6 +3306,165 @@ func TestApplyToHostAgencyPropagatesClientCommandID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHostCenterAgencyUsesCurrentHostAgencyAndOwnerProfile(t *testing.T) {
|
||||||
|
hostClient := &fakeUserHostClient{
|
||||||
|
hostProfile: &userv1.HostProfile{
|
||||||
|
UserId: 42,
|
||||||
|
Status: "active",
|
||||||
|
RegionId: 30,
|
||||||
|
CurrentAgencyId: 7001,
|
||||||
|
},
|
||||||
|
getAgencyResp: &userv1.GetAgencyResponse{Agency: &userv1.Agency{
|
||||||
|
AgencyId: 7001,
|
||||||
|
OwnerUserId: 99,
|
||||||
|
RegionId: 30,
|
||||||
|
Name: "Yumi Star Agency",
|
||||||
|
Status: "active",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
|
||||||
|
99: {UserId: 99, DisplayUserId: "163003", Username: "agency-owner", Avatar: "https://cdn.example/agency-owner.png"},
|
||||||
|
}}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||||
|
handler.SetUserHostClient(hostClient)
|
||||||
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/host-center/agency?agency_id=9999", nil)
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
request.Header.Set("X-Request-ID", "req-host-center-agency")
|
||||||
|
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.lastHost == nil || hostClient.lastHost.GetUserId() != 42 {
|
||||||
|
t.Fatalf("host profile request mismatch: %+v", hostClient.lastHost)
|
||||||
|
}
|
||||||
|
if hostClient.lastGetAgency == nil || hostClient.lastGetAgency.GetAgencyId() != 7001 {
|
||||||
|
t.Fatalf("agency request must use current host agency: %+v", hostClient.lastGetAgency)
|
||||||
|
}
|
||||||
|
if profileClient.lastBatch == nil || len(profileClient.lastBatch.GetUserIds()) != 1 || profileClient.lastBatch.GetUserIds()[0] != 99 {
|
||||||
|
t.Fatalf("owner profile request mismatch: %+v", profileClient.lastBatch)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
agency := data["agency"].(map[string]any)
|
||||||
|
if agency["agency_id"] != "7001" || agency["short_id"] != "163003" || agency["avatar"] != "https://cdn.example/agency-owner.png" || agency["name"] != "Yumi Star Agency" {
|
||||||
|
t.Fatalf("host center agency response mismatch: %+v", agency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostCenterPlatformPolicyUsesCurrentHostRegion(t *testing.T) {
|
||||||
|
hostClient := &fakeUserHostClient{hostProfile: &userv1.HostProfile{
|
||||||
|
UserId: 42,
|
||||||
|
Status: "active",
|
||||||
|
RegionId: 30,
|
||||||
|
CurrentAgencyId: 7001,
|
||||||
|
}}
|
||||||
|
walletClient := &fakeWalletClient{
|
||||||
|
hostSalaryPolicyResp: &walletv1.GetActiveHostSalaryPolicyResponse{
|
||||||
|
Found: true,
|
||||||
|
Policy: &walletv1.HostSalaryPolicy{
|
||||||
|
PolicyId: 9001,
|
||||||
|
Name: "Host Growth Policy",
|
||||||
|
RegionId: 30,
|
||||||
|
Status: "active",
|
||||||
|
SettlementMode: "half_month",
|
||||||
|
SettlementTriggerMode: "automatic",
|
||||||
|
GiftCoinToDiamondRatio: "1.0000",
|
||||||
|
ResidualDiamondToUsdRate: "0.0100",
|
||||||
|
EffectiveFromMs: 1700000000000,
|
||||||
|
Levels: []*walletv1.HostSalaryPolicyLevel{
|
||||||
|
{LevelNo: 1, RequiredDiamonds: 1000, HostSalaryUsdMinor: 1500, HostCoinReward: 200, AgencySalaryUsdMinor: 300, Status: "active", SortOrder: 1},
|
||||||
|
{LevelNo: 2, RequiredDiamonds: 3000, HostSalaryUsdMinor: 4500, HostCoinReward: 500, AgencySalaryUsdMinor: 900, Status: "active", SortOrder: 2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hostSalaryProgressResp: &walletv1.GetHostSalaryProgressResponse{Progress: &walletv1.HostSalaryProgress{
|
||||||
|
HostUserId: 42,
|
||||||
|
CycleKey: "2026-06",
|
||||||
|
RegionId: 30,
|
||||||
|
AgencyOwnerUserId: 99,
|
||||||
|
TotalDiamonds: 1200,
|
||||||
|
GiftDiamondTotal: 1200,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||||
|
handler.SetUserHostClient(hostClient)
|
||||||
|
handler.SetWalletClient(walletClient)
|
||||||
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/host-center/platform-policy", nil)
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
request.Header.Set("X-Request-ID", "req-host-policy")
|
||||||
|
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.lastHost == nil || hostClient.lastHost.GetUserId() != 42 {
|
||||||
|
t.Fatalf("host profile request mismatch: %+v", hostClient.lastHost)
|
||||||
|
}
|
||||||
|
if walletClient.lastHostSalaryPolicy == nil || walletClient.lastHostSalaryPolicy.GetRegionId() != 30 || walletClient.lastHostSalaryPolicy.GetSettlementTriggerMode() != "automatic" || walletClient.lastHostSalaryPolicy.GetAppCode() == "" || walletClient.lastHostSalaryPolicy.GetRequestId() == "" {
|
||||||
|
t.Fatalf("host salary policy request mismatch: %+v", walletClient.lastHostSalaryPolicy)
|
||||||
|
}
|
||||||
|
if walletClient.lastHostSalaryProgress == nil || walletClient.lastHostSalaryProgress.GetHostUserId() != 42 || walletClient.lastHostSalaryProgress.GetAppCode() == "" || walletClient.lastHostSalaryProgress.GetRequestId() == "" {
|
||||||
|
t.Fatalf("host salary progress request mismatch: %+v", walletClient.lastHostSalaryProgress)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
policy := data["policy"].(map[string]any)
|
||||||
|
levels := policy["levels"].([]any)
|
||||||
|
firstLevel := levels[0].(map[string]any)
|
||||||
|
progress := data["progress"].(map[string]any)
|
||||||
|
levelProgress := data["level_progress"].(map[string]any)
|
||||||
|
if data["found"] != true || data["host_region_id"] != float64(30) || policy["policy_id"] != "9001" || policy["settlement_mode"] != "half_month" || firstLevel["host_salary_usd"] != 15.0 || firstLevel["agency_salary_usd"] != 3.0 {
|
||||||
|
t.Fatalf("host policy response mismatch: %+v", data)
|
||||||
|
}
|
||||||
|
if progress["cycle_key"] != "2026-06" || progress["total_diamonds"] != float64(1200) {
|
||||||
|
t.Fatalf("host salary progress response mismatch: %+v", progress)
|
||||||
|
}
|
||||||
|
if levelProgress["current_level"] != float64(1) || levelProgress["current_value"] != float64(1200) || levelProgress["next_level"] != float64(2) || levelProgress["needed_for_next_level"] != float64(1800) {
|
||||||
|
t.Fatalf("host level progress response mismatch: %+v", levelProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostCenterPlatformPolicyRejectsNonActiveHost(t *testing.T) {
|
||||||
|
hostClient := &fakeUserHostClient{hostProfile: &userv1.HostProfile{
|
||||||
|
UserId: 42,
|
||||||
|
Status: "pending",
|
||||||
|
RegionId: 30,
|
||||||
|
}}
|
||||||
|
walletClient := &fakeWalletClient{}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||||
|
handler.SetUserHostClient(hostClient)
|
||||||
|
handler.SetWalletClient(walletClient)
|
||||||
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/host-center/platform-policy", 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())
|
||||||
|
}
|
||||||
|
if walletClient.lastHostSalaryPolicy != nil {
|
||||||
|
t.Fatalf("wallet should not be called for non-active host: %+v", walletClient.lastHostSalaryPolicy)
|
||||||
|
}
|
||||||
|
if walletClient.lastHostSalaryProgress != nil {
|
||||||
|
t.Fatalf("progress should not be called for non-active host: %+v", walletClient.lastHostSalaryProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAgencyCenterOverviewUsesOwnerAgencySalaryAndPendingApplications(t *testing.T) {
|
func TestAgencyCenterOverviewUsesOwnerAgencySalaryAndPendingApplications(t *testing.T) {
|
||||||
hostClient := &fakeUserHostClient{
|
hostClient := &fakeUserHostClient{
|
||||||
roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: true, AgencyId: 7001},
|
roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: true, AgencyId: 7001},
|
||||||
@ -3463,6 +3665,27 @@ func TestConfirmMicPublishingForwardsSessionVersionAndEventTime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMicHeartbeatForwardsCurrentSession(t *testing.T) {
|
||||||
|
client := &fakeRoomClient{}
|
||||||
|
router := NewHandler(client).Routes(auth.NewVerifier("secret"))
|
||||||
|
body := []byte(`{"room_id":"room-1","command_id":"cmd-mic-heartbeat","target_user_id":43,"mic_session_id":"mic-session-1"}`)
|
||||||
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/mic/heartbeat", bytes.NewReader(body))
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
request.Header.Set("X-Request-ID", "req-mic-heartbeat")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
assertEnvelope(t, recorder, http.StatusOK, httpkit.CodeOK, "req-mic-heartbeat")
|
||||||
|
if client.lastMicHeartbeat == nil {
|
||||||
|
t.Fatal("MicHeartbeat request was not sent")
|
||||||
|
}
|
||||||
|
if client.lastMicHeartbeat.GetTargetUserId() != 43 ||
|
||||||
|
client.lastMicHeartbeat.GetMicSessionId() != "mic-session-1" {
|
||||||
|
t.Fatalf("mic heartbeat fields mismatch: %+v", client.lastMicHeartbeat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestThirdPartyLoginUsesPublicEnvelopeAndPropagatesRequestID(t *testing.T) {
|
func TestThirdPartyLoginUsesPublicEnvelopeAndPropagatesRequestID(t *testing.T) {
|
||||||
userClient := &fakeUserAuthClient{}
|
userClient := &fakeUserAuthClient{}
|
||||||
router := NewHandler(&fakeRoomClient{}, userClient).Routes(auth.NewVerifier("secret"))
|
router := NewHandler(&fakeRoomClient{}, userClient).Routes(auth.NewVerifier("secret"))
|
||||||
@ -5335,6 +5558,7 @@ func TestProfileGateRejectsIncompleteUsersForRoomIMRTCPaidCapabilities(t *testin
|
|||||||
{name: "profile_update", method: http.MethodPost, path: "/api/v1/rooms/profile/update", body: `{"room_id":"room-1"}`},
|
{name: "profile_update", method: http.MethodPost, path: "/api/v1/rooms/profile/update", body: `{"room_id":"room-1"}`},
|
||||||
{name: "join_room", method: http.MethodPost, path: "/api/v1/rooms/join", body: `{"room_id":"room-1"}`},
|
{name: "join_room", method: http.MethodPost, path: "/api/v1/rooms/join", body: `{"room_id":"room-1"}`},
|
||||||
{name: "room_heartbeat", method: http.MethodPost, path: "/api/v1/rooms/heartbeat", body: `{"room_id":"room-1"}`},
|
{name: "room_heartbeat", method: http.MethodPost, path: "/api/v1/rooms/heartbeat", body: `{"room_id":"room-1"}`},
|
||||||
|
{name: "mic_heartbeat", method: http.MethodPost, path: "/api/v1/rooms/mic/heartbeat", body: `{"room_id":"room-1","mic_session_id":"mic-1"}`},
|
||||||
{name: "im_usersig", method: http.MethodGet, path: "/api/v1/im/usersig"},
|
{name: "im_usersig", method: http.MethodGet, path: "/api/v1/im/usersig"},
|
||||||
{name: "rtc_token", method: http.MethodPost, path: "/api/v1/rtc/token", body: `{"room_id":"room-1"}`},
|
{name: "rtc_token", method: http.MethodPost, path: "/api/v1/rtc/token", body: `{"room_id":"room-1"}`},
|
||||||
{name: "send_gift", method: http.MethodPost, path: "/api/v1/rooms/gift/send", body: `{"room_id":"room-1","gift_id":"rose"}`},
|
{name: "send_gift", method: http.MethodPost, path: "/api/v1/rooms/gift/send", body: `{"room_id":"room-1","gift_id":"rose"}`},
|
||||||
|
|||||||
@ -77,6 +77,7 @@ func (h *Handler) RoomHandlers() httproutes.RoomHandlers {
|
|||||||
MicDown: h.micDown,
|
MicDown: h.micDown,
|
||||||
ChangeMicSeat: h.changeMicSeat,
|
ChangeMicSeat: h.changeMicSeat,
|
||||||
ConfirmMicPublishing: h.confirmMicPublishing,
|
ConfirmMicPublishing: h.confirmMicPublishing,
|
||||||
|
MicHeartbeat: h.micHeartbeat,
|
||||||
SetMicMute: h.setMicMute,
|
SetMicMute: h.setMicMute,
|
||||||
SetMicSeatLock: h.setMicSeatLock,
|
SetMicSeatLock: h.setMicSeatLock,
|
||||||
SetChatEnabled: h.setChatEnabled,
|
SetChatEnabled: h.setChatEnabled,
|
||||||
|
|||||||
@ -1073,6 +1073,27 @@ func (h *Handler) confirmMicPublishing(writer http.ResponseWriter, request *http
|
|||||||
httpkit.Write(writer, request, resp, err)
|
httpkit.Write(writer, request, resp, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// micHeartbeat 把客户端麦上心跳转换为 room-service 当前 mic_session 刷新命令。
|
||||||
|
func (h *Handler) micHeartbeat(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
var body struct {
|
||||||
|
RoomID string `json:"room_id"`
|
||||||
|
CommandID string `json:"command_id"`
|
||||||
|
TargetUserID int64 `json:"target_user_id"`
|
||||||
|
MicSessionID string `json:"mic_session_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.roomClient.MicHeartbeat(request.Context(), &roomv1.MicHeartbeatRequest{
|
||||||
|
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||||
|
TargetUserId: body.TargetUserID,
|
||||||
|
MicSessionId: body.MicSessionID,
|
||||||
|
})
|
||||||
|
httpkit.Write(writer, request, resp, err)
|
||||||
|
}
|
||||||
|
|
||||||
// setMicMute 把麦克风静音 HTTP 请求转换为 room-service SetMicMute 命令。
|
// setMicMute 把麦克风静音 HTTP 请求转换为 room-service SetMicMute 命令。
|
||||||
func (h *Handler) setMicMute(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) setMicMute(writer http.ResponseWriter, request *http.Request) {
|
||||||
var body struct {
|
var body struct {
|
||||||
|
|||||||
@ -332,6 +332,7 @@ type roomSeatData struct {
|
|||||||
PublishDeadlineMS int64 `json:"publish_deadline_ms,omitempty"`
|
PublishDeadlineMS int64 `json:"publish_deadline_ms,omitempty"`
|
||||||
MicMuted bool `json:"mic_muted"`
|
MicMuted bool `json:"mic_muted"`
|
||||||
SeatStatus string `json:"seat_status,omitempty"`
|
SeatStatus string `json:"seat_status,omitempty"`
|
||||||
|
MicHeartbeatAtMS int64 `json:"mic_heartbeat_at_ms,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type roomRankItemData struct {
|
type roomRankItemData struct {
|
||||||
@ -653,6 +654,7 @@ func roomSeatDataFromSnapshot(snapshot *roomv1.RoomSnapshot) []roomSeatData {
|
|||||||
PublishDeadlineMS: seat.GetPublishDeadlineMs(),
|
PublishDeadlineMS: seat.GetPublishDeadlineMs(),
|
||||||
MicMuted: seat.GetMicMuted(),
|
MicMuted: seat.GetMicMuted(),
|
||||||
SeatStatus: seat.GetSeatStatus(),
|
SeatStatus: seat.GetSeatStatus(),
|
||||||
|
MicHeartbeatAtMS: seat.GetMicHeartbeatAtMs(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
|
|||||||
@ -56,6 +56,8 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
|||||||
GetMyRoleSummary: h.getMyRoleSummary,
|
GetMyRoleSummary: h.getMyRoleSummary,
|
||||||
SearchHostAgencies: h.searchHostAgencies,
|
SearchHostAgencies: h.searchHostAgencies,
|
||||||
ApplyToHostAgency: h.applyToHostAgency,
|
ApplyToHostAgency: h.applyToHostAgency,
|
||||||
|
GetHostCenterAgency: h.getHostCenterAgency,
|
||||||
|
GetHostCenterPlatformPolicy: h.getHostCenterPlatformPolicy,
|
||||||
GetAgencyCenterOverview: h.getAgencyCenterOverview,
|
GetAgencyCenterOverview: h.getAgencyCenterOverview,
|
||||||
ListAgencyCenterHosts: h.listAgencyCenterHosts,
|
ListAgencyCenterHosts: h.listAgencyCenterHosts,
|
||||||
ListAgencyCenterApplications: h.listAgencyCenterApplications,
|
ListAgencyCenterApplications: h.listAgencyCenterApplications,
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
package userapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hostCenterAgencyData struct {
|
||||||
|
AgencyID string `json:"agency_id"`
|
||||||
|
ShortID string `json:"short_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) getHostCenterAgency(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
profile, ok := h.resolveCurrentActiveHostProfile(writer, request)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if profile.GetCurrentAgencyId() <= 0 {
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{"agency": nil})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.userHostClient == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前主播所属 agency 只能从 user-service 的 host_profile 快照读取,前端不能传任意 agency_id 查看。
|
||||||
|
agencyResp, err := h.userHostClient.GetAgency(request.Context(), &userv1.GetAgencyRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
AgencyId: profile.GetCurrentAgencyId(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
agency := agencyResp.GetAgency()
|
||||||
|
if agency == nil || agency.GetAgencyId() != profile.GetCurrentAgencyId() || agency.GetStatus() != agencyStatusActive {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := hostCenterAgencyData{
|
||||||
|
AgencyID: int64String(agency.GetAgencyId()),
|
||||||
|
ShortID: int64String(agency.GetAgencyId()),
|
||||||
|
Name: agency.GetName(),
|
||||||
|
Avatar: agency.GetAvatar(),
|
||||||
|
}
|
||||||
|
if agency.GetOwnerUserId() > 0 {
|
||||||
|
profiles, err := h.userProfiles(request, []int64{agency.GetOwnerUserId()})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if owner, exists := profiles[agency.GetOwnerUserId()]; exists {
|
||||||
|
if owner.DisplayUserID != "" {
|
||||||
|
data.ShortID = owner.DisplayUserID
|
||||||
|
}
|
||||||
|
if owner.Avatar != "" {
|
||||||
|
data.Avatar = owner.Avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{"agency": data})
|
||||||
|
}
|
||||||
@ -0,0 +1,219 @@
|
|||||||
|
package userapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
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 hostProfileStatusActive = "active"
|
||||||
|
|
||||||
|
type hostCenterPolicyData struct {
|
||||||
|
PolicyID string `json:"policy_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
RegionID int64 `json:"region_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
SettlementMode string `json:"settlement_mode"`
|
||||||
|
SettlementTriggerMode string `json:"settlement_trigger_mode"`
|
||||||
|
GiftCoinToDiamondRatio string `json:"gift_coin_to_diamond_ratio"`
|
||||||
|
ResidualDiamondToUSDRate string `json:"residual_diamond_to_usd_rate"`
|
||||||
|
EffectiveFromMS int64 `json:"effective_from_ms"`
|
||||||
|
EffectiveToMS int64 `json:"effective_to_ms"`
|
||||||
|
Levels []hostCenterPolicyLevelData `json:"levels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hostCenterPolicyLevelData struct {
|
||||||
|
LevelNo int32 `json:"level_no"`
|
||||||
|
RequiredDiamonds int64 `json:"required_diamonds"`
|
||||||
|
HostSalaryUSDMinor int64 `json:"host_salary_usd_minor"`
|
||||||
|
HostSalaryUSD float64 `json:"host_salary_usd"`
|
||||||
|
HostCoinReward int64 `json:"host_coin_reward"`
|
||||||
|
AgencySalaryUSDMinor int64 `json:"agency_salary_usd_minor"`
|
||||||
|
AgencySalaryUSD float64 `json:"agency_salary_usd"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hostCenterSalaryProgressData struct {
|
||||||
|
HostUserID int64 `json:"host_user_id"`
|
||||||
|
CycleKey string `json:"cycle_key"`
|
||||||
|
RegionID int64 `json:"region_id"`
|
||||||
|
AgencyOwnerUserID int64 `json:"agency_owner_user_id"`
|
||||||
|
TotalDiamonds int64 `json:"total_diamonds"`
|
||||||
|
GiftDiamondTotal int64 `json:"gift_diamond_total"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hostCenterLevelProgressData struct {
|
||||||
|
CurrentLevel int32 `json:"current_level"`
|
||||||
|
CurrentRequiredDiamonds int64 `json:"current_required_diamonds"`
|
||||||
|
CurrentValue int64 `json:"current_value"`
|
||||||
|
NextLevel int32 `json:"next_level"`
|
||||||
|
NextRequiredDiamonds int64 `json:"next_required_diamonds"`
|
||||||
|
NeededForNextLevel int64 `json:"needed_for_next_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) getHostCenterPlatformPolicy(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
profile, ok := h.resolveCurrentActiveHostProfile(writer, request)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.walletClient == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// H5 不传 region 或 policy id;gateway 固定使用当前主播身份里的区域快照读取实际生效政策。
|
||||||
|
resp, err := h.walletClient.GetActiveHostSalaryPolicy(request.Context(), &walletv1.GetActiveHostSalaryPolicyRequest{
|
||||||
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
|
RegionId: profile.GetRegionId(),
|
||||||
|
SettlementTriggerMode: "automatic",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等级进度必须按工资周期钻石账户计算,不能使用用户成长等级,否则“距离下一级”会和后台工资政策不一致。
|
||||||
|
progressResp, err := h.walletClient.GetHostSalaryProgress(request.Context(), &walletv1.GetHostSalaryProgressRequest{
|
||||||
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
|
HostUserId: profile.GetUserId(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
progress := progressResp.GetProgress()
|
||||||
|
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
|
"found": resp.GetFound(),
|
||||||
|
"host_region_id": profile.GetRegionId(),
|
||||||
|
"policy": hostCenterPolicyFromProto(resp.GetPolicy()),
|
||||||
|
"progress": hostCenterSalaryProgressFromProto(progress),
|
||||||
|
"level_progress": hostCenterLevelProgressFromPolicy(resp.GetPolicy(), progress.GetTotalDiamonds()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) resolveCurrentActiveHostProfile(writer http.ResponseWriter, request *http.Request) (*userv1.HostProfile, bool) {
|
||||||
|
if h.userHostClient == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
|
resp, err := h.userHostClient.GetHostProfile(request.Context(), &userv1.GetHostProfileRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
UserId: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
profile := resp.GetHostProfile()
|
||||||
|
if profile == nil || profile.GetUserId() != userID || profile.GetStatus() != hostProfileStatusActive || profile.GetRegionId() <= 0 {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return profile, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostCenterPolicyFromProto(policy *walletv1.HostSalaryPolicy) *hostCenterPolicyData {
|
||||||
|
if policy == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
levels := make([]hostCenterPolicyLevelData, 0, len(policy.GetLevels()))
|
||||||
|
for _, level := range policy.GetLevels() {
|
||||||
|
levels = append(levels, hostCenterPolicyLevelFromProto(level))
|
||||||
|
}
|
||||||
|
return &hostCenterPolicyData{
|
||||||
|
PolicyID: strconv.FormatUint(policy.GetPolicyId(), 10),
|
||||||
|
Name: policy.GetName(),
|
||||||
|
RegionID: policy.GetRegionId(),
|
||||||
|
Status: policy.GetStatus(),
|
||||||
|
SettlementMode: policy.GetSettlementMode(),
|
||||||
|
SettlementTriggerMode: policy.GetSettlementTriggerMode(),
|
||||||
|
GiftCoinToDiamondRatio: policy.GetGiftCoinToDiamondRatio(),
|
||||||
|
ResidualDiamondToUSDRate: policy.GetResidualDiamondToUsdRate(),
|
||||||
|
EffectiveFromMS: policy.GetEffectiveFromMs(),
|
||||||
|
EffectiveToMS: policy.GetEffectiveToMs(),
|
||||||
|
Levels: levels,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostCenterPolicyLevelFromProto(level *walletv1.HostSalaryPolicyLevel) hostCenterPolicyLevelData {
|
||||||
|
if level == nil {
|
||||||
|
return hostCenterPolicyLevelData{}
|
||||||
|
}
|
||||||
|
return hostCenterPolicyLevelData{
|
||||||
|
LevelNo: level.GetLevelNo(),
|
||||||
|
RequiredDiamonds: level.GetRequiredDiamonds(),
|
||||||
|
HostSalaryUSDMinor: level.GetHostSalaryUsdMinor(),
|
||||||
|
HostSalaryUSD: float64(level.GetHostSalaryUsdMinor()) / 100,
|
||||||
|
HostCoinReward: level.GetHostCoinReward(),
|
||||||
|
AgencySalaryUSDMinor: level.GetAgencySalaryUsdMinor(),
|
||||||
|
AgencySalaryUSD: float64(level.GetAgencySalaryUsdMinor()) / 100,
|
||||||
|
Status: level.GetStatus(),
|
||||||
|
SortOrder: level.GetSortOrder(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostCenterSalaryProgressFromProto(progress *walletv1.HostSalaryProgress) hostCenterSalaryProgressData {
|
||||||
|
if progress == nil {
|
||||||
|
return hostCenterSalaryProgressData{}
|
||||||
|
}
|
||||||
|
return hostCenterSalaryProgressData{
|
||||||
|
HostUserID: progress.GetHostUserId(),
|
||||||
|
CycleKey: progress.GetCycleKey(),
|
||||||
|
RegionID: progress.GetRegionId(),
|
||||||
|
AgencyOwnerUserID: progress.GetAgencyOwnerUserId(),
|
||||||
|
TotalDiamonds: progress.GetTotalDiamonds(),
|
||||||
|
GiftDiamondTotal: progress.GetGiftDiamondTotal(),
|
||||||
|
UpdatedAtMS: progress.GetUpdatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostCenterLevelProgressFromPolicy(policy *walletv1.HostSalaryPolicy, currentValue int64) *hostCenterLevelProgressData {
|
||||||
|
levels := policy.GetLevels()
|
||||||
|
if len(levels) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var current *walletv1.HostSalaryPolicyLevel
|
||||||
|
var next *walletv1.HostSalaryPolicyLevel
|
||||||
|
for _, level := range levels {
|
||||||
|
if level == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
required := level.GetRequiredDiamonds()
|
||||||
|
if required <= currentValue {
|
||||||
|
if current == nil || required > current.GetRequiredDiamonds() {
|
||||||
|
current = level
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if next == nil || required < next.GetRequiredDiamonds() {
|
||||||
|
next = level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := &hostCenterLevelProgressData{
|
||||||
|
CurrentValue: currentValue,
|
||||||
|
}
|
||||||
|
if current != nil {
|
||||||
|
result.CurrentLevel = current.GetLevelNo()
|
||||||
|
result.CurrentRequiredDiamonds = current.GetRequiredDiamonds()
|
||||||
|
}
|
||||||
|
if next != nil {
|
||||||
|
result.NextLevel = next.GetLevelNo()
|
||||||
|
result.NextRequiredDiamonds = next.GetRequiredDiamonds()
|
||||||
|
result.NeededForNextLevel = next.GetRequiredDiamonds() - currentValue
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.NextLevel = result.CurrentLevel
|
||||||
|
result.NextRequiredDiamonds = result.CurrentRequiredDiamonds
|
||||||
|
return result
|
||||||
|
}
|
||||||
@ -212,14 +212,12 @@ func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.Room
|
|||||||
base.Attributes = map[string]string{"enabled": fmt.Sprintf("%t", body.GetEnabled())}
|
base.Attributes = map[string]string{"enabled": fmt.Sprintf("%t", body.GetEnabled())}
|
||||||
return base, true, nil
|
return base, true, nil
|
||||||
case "RoomPasswordChanged":
|
case "RoomPasswordChanged":
|
||||||
// 锁房事件只广播布尔状态,密码明文和哈希都不会进入 IM 自定义消息。
|
// 锁房事件改由 activity-service 按 visible_region_id 生成区域通知;房间群不再重复投递。
|
||||||
var body roomeventsv1.RoomPasswordChanged
|
var body roomeventsv1.RoomPasswordChanged
|
||||||
if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil {
|
if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil {
|
||||||
return tencentim.RoomEvent{}, false, err
|
return tencentim.RoomEvent{}, false, err
|
||||||
}
|
}
|
||||||
base.ActorUserID = body.GetActorUserId()
|
return tencentim.RoomEvent{}, false, nil
|
||||||
base.Attributes = map[string]string{"locked": fmt.Sprintf("%t", body.GetLocked())}
|
|
||||||
return base, true, nil
|
|
||||||
case "RoomAdminChanged":
|
case "RoomAdminChanged":
|
||||||
// 管理员变更进入系统消息,客户端收到后仍应以最新 snapshot 修正本地权限 UI。
|
// 管理员变更进入系统消息,客户端收到后仍应以最新 snapshot 修正本地权限 UI。
|
||||||
var body roomeventsv1.RoomAdminChanged
|
var body roomeventsv1.RoomAdminChanged
|
||||||
|
|||||||
@ -87,6 +87,25 @@ func TestRoomBackgroundChangedPublishesDedicatedIMEvent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRoomPasswordChangedSkipsRoomGroupIM(t *testing.T) {
|
||||||
|
record, err := outbox.Build("room-lock", "RoomPasswordChanged", 8, time.Now(), &roomeventsv1.RoomPasswordChanged{
|
||||||
|
ActorUserId: 42,
|
||||||
|
Locked: true,
|
||||||
|
VisibleRegionId: 86,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build RoomPasswordChanged envelope failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
event, publish, err := roomEventFromEnvelope(record.Envelope)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RoomPasswordChanged should decode: %v", err)
|
||||||
|
}
|
||||||
|
if publish {
|
||||||
|
t.Fatalf("RoomPasswordChanged must use activity region broadcast, not room group IM: %+v", event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTencentIMPublisherRemovesGroupMemberBeforeKickMessage(t *testing.T) {
|
func TestTencentIMPublisherRemovesGroupMemberBeforeKickMessage(t *testing.T) {
|
||||||
paths := make([]string, 0, 2)
|
paths := make([]string, 0, 2)
|
||||||
client, err := tencentim.NewRESTClient(tencentim.RESTConfig{
|
client, err := tencentim.NewRESTClient(tencentim.RESTConfig{
|
||||||
|
|||||||
@ -225,6 +225,18 @@ type ConfirmMicPublishing struct {
|
|||||||
// Type 返回命令类型。
|
// Type 返回命令类型。
|
||||||
func (ConfirmMicPublishing) Type() string { return "confirm_mic_publishing" }
|
func (ConfirmMicPublishing) Type() string { return "confirm_mic_publishing" }
|
||||||
|
|
||||||
|
// MicHeartbeat 定义当前麦位会话的服务端心跳刷新请求。
|
||||||
|
type MicHeartbeat struct {
|
||||||
|
Base
|
||||||
|
// TargetUserID 是被刷新麦上心跳的用户;为空时服务层使用 ActorUserID。
|
||||||
|
TargetUserID int64 `json:"target_user_id"`
|
||||||
|
// MicSessionID 必须匹配当前麦位会话,避免旧客户端心跳续住新麦位。
|
||||||
|
MicSessionID string `json:"mic_session_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type 返回命令类型。
|
||||||
|
func (MicHeartbeat) Type() string { return "mic_heartbeat" }
|
||||||
|
|
||||||
// SetMicMute 定义麦克风静音状态同步请求。
|
// SetMicMute 定义麦克风静音状态同步请求。
|
||||||
type SetMicMute struct {
|
type SetMicMute struct {
|
||||||
Base
|
Base
|
||||||
@ -563,6 +575,8 @@ func Deserialize(commandType string, payload []byte) (Command, error) {
|
|||||||
cmd = &MicDown{}
|
cmd = &MicDown{}
|
||||||
case ConfirmMicPublishing{}.Type():
|
case ConfirmMicPublishing{}.Type():
|
||||||
cmd = &ConfirmMicPublishing{}
|
cmd = &ConfirmMicPublishing{}
|
||||||
|
case MicHeartbeat{}.Type():
|
||||||
|
cmd = &MicHeartbeat{}
|
||||||
case SetMicMute{}.Type():
|
case SetMicMute{}.Type():
|
||||||
cmd = &SetMicMute{}
|
cmd = &SetMicMute{}
|
||||||
case ApplyRTCEvent{}.Type():
|
case ApplyRTCEvent{}.Type():
|
||||||
|
|||||||
@ -102,4 +102,26 @@ func TestRoomPasswordControlsJoinAndListLockedFlag(t *testing.T) {
|
|||||||
if joinResp.GetUser().GetUserId() != viewerID || !joinResp.GetRoom().GetLocked() {
|
if joinResp.GetUser().GetUserId() != viewerID || !joinResp.GetRoom().GetLocked() {
|
||||||
t.Fatalf("join response mismatch: user=%+v room=%+v", joinResp.GetUser(), joinResp.GetRoom())
|
t.Fatalf("join response mismatch: user=%+v room=%+v", joinResp.GetUser(), joinResp.GetRoom())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := svc.SetRoomPassword(ctx, &roomv1.SetRoomPasswordRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
||||||
|
Locked: true,
|
||||||
|
Password: "5678",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("change room password after viewer joined failed: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
||||||
|
Role: "audience",
|
||||||
|
Password: "1234",
|
||||||
|
}); err == nil {
|
||||||
|
t.Fatalf("existing online user must not bypass changed room password")
|
||||||
|
}
|
||||||
|
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
||||||
|
Role: "audience",
|
||||||
|
Password: "5678",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("existing online user should reconnect with latest room password: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,7 @@ func (s *Service) MicUp(ctx context.Context, req *roomv1.MicUpRequest) (*roomv1.
|
|||||||
// 新 session 尚未接受任何 RTC/客户端事件;不能用服务端 MicUp 时间初始化,
|
// 新 session 尚未接受任何 RTC/客户端事件;不能用服务端 MicUp 时间初始化,
|
||||||
// 否则客户端设备时间略慢时,合法的首次确认会被误判为旧事件。
|
// 否则客户端设备时间略慢时,合法的首次确认会被误判为旧事件。
|
||||||
current.MicSeats[index].LastPublishEventTimeMS = 0
|
current.MicSeats[index].LastPublishEventTimeMS = 0
|
||||||
|
current.MicSeats[index].MicHeartbeatAtMS = 0
|
||||||
current.MicSeats[index].MicMuted = false
|
current.MicSeats[index].MicMuted = false
|
||||||
seatStatus := current.MicSeats[index].SeatStatus()
|
seatStatus := current.MicSeats[index].SeatStatus()
|
||||||
|
|
||||||
@ -283,6 +284,7 @@ func (s *Service) ChangeMicSeat(ctx context.Context, req *roomv1.ChangeMicSeatRe
|
|||||||
current.MicSeats[fromIndex].PublishDeadlineMS = 0
|
current.MicSeats[fromIndex].PublishDeadlineMS = 0
|
||||||
current.MicSeats[fromIndex].MicSessionRoomVersion = 0
|
current.MicSeats[fromIndex].MicSessionRoomVersion = 0
|
||||||
current.MicSeats[fromIndex].LastPublishEventTimeMS = 0
|
current.MicSeats[fromIndex].LastPublishEventTimeMS = 0
|
||||||
|
current.MicSeats[fromIndex].MicHeartbeatAtMS = 0
|
||||||
current.MicSeats[fromIndex].MicMuted = false
|
current.MicSeats[fromIndex].MicMuted = false
|
||||||
current.MicSeats[toIndex].UserID = cmd.TargetUserID
|
current.MicSeats[toIndex].UserID = cmd.TargetUserID
|
||||||
current.MicSeats[toIndex].PublishState = movedSeat.PublishState
|
current.MicSeats[toIndex].PublishState = movedSeat.PublishState
|
||||||
@ -290,6 +292,7 @@ func (s *Service) ChangeMicSeat(ctx context.Context, req *roomv1.ChangeMicSeatRe
|
|||||||
current.MicSeats[toIndex].PublishDeadlineMS = movedSeat.PublishDeadlineMS
|
current.MicSeats[toIndex].PublishDeadlineMS = movedSeat.PublishDeadlineMS
|
||||||
current.MicSeats[toIndex].MicSessionRoomVersion = movedSeat.MicSessionRoomVersion
|
current.MicSeats[toIndex].MicSessionRoomVersion = movedSeat.MicSessionRoomVersion
|
||||||
current.MicSeats[toIndex].LastPublishEventTimeMS = movedSeat.LastPublishEventTimeMS
|
current.MicSeats[toIndex].LastPublishEventTimeMS = movedSeat.LastPublishEventTimeMS
|
||||||
|
current.MicSeats[toIndex].MicHeartbeatAtMS = movedSeat.MicHeartbeatAtMS
|
||||||
current.MicSeats[toIndex].MicMuted = movedSeat.MicMuted
|
current.MicSeats[toIndex].MicMuted = movedSeat.MicMuted
|
||||||
current.Version++
|
current.Version++
|
||||||
seatStatus := current.MicSeats[toIndex].SeatStatus()
|
seatStatus := current.MicSeats[toIndex].SeatStatus()
|
||||||
@ -391,6 +394,8 @@ func (s *Service) ConfirmMicPublishing(ctx context.Context, req *roomv1.ConfirmM
|
|||||||
index := current.SeatIndex(seat.SeatNo)
|
index := current.SeatIndex(seat.SeatNo)
|
||||||
current.MicSeats[index].PublishState = state.MicPublishPublishing
|
current.MicSeats[index].PublishState = state.MicPublishPublishing
|
||||||
current.MicSeats[index].LastPublishEventTimeMS = cmd.EventTimeMS
|
current.MicSeats[index].LastPublishEventTimeMS = cmd.EventTimeMS
|
||||||
|
// 确认发流本身代表当前会话刚被服务端接受,先初始化心跳时间,后续 MicHeartbeat 只负责刷新这个独立字段。
|
||||||
|
current.MicSeats[index].MicHeartbeatAtMS = now.UnixMilli()
|
||||||
if micPublishConfirmSourceIsMuted(cmd.Source) {
|
if micPublishConfirmSourceIsMuted(cmd.Source) {
|
||||||
current.MicSeats[index].MicMuted = true
|
current.MicSeats[index].MicMuted = true
|
||||||
}
|
}
|
||||||
@ -419,6 +424,7 @@ func (s *Service) ConfirmMicPublishing(ctx context.Context, req *roomv1.ConfirmM
|
|||||||
seatNo: seat.SeatNo,
|
seatNo: seat.SeatNo,
|
||||||
micSessionID: cmd.MicSessionID,
|
micSessionID: cmd.MicSessionID,
|
||||||
publishDeadlineMS: seat.PublishDeadlineMS,
|
publishDeadlineMS: seat.PublishDeadlineMS,
|
||||||
|
micHeartbeatAtMS: current.MicSeats[index].MicHeartbeatAtMS,
|
||||||
syncEvent: &tencentim.RoomEvent{
|
syncEvent: &tencentim.RoomEvent{
|
||||||
EventID: micEvent.EventID,
|
EventID: micEvent.EventID,
|
||||||
RoomID: current.RoomID,
|
RoomID: current.RoomID,
|
||||||
@ -450,6 +456,77 @@ func (s *Service) ConfirmMicPublishing(ctx context.Context, req *roomv1.ConfirmM
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MicHeartbeat 刷新当前 publishing 麦位会话的服务端心跳时间。
|
||||||
|
func (s *Service) MicHeartbeat(ctx context.Context, req *roomv1.MicHeartbeatRequest) (*roomv1.MicHeartbeatResponse, error) {
|
||||||
|
ctx = contextFromMeta(ctx, req.GetMeta())
|
||||||
|
cmd := command.MicHeartbeat{
|
||||||
|
Base: baseFromMeta(req.GetMeta()),
|
||||||
|
TargetUserID: req.GetTargetUserId(),
|
||||||
|
MicSessionID: req.GetMicSessionId(),
|
||||||
|
}
|
||||||
|
if cmd.TargetUserID == 0 {
|
||||||
|
cmd.TargetUserID = cmd.ActorUserID()
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
||||||
|
if err := requireActiveRoom(current); err != nil {
|
||||||
|
return mutationResult{}, nil, err
|
||||||
|
}
|
||||||
|
if cmd.TargetUserID <= 0 || cmd.MicSessionID == "" {
|
||||||
|
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "mic heartbeat is incomplete")
|
||||||
|
}
|
||||||
|
if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
|
||||||
|
return mutationResult{}, nil, err
|
||||||
|
}
|
||||||
|
if cmd.ActorUserID() != cmd.TargetUserID {
|
||||||
|
if err := canManageTarget(current, cmd.ActorUserID(), cmd.TargetUserID); err != nil {
|
||||||
|
return mutationResult{}, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seat, exists := current.SeatByUser(cmd.TargetUserID)
|
||||||
|
if !exists {
|
||||||
|
return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not on seat")
|
||||||
|
}
|
||||||
|
if seat.MicSessionID != cmd.MicSessionID {
|
||||||
|
// 旧客户端或旧 RTC 会话的心跳不能续住新麦位会话。
|
||||||
|
return mutationResult{}, nil, xerr.New(xerr.Conflict, "mic session mismatch")
|
||||||
|
}
|
||||||
|
if seat.PublishState != state.MicPublishPublishing {
|
||||||
|
// pending_publish 还没有有效发流,只能先走 ConfirmMicPublishing,不能靠心跳开始计时。
|
||||||
|
return mutationResult{}, nil, xerr.New(xerr.Conflict, "mic is not publishing")
|
||||||
|
}
|
||||||
|
|
||||||
|
heartbeatAtMS := now.UnixMilli()
|
||||||
|
if existing, ok := current.OnlineUsers[cmd.TargetUserID]; ok {
|
||||||
|
// 麦上心跳也证明用户仍处于房间页,顺手刷新通用 presence,避免客户端同时打两个心跳。
|
||||||
|
existing.LastSeenAtMS = heartbeatAtMS
|
||||||
|
}
|
||||||
|
index := current.SeatIndex(seat.SeatNo)
|
||||||
|
current.MicSeats[index].MicHeartbeatAtMS = heartbeatAtMS
|
||||||
|
current.Version++
|
||||||
|
|
||||||
|
return mutationResult{
|
||||||
|
snapshot: current.ToProto(),
|
||||||
|
seatNo: seat.SeatNo,
|
||||||
|
micHeartbeatAtMS: heartbeatAtMS,
|
||||||
|
}, nil, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result.micHeartbeatAtMS == 0 {
|
||||||
|
result.seatNo, result.micHeartbeatAtMS = micHeartbeatFromSnapshot(result.snapshot, cmd.TargetUserID, cmd.MicSessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &roomv1.MicHeartbeatResponse{
|
||||||
|
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||||||
|
SeatNo: result.seatNo,
|
||||||
|
Room: result.snapshot,
|
||||||
|
MicHeartbeatAtMs: result.micHeartbeatAtMS,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetMicMute 修改麦位上的服务端可见静音态。
|
// SetMicMute 修改麦位上的服务端可见静音态。
|
||||||
func (s *Service) SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error) {
|
func (s *Service) SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error) {
|
||||||
ctx = contextFromMeta(ctx, req.GetMeta())
|
ctx = contextFromMeta(ctx, req.GetMeta())
|
||||||
@ -555,6 +632,15 @@ func micPublishConfirmSourceIsMuted(source string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func micHeartbeatFromSnapshot(snapshot *roomv1.RoomSnapshot, userID int64, micSessionID string) (int32, int64) {
|
||||||
|
for _, seat := range snapshot.GetMicSeats() {
|
||||||
|
if seat.GetUserId() == userID && seat.GetMicSessionId() == micSessionID {
|
||||||
|
return seat.GetSeatNo(), seat.GetMicHeartbeatAtMs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
func protoSeatByUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.SeatState {
|
func protoSeatByUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.SeatState {
|
||||||
if snapshot == nil {
|
if snapshot == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
112
services/room-service/internal/room/service/mic_test.go
Normal file
112
services/room-service/internal/room/service/mic_test.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package service_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
|
"hyapp/services/room-service/internal/testutil/mysqltest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMicHeartbeatRefreshesPublishingSessionAndPresence(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
now := &fixedRoomTreasureClock{now: time.Date(2026, 6, 4, 8, 0, 0, 0, time.UTC)}
|
||||||
|
svc := newTreasureTestService(t, repository, &treasureTestWallet{}, now)
|
||||||
|
|
||||||
|
roomID := "room-mic-heartbeat"
|
||||||
|
ownerID := int64(8601)
|
||||||
|
speakerID := int64(8602)
|
||||||
|
createTreasureRoom(t, ctx, svc, roomID, ownerID, 9101)
|
||||||
|
joinTreasureRoom(t, ctx, svc, roomID, speakerID)
|
||||||
|
|
||||||
|
upResp, err := svc.MicUp(ctx, &roomv1.MicUpRequest{
|
||||||
|
Meta: treasureMeta(roomID, speakerID, "mic-heartbeat-up"),
|
||||||
|
SeatNo: 2,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mic up failed: %v", err)
|
||||||
|
}
|
||||||
|
now.now = now.now.Add(500 * time.Millisecond)
|
||||||
|
confirmResp, err := svc.ConfirmMicPublishing(ctx, &roomv1.ConfirmMicPublishingRequest{
|
||||||
|
Meta: treasureMeta(roomID, speakerID, "mic-heartbeat-confirm"),
|
||||||
|
MicSessionId: upResp.GetMicSessionId(),
|
||||||
|
RoomVersion: upResp.GetRoom().GetVersion(),
|
||||||
|
EventTimeMs: now.Now().UnixMilli(),
|
||||||
|
Source: "client",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("confirm mic publishing failed: %v", err)
|
||||||
|
}
|
||||||
|
confirmedSeat := seatByNo(confirmResp.GetRoom(), 2)
|
||||||
|
if confirmedSeat == nil || confirmedSeat.GetPublishState() != "publishing" || confirmedSeat.GetMicHeartbeatAtMs() == 0 {
|
||||||
|
t.Fatalf("publish confirmation must initialize heartbeat: %+v", confirmedSeat)
|
||||||
|
}
|
||||||
|
|
||||||
|
now.now = now.now.Add(3 * time.Second)
|
||||||
|
heartbeatAtMS := now.Now().UnixMilli()
|
||||||
|
heartbeatResp, err := svc.MicHeartbeat(ctx, &roomv1.MicHeartbeatRequest{
|
||||||
|
Meta: treasureMeta(roomID, speakerID, "mic-heartbeat-refresh"),
|
||||||
|
MicSessionId: upResp.GetMicSessionId(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mic heartbeat failed: %v", err)
|
||||||
|
}
|
||||||
|
if heartbeatResp.GetSeatNo() != 2 || heartbeatResp.GetMicHeartbeatAtMs() != heartbeatAtMS {
|
||||||
|
t.Fatalf("heartbeat response mismatch: %+v want_at=%d", heartbeatResp, heartbeatAtMS)
|
||||||
|
}
|
||||||
|
seat := seatByNo(heartbeatResp.GetRoom(), 2)
|
||||||
|
if seat == nil || seat.GetMicHeartbeatAtMs() != heartbeatAtMS {
|
||||||
|
t.Fatalf("seat heartbeat must be visible in snapshot: %+v", seat)
|
||||||
|
}
|
||||||
|
user := onlineUserByID(heartbeatResp.GetRoom(), speakerID)
|
||||||
|
if user == nil || user.GetLastSeenAtMs() != heartbeatAtMS {
|
||||||
|
t.Fatalf("mic heartbeat must refresh room presence: %+v", user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMicHeartbeatRejectsPendingSession(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
now := &fixedRoomTreasureClock{now: time.Date(2026, 6, 4, 8, 0, 0, 0, time.UTC)}
|
||||||
|
svc := newTreasureTestService(t, repository, &treasureTestWallet{}, now)
|
||||||
|
|
||||||
|
roomID := "room-mic-heartbeat-pending"
|
||||||
|
ownerID := int64(8701)
|
||||||
|
speakerID := int64(8702)
|
||||||
|
createTreasureRoom(t, ctx, svc, roomID, ownerID, 9101)
|
||||||
|
joinTreasureRoom(t, ctx, svc, roomID, speakerID)
|
||||||
|
|
||||||
|
upResp, err := svc.MicUp(ctx, &roomv1.MicUpRequest{
|
||||||
|
Meta: treasureMeta(roomID, speakerID, "mic-heartbeat-pending-up"),
|
||||||
|
SeatNo: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mic up failed: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := svc.MicHeartbeat(ctx, &roomv1.MicHeartbeatRequest{
|
||||||
|
Meta: treasureMeta(roomID, speakerID, "mic-heartbeat-pending-refresh"),
|
||||||
|
MicSessionId: upResp.GetMicSessionId(),
|
||||||
|
}); err == nil {
|
||||||
|
t.Fatalf("pending_publish session must not accept mic heartbeat before publish confirmation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func seatByNo(snapshot *roomv1.RoomSnapshot, seatNo int32) *roomv1.SeatState {
|
||||||
|
for _, seat := range snapshot.GetMicSeats() {
|
||||||
|
if seat.GetSeatNo() == seatNo {
|
||||||
|
return seat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func onlineUserByID(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.RoomUser {
|
||||||
|
for _, user := range snapshot.GetOnlineUsers() {
|
||||||
|
if user.GetUserId() == userID {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -49,6 +49,11 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
|
|||||||
delete(current.BanUsers, cmd.ActorUserID())
|
delete(current.BanUsers, cmd.ActorUserID())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if current.RoomPasswordHash != "" && current.OwnerUserID != cmd.ActorUserID() && !roomPasswordMatches(current.RoomPasswordHash, password) {
|
||||||
|
// 锁房校验必须覆盖新进房和已有 presence 的重连;否则房主改密后,悬浮保活或 stale 未清理用户会绕过最新密码。
|
||||||
|
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "room password is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
if existing, exists := current.OnlineUsers[cmd.ActorUserID()]; exists {
|
if existing, exists := current.OnlineUsers[cmd.ActorUserID()]; exists {
|
||||||
// 重复 JoinRoom 表示重连或刷新业务 presence,只更新 last_seen,不重复发进房系统消息。
|
// 重复 JoinRoom 表示重连或刷新业务 presence,只更新 last_seen,不重复发进房系统消息。
|
||||||
existing.LastSeenAtMS = now.UnixMilli()
|
existing.LastSeenAtMS = now.UnixMilli()
|
||||||
@ -60,11 +65,6 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
|
|||||||
user: findProtoUser(snapshot, cmd.ActorUserID()),
|
user: findProtoUser(snapshot, cmd.ActorUserID()),
|
||||||
}, nil, nil
|
}, nil, nil
|
||||||
}
|
}
|
||||||
if current.RoomPasswordHash != "" && current.OwnerUserID != cmd.ActorUserID() && !roomPasswordMatches(current.RoomPasswordHash, password) {
|
|
||||||
// 锁房只拦截新进入的非房主用户;已在房内的用户和 owner 不因为锁房状态重设被挤出。
|
|
||||||
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "room password is invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
current.OnlineUsers[cmd.ActorUserID()] = &state.RoomUserState{
|
current.OnlineUsers[cmd.ActorUserID()] = &state.RoomUserState{
|
||||||
UserID: cmd.ActorUserID(),
|
UserID: cmd.ActorUserID(),
|
||||||
Role: cmd.Role,
|
Role: cmd.Role,
|
||||||
|
|||||||
@ -228,6 +228,22 @@ func replay(current *state.RoomState, cmd command.Command, committedAtMS int64)
|
|||||||
index := current.SeatIndex(seat.SeatNo)
|
index := current.SeatIndex(seat.SeatNo)
|
||||||
current.MicSeats[index].PublishState = state.MicPublishPublishing
|
current.MicSeats[index].PublishState = state.MicPublishPublishing
|
||||||
current.MicSeats[index].LastPublishEventTimeMS = typed.EventTimeMS
|
current.MicSeats[index].LastPublishEventTimeMS = typed.EventTimeMS
|
||||||
|
current.MicSeats[index].MicHeartbeatAtMS = typed.SentAtMS
|
||||||
|
current.Version++
|
||||||
|
case *command.MicHeartbeat:
|
||||||
|
seat, exists := current.SeatByUser(typed.TargetUserID)
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("mic_heartbeat replay target is not on seat: %d", typed.TargetUserID)
|
||||||
|
}
|
||||||
|
if seat.MicSessionID != typed.MicSessionID || seat.PublishState != state.MicPublishPublishing {
|
||||||
|
return fmt.Errorf("mic_heartbeat replay target session is not publishing: %d", typed.TargetUserID)
|
||||||
|
}
|
||||||
|
if existing, ok := current.OnlineUsers[typed.TargetUserID]; ok {
|
||||||
|
// 麦上心跳回放要恢复同步刷新的 presence 时间,避免 snapshot 落后时重启后又被 stale worker 清理。
|
||||||
|
existing.LastSeenAtMS = typed.SentAtMS
|
||||||
|
}
|
||||||
|
index := current.SeatIndex(seat.SeatNo)
|
||||||
|
current.MicSeats[index].MicHeartbeatAtMS = typed.SentAtMS
|
||||||
current.Version++
|
current.Version++
|
||||||
case *command.ApplyRTCEvent:
|
case *command.ApplyRTCEvent:
|
||||||
switch typed.EventType {
|
switch typed.EventType {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
"hyapp/pkg/tencentim"
|
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
"hyapp/services/room-service/internal/room/command"
|
"hyapp/services/room-service/internal/room/command"
|
||||||
"hyapp/services/room-service/internal/room/outbox"
|
"hyapp/services/room-service/internal/room/outbox"
|
||||||
@ -61,8 +60,9 @@ func (s *Service) SetRoomPassword(ctx context.Context, req *roomv1.SetRoomPasswo
|
|||||||
current.Version++
|
current.Version++
|
||||||
currentHash := current.RoomPasswordHash
|
currentHash := current.RoomPasswordHash
|
||||||
passwordEvent, err := outbox.Build(current.RoomID, "RoomPasswordChanged", current.Version, now, &roomeventsv1.RoomPasswordChanged{
|
passwordEvent, err := outbox.Build(current.RoomID, "RoomPasswordChanged", current.Version, now, &roomeventsv1.RoomPasswordChanged{
|
||||||
ActorUserId: cmd.ActorUserID(),
|
ActorUserId: cmd.ActorUserID(),
|
||||||
Locked: cmd.Locked,
|
Locked: cmd.Locked,
|
||||||
|
VisibleRegionId: current.VisibleRegionID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mutationResult{}, nil, err
|
return mutationResult{}, nil, err
|
||||||
@ -71,16 +71,6 @@ func (s *Service) SetRoomPassword(ctx context.Context, req *roomv1.SetRoomPasswo
|
|||||||
return mutationResult{
|
return mutationResult{
|
||||||
snapshot: current.ToProto(),
|
snapshot: current.ToProto(),
|
||||||
roomPasswordHash: ¤tHash,
|
roomPasswordHash: ¤tHash,
|
||||||
syncEvent: &tencentim.RoomEvent{
|
|
||||||
EventID: passwordEvent.EventID,
|
|
||||||
RoomID: current.RoomID,
|
|
||||||
EventType: "room_password_changed",
|
|
||||||
ActorUserID: cmd.ActorUserID(),
|
|
||||||
RoomVersion: current.Version,
|
|
||||||
Attributes: map[string]string{
|
|
||||||
"locked": boolString(cmd.Locked),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, []outbox.Record{passwordEvent}, nil
|
}, []outbox.Record{passwordEvent}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -107,6 +107,7 @@ func (s *Service) applyRTCAudioStarted(now time.Time, current *state.RoomState,
|
|||||||
index := current.SeatIndex(seat.SeatNo)
|
index := current.SeatIndex(seat.SeatNo)
|
||||||
current.MicSeats[index].PublishState = state.MicPublishPublishing
|
current.MicSeats[index].PublishState = state.MicPublishPublishing
|
||||||
current.MicSeats[index].LastPublishEventTimeMS = cmd.EventTimeMS
|
current.MicSeats[index].LastPublishEventTimeMS = cmd.EventTimeMS
|
||||||
|
current.MicSeats[index].MicHeartbeatAtMS = now.UnixMilli()
|
||||||
current.Version++
|
current.Version++
|
||||||
seatStatus := current.MicSeats[index].SeatStatus()
|
seatStatus := current.MicSeats[index].SeatStatus()
|
||||||
|
|
||||||
|
|||||||
@ -105,6 +105,8 @@ type mutationResult struct {
|
|||||||
micSessionID string
|
micSessionID string
|
||||||
// publishDeadlineMS 是 pending_publish 自动释放的截止时间。
|
// publishDeadlineMS 是 pending_publish 自动释放的截止时间。
|
||||||
publishDeadlineMS int64
|
publishDeadlineMS int64
|
||||||
|
// micHeartbeatAtMS 是服务端接受当前麦上心跳的时间,只属于麦位活跃探测,不改变时长口径。
|
||||||
|
micHeartbeatAtMS int64
|
||||||
// billingReceiptID 是 SendGift 从 wallet-service 得到的账务回执。
|
// billingReceiptID 是 SendGift 从 wallet-service 得到的账务回执。
|
||||||
billingReceiptID string
|
billingReceiptID string
|
||||||
// roomHeat 是 SendGift 后的房间热度。
|
// roomHeat 是 SendGift 后的房间热度。
|
||||||
|
|||||||
@ -62,6 +62,8 @@ type MicSeat struct {
|
|||||||
MicSessionRoomVersion int64
|
MicSessionRoomVersion int64
|
||||||
// LastPublishEventTimeMS 是已接受的最新 RTC/客户端发布事件时间。
|
// LastPublishEventTimeMS 是已接受的最新 RTC/客户端发布事件时间。
|
||||||
LastPublishEventTimeMS int64
|
LastPublishEventTimeMS int64
|
||||||
|
// MicHeartbeatAtMS 是当前 mic_session 最近一次服务端接受麦上心跳的时间;它不参与 RTC 事件新旧判断。
|
||||||
|
MicHeartbeatAtMS int64
|
||||||
// MicMuted 是服务端可见的麦克风静音状态,用于同步其他用户 UI。
|
// MicMuted 是服务端可见的麦克风静音状态,用于同步其他用户 UI。
|
||||||
MicMuted bool
|
MicMuted bool
|
||||||
}
|
}
|
||||||
@ -379,6 +381,7 @@ func (s *RoomState) ToProto() *roomv1.RoomSnapshot {
|
|||||||
LastPublishEventTimeMs: seat.LastPublishEventTimeMS,
|
LastPublishEventTimeMs: seat.LastPublishEventTimeMS,
|
||||||
MicMuted: seat.MicMuted,
|
MicMuted: seat.MicMuted,
|
||||||
SeatStatus: seat.SeatStatus(),
|
SeatStatus: seat.SeatStatus(),
|
||||||
|
MicHeartbeatAtMs: seat.MicHeartbeatAtMS,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,6 +458,7 @@ func FromProto(snapshot *roomv1.RoomSnapshot) *RoomState {
|
|||||||
PublishDeadlineMS: seat.GetPublishDeadlineMs(),
|
PublishDeadlineMS: seat.GetPublishDeadlineMs(),
|
||||||
MicSessionRoomVersion: seat.GetMicSessionRoomVersion(),
|
MicSessionRoomVersion: seat.GetMicSessionRoomVersion(),
|
||||||
LastPublishEventTimeMS: seat.GetLastPublishEventTimeMs(),
|
LastPublishEventTimeMS: seat.GetLastPublishEventTimeMs(),
|
||||||
|
MicHeartbeatAtMS: seat.GetMicHeartbeatAtMs(),
|
||||||
MicMuted: seat.GetMicMuted(),
|
MicMuted: seat.GetMicMuted(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -235,6 +235,18 @@ func (s *Server) ConfirmMicPublishing(ctx context.Context, req *roomv1.ConfirmMi
|
|||||||
return mapServiceResult(s.svc.ConfirmMicPublishing(ctx, req))
|
return mapServiceResult(s.svc.ConfirmMicPublishing(ctx, req))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MicHeartbeat 代理到领域服务。
|
||||||
|
func (s *Server) MicHeartbeat(ctx context.Context, req *roomv1.MicHeartbeatRequest) (*roomv1.MicHeartbeatResponse, error) {
|
||||||
|
// 心跳仍由 Room Cell 串行提交,保证 presence、麦位快照和 command log 同版本。
|
||||||
|
ctx = contextWithMetaApp(ctx, req.GetMeta())
|
||||||
|
if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.MicHeartbeatResponse, error) {
|
||||||
|
return client.MicHeartbeat(callCtx, req)
|
||||||
|
}); forwarded {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
return mapServiceResult(s.svc.MicHeartbeat(ctx, req))
|
||||||
|
}
|
||||||
|
|
||||||
// SetMicMute 代理到领域服务。
|
// SetMicMute 代理到领域服务。
|
||||||
func (s *Server) SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error) {
|
func (s *Server) SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest) (*roomv1.SetMicMuteResponse, error) {
|
||||||
// 服务端可见静音态必须经 Room Cell 提交,确保其他用户 UI 和系统消息一致。
|
// 服务端可见静音态必须经 Room Cell 提交,确保其他用户 UI 和系统消息一致。
|
||||||
|
|||||||
@ -227,6 +227,17 @@ type HostSalaryPolicyLevel struct {
|
|||||||
SortOrder int
|
SortOrder int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HostSalaryProgress 是主播当前工资周期的钻石累计投影,用于 H5 按工资政策计算距离下一等级的差值。
|
||||||
|
type HostSalaryProgress struct {
|
||||||
|
HostUserID int64
|
||||||
|
CycleKey string
|
||||||
|
RegionID int64
|
||||||
|
AgencyOwnerUserID int64
|
||||||
|
TotalDiamonds int64
|
||||||
|
GiftDiamondTotal int64
|
||||||
|
UpdatedAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
// CoinSellerTransferCommand 是币商给玩家转普通金币的账务命令。
|
// CoinSellerTransferCommand 是币商给玩家转普通金币的账务命令。
|
||||||
type CoinSellerTransferCommand struct {
|
type CoinSellerTransferCommand struct {
|
||||||
AppCode string
|
AppCode string
|
||||||
|
|||||||
@ -21,6 +21,8 @@ type Repository interface {
|
|||||||
DebitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error)
|
DebitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error)
|
||||||
BatchDebitGift(ctx context.Context, command ledger.BatchDebitGiftCommand) (ledger.BatchGiftReceipt, error)
|
BatchDebitGift(ctx context.Context, command ledger.BatchDebitGiftCommand) (ledger.BatchGiftReceipt, error)
|
||||||
GetBalances(ctx context.Context, userID int64, assetTypes []string) ([]ledger.AssetBalance, error)
|
GetBalances(ctx context.Context, userID int64, assetTypes []string) ([]ledger.AssetBalance, error)
|
||||||
|
GetActiveHostSalaryPolicy(ctx context.Context, regionID int64, settlementMode string, triggerMode string, nowMs int64) (ledger.HostSalaryPolicy, bool, error)
|
||||||
|
GetHostSalaryProgress(ctx context.Context, userID int64, cycleKey string) (ledger.HostSalaryProgress, error)
|
||||||
AdminCreditAsset(ctx context.Context, command ledger.AdminCreditAssetCommand) (ledger.AssetBalance, string, error)
|
AdminCreditAsset(ctx context.Context, command ledger.AdminCreditAssetCommand) (ledger.AssetBalance, string, error)
|
||||||
AdminCreditCoinSellerStock(ctx context.Context, command ledger.CoinSellerStockCreditCommand) (ledger.CoinSellerStockCreditReceipt, error)
|
AdminCreditCoinSellerStock(ctx context.Context, command ledger.CoinSellerStockCreditCommand) (ledger.CoinSellerStockCreditReceipt, error)
|
||||||
TransferCoinFromSeller(ctx context.Context, command ledger.CoinSellerTransferCommand) (ledger.CoinSellerTransferReceipt, error)
|
TransferCoinFromSeller(ctx context.Context, command ledger.CoinSellerTransferCommand) (ledger.CoinSellerTransferReceipt, error)
|
||||||
@ -238,6 +240,54 @@ func (s *Service) GetBalances(ctx context.Context, userID int64, assetTypes []st
|
|||||||
return s.repository.GetBalances(ctx, userID, normalized)
|
return s.repository.GetBalances(ctx, userID, normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetActiveHostSalaryPolicy 读取主播所在区域当前生效的工资政策,返回的 policy 与工资结算使用同一张 runtime 表。
|
||||||
|
func (s *Service) GetActiveHostSalaryPolicy(ctx context.Context, appCode string, regionID int64, settlementMode string, triggerMode string, nowMs int64) (ledger.HostSalaryPolicy, bool, error) {
|
||||||
|
if regionID <= 0 {
|
||||||
|
return ledger.HostSalaryPolicy{}, false, xerr.New(xerr.InvalidArgument, "region_id is required")
|
||||||
|
}
|
||||||
|
if s.repository == nil {
|
||||||
|
return ledger.HostSalaryPolicy{}, false, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||||
|
}
|
||||||
|
settlementMode = strings.TrimSpace(settlementMode)
|
||||||
|
if settlementMode != "" && settlementMode != ledger.HostSalarySettlementModeDaily && settlementMode != ledger.HostSalarySettlementModeHalfMonth {
|
||||||
|
return ledger.HostSalaryPolicy{}, false, xerr.New(xerr.InvalidArgument, "settlement_mode is invalid")
|
||||||
|
}
|
||||||
|
triggerMode = strings.TrimSpace(triggerMode)
|
||||||
|
if triggerMode == "" {
|
||||||
|
triggerMode = ledger.HostSalarySettlementTriggerAutomatic
|
||||||
|
}
|
||||||
|
if triggerMode != ledger.HostSalarySettlementTriggerAutomatic && triggerMode != ledger.HostSalarySettlementTriggerManual {
|
||||||
|
return ledger.HostSalaryPolicy{}, false, xerr.New(xerr.InvalidArgument, "settlement_trigger_mode is invalid")
|
||||||
|
}
|
||||||
|
if nowMs <= 0 {
|
||||||
|
nowMs = s.now().UTC().UnixMilli()
|
||||||
|
}
|
||||||
|
appCode = appcode.Normalize(appCode)
|
||||||
|
ctx = appcode.WithContext(ctx, appCode)
|
||||||
|
// 不传 settlement_mode 时按同区域最新 active policy 返回,用于 H5 展示平台当前规则。
|
||||||
|
return s.repository.GetActiveHostSalaryPolicy(ctx, regionID, settlementMode, triggerMode, nowMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHostSalaryProgress 返回主播当前工资周期的钻石累计;没有收礼账户时返回 0,避免 H5 用其他等级系统兜底导致进度口径错误。
|
||||||
|
func (s *Service) GetHostSalaryProgress(ctx context.Context, appCode string, userID int64, cycleKey string, nowMs int64) (ledger.HostSalaryProgress, error) {
|
||||||
|
if userID <= 0 {
|
||||||
|
return ledger.HostSalaryProgress{}, xerr.New(xerr.InvalidArgument, "host_user_id is required")
|
||||||
|
}
|
||||||
|
if s.repository == nil {
|
||||||
|
return ledger.HostSalaryProgress{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||||
|
}
|
||||||
|
if nowMs <= 0 {
|
||||||
|
nowMs = s.now().UTC().UnixMilli()
|
||||||
|
}
|
||||||
|
cycleKey = strings.TrimSpace(cycleKey)
|
||||||
|
if cycleKey == "" {
|
||||||
|
cycleKey = hostSalaryCycleKey(nowMs)
|
||||||
|
}
|
||||||
|
appCode = appcode.Normalize(appCode)
|
||||||
|
ctx = appcode.WithContext(ctx, appCode)
|
||||||
|
return s.repository.GetHostSalaryProgress(ctx, userID, cycleKey)
|
||||||
|
}
|
||||||
|
|
||||||
// AdminCreditAsset 执行后台手动调账;amount 为正数入账、负数扣账,审计字段必须随交易一起落库。
|
// AdminCreditAsset 执行后台手动调账;amount 为正数入账、负数扣账,审计字段必须随交易一起落库。
|
||||||
func (s *Service) AdminCreditAsset(ctx context.Context, command ledger.AdminCreditAssetCommand) (ledger.AssetBalance, string, error) {
|
func (s *Service) AdminCreditAsset(ctx context.Context, command ledger.AdminCreditAssetCommand) (ledger.AssetBalance, string, error) {
|
||||||
if command.CommandID == "" || command.TargetUserID <= 0 || command.OperatorUserID <= 0 || command.Amount == 0 {
|
if command.CommandID == "" || command.TargetUserID <= 0 || command.OperatorUserID <= 0 || command.Amount == 0 {
|
||||||
|
|||||||
@ -287,6 +287,84 @@ func TestDebitGiftRejectsHostPeriodWithoutRegion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGetActiveHostSalaryPolicyReadsRuntimePolicy 验证 H5 展示读取的是工资结算 runtime 表里的生效政策和档位。
|
||||||
|
func TestGetActiveHostSalaryPolicyReadsRuntimePolicy(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
nowMs := time.Now().UnixMilli()
|
||||||
|
repository.SetHostSalaryPolicy(ledger.HostSalaryPolicy{
|
||||||
|
PolicyID: 901,
|
||||||
|
Name: "current host policy",
|
||||||
|
RegionID: 8801,
|
||||||
|
Status: "active",
|
||||||
|
SettlementMode: ledger.HostSalarySettlementModeHalfMonth,
|
||||||
|
SettlementTriggerMode: ledger.HostSalarySettlementTriggerAutomatic,
|
||||||
|
GiftCoinToDiamondRatio: "1.0000",
|
||||||
|
ResidualDiamondToUSDRate: "0.0100",
|
||||||
|
EffectiveFromMs: nowMs - 1000,
|
||||||
|
EffectiveToMs: 0,
|
||||||
|
Levels: []ledger.HostSalaryPolicyLevel{
|
||||||
|
{LevelNo: 1, RequiredDiamonds: 1000, HostSalaryUSDMinor: 1500, HostCoinReward: 200, AgencySalaryUSDMinor: 300, SortOrder: 1},
|
||||||
|
{LevelNo: 2, RequiredDiamonds: 3000, HostSalaryUSDMinor: 4500, HostCoinReward: 500, AgencySalaryUSDMinor: 900, SortOrder: 2},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
|
||||||
|
policy, found, err := svc.GetActiveHostSalaryPolicy(context.Background(), "lalu", 8801, "", ledger.HostSalarySettlementTriggerAutomatic, nowMs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetActiveHostSalaryPolicy failed: %v", err)
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatal("expected active host salary policy")
|
||||||
|
}
|
||||||
|
if policy.PolicyID != 901 || policy.Name != "current host policy" || policy.SettlementMode != ledger.HostSalarySettlementModeHalfMonth || policy.RegionID != 8801 {
|
||||||
|
t.Fatalf("policy mismatch: %+v", policy)
|
||||||
|
}
|
||||||
|
if len(policy.Levels) != 2 || policy.Levels[1].LevelNo != 2 || policy.Levels[1].HostSalaryUSDMinor != 4500 || policy.Levels[1].AgencySalaryUSDMinor != 900 {
|
||||||
|
t.Fatalf("policy levels mismatch: %+v", policy.Levels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetHostSalaryProgressReadsCurrentCycleDiamonds 验证 H5 等级进度读取工资周期钻石账户,查询不到时保持 0 累计。
|
||||||
|
func TestGetHostSalaryProgressReadsCurrentCycleDiamonds(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
repository.SetBalance(10001, 100)
|
||||||
|
repository.SetGiftPrice("rose", "salary-progress", 200, 10, 10)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
|
||||||
|
receipt, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{
|
||||||
|
CommandID: "cmd-salary-progress",
|
||||||
|
RoomID: "room-1",
|
||||||
|
SenderUserID: 10001,
|
||||||
|
TargetUserID: 13002,
|
||||||
|
GiftID: "rose",
|
||||||
|
GiftCount: 1,
|
||||||
|
PriceVersion: "salary-progress",
|
||||||
|
SenderRegionID: 8801,
|
||||||
|
TargetIsHost: true,
|
||||||
|
TargetHostRegionID: 8801,
|
||||||
|
TargetAgencyOwnerUserID: 30001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("salary gift failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
progress, err := svc.GetHostSalaryProgress(context.Background(), "lalu", 13002, receipt.HostPeriodCycleKey, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetHostSalaryProgress failed: %v", err)
|
||||||
|
}
|
||||||
|
if progress.HostUserID != 13002 || progress.CycleKey != receipt.HostPeriodCycleKey || progress.RegionID != 8801 || progress.AgencyOwnerUserID != 30001 || progress.TotalDiamonds != 14 || progress.GiftDiamondTotal != 14 {
|
||||||
|
t.Fatalf("host salary progress mismatch: %+v", progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
missing, err := svc.GetHostSalaryProgress(context.Background(), "lalu", 99999, "2026-06", 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetHostSalaryProgress missing account failed: %v", err)
|
||||||
|
}
|
||||||
|
if missing.HostUserID != 99999 || missing.CycleKey != "2026-06" || missing.TotalDiamonds != 0 {
|
||||||
|
t.Fatalf("missing progress must return zero account: %+v", missing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestHostSalarySettlementCreditsIncrementalRewards 走真实送礼入钻石账户后,验证日结只发放等级累计差额。
|
// TestHostSalarySettlementCreditsIncrementalRewards 走真实送礼入钻石账户后,验证日结只发放等级累计差额。
|
||||||
func TestHostSalarySettlementCreditsIncrementalRewards(t *testing.T) {
|
func TestHostSalarySettlementCreditsIncrementalRewards(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
|
|||||||
@ -68,6 +68,11 @@ type hostSalarySettlementMetadata struct {
|
|||||||
ProcessedAtMS int64 `json:"processed_at_ms"`
|
ProcessedAtMS int64 `json:"processed_at_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hostSalaryPolicyQuerier interface {
|
||||||
|
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
|
||||||
|
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessHostSalarySettlementBatch 按周期账户扫描并结算主播美元、主播金币奖励和代理美元工资。
|
// ProcessHostSalarySettlementBatch 按周期账户扫描并结算主播美元、主播金币奖励和代理美元工资。
|
||||||
// 每个主播单独开事务,避免某个主播配置异常拖垮整个批次,也让 cron-service 可按 has_more 继续拉下一页。
|
// 每个主播单独开事务,避免某个主播配置异常拖垮整个批次,也让 cron-service 可按 has_more 继续拉下一页。
|
||||||
func (r *Repository) ProcessHostSalarySettlementBatch(ctx context.Context, command ledger.HostSalarySettlementBatchCommand) (ledger.HostSalarySettlementBatchResult, error) {
|
func (r *Repository) ProcessHostSalarySettlementBatch(ctx context.Context, command ledger.HostSalarySettlementBatchCommand) (ledger.HostSalarySettlementBatchResult, error) {
|
||||||
@ -345,13 +350,53 @@ func (r *Repository) lockHostSalaryProgress(ctx context.Context, tx *sql.Tx, use
|
|||||||
return progress, nil
|
return progress, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetActiveHostSalaryPolicy 返回当前 app、区域和触发模式下正在生效的主播工资政策;这是 H5 展示平台政策的只读入口。
|
||||||
|
func (r *Repository) GetActiveHostSalaryPolicy(ctx context.Context, regionID int64, settlementMode string, triggerMode string, nowMs int64) (ledger.HostSalaryPolicy, bool, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return ledger.HostSalaryPolicy{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
return r.queryActiveHostSalaryPolicy(ctx, r.db, regionID, settlementMode, triggerMode, nowMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHostSalaryProgress 读取主播工资周期钻石账户;没有任何收礼时返回空累计,前端据此展示距离首档还差多少钻石。
|
||||||
|
func (r *Repository) GetHostSalaryProgress(ctx context.Context, userID int64, cycleKey string) (ledger.HostSalaryProgress, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return ledger.HostSalaryProgress{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
progress := ledger.HostSalaryProgress{
|
||||||
|
HostUserID: userID,
|
||||||
|
CycleKey: cycleKey,
|
||||||
|
}
|
||||||
|
row := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT user_id, cycle_key, region_id, agency_owner_user_id, total_diamonds,
|
||||||
|
gift_diamond_total, updated_at_ms
|
||||||
|
FROM host_period_diamond_accounts
|
||||||
|
WHERE app_code = ? AND user_id = ? AND cycle_key = ?`,
|
||||||
|
appcode.FromContext(ctx), userID, cycleKey)
|
||||||
|
if err := row.Scan(&progress.HostUserID, &progress.CycleKey, &progress.RegionID, &progress.AgencyOwnerUserID, &progress.TotalDiamonds, &progress.GiftDiamondTotal, &progress.UpdatedAtMS); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return progress, nil
|
||||||
|
}
|
||||||
|
return ledger.HostSalaryProgress{}, err
|
||||||
|
}
|
||||||
|
return progress, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) resolveHostSalaryPolicy(ctx context.Context, tx *sql.Tx, regionID int64, settlementType string, triggerMode string, nowMs int64) (ledger.HostSalaryPolicy, bool, error) {
|
func (r *Repository) resolveHostSalaryPolicy(ctx context.Context, tx *sql.Tx, regionID int64, settlementType string, triggerMode string, nowMs int64) (ledger.HostSalaryPolicy, bool, error) {
|
||||||
|
settlementMode := ""
|
||||||
|
if settlementType != ledger.HostSalarySettlementTypeMonthEnd {
|
||||||
|
settlementMode = settlementType
|
||||||
|
}
|
||||||
|
return r.queryActiveHostSalaryPolicy(ctx, tx, regionID, settlementMode, triggerMode, nowMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) queryActiveHostSalaryPolicy(ctx context.Context, q hostSalaryPolicyQuerier, regionID int64, settlementMode string, triggerMode string, nowMs int64) (ledger.HostSalaryPolicy, bool, error) {
|
||||||
modeClause := ""
|
modeClause := ""
|
||||||
args := []any{appcode.FromContext(ctx), regionID, nowMs, nowMs, triggerMode}
|
args := []any{appcode.FromContext(ctx), regionID, nowMs, nowMs, triggerMode}
|
||||||
if settlementType != ledger.HostSalarySettlementTypeMonthEnd {
|
if strings.TrimSpace(settlementMode) != "" {
|
||||||
// 人工/自动触发模式也参与政策匹配,避免自动任务误结算只允许手动处理的政策。
|
// 结算任务必须精确匹配结算模式;H5 展示不传模式时只按区域和触发模式取最新生效政策。
|
||||||
modeClause = "AND settlement_mode = ?"
|
modeClause = "AND settlement_mode = ?"
|
||||||
args = append(args, settlementType)
|
args = append(args, settlementMode)
|
||||||
}
|
}
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
SELECT policy_id, name, region_id, status, settlement_mode, settlement_trigger_mode,
|
SELECT policy_id, name, region_id, status, settlement_mode, settlement_trigger_mode,
|
||||||
@ -369,14 +414,14 @@ func (r *Repository) resolveHostSalaryPolicy(ctx context.Context, tx *sql.Tx, re
|
|||||||
ORDER BY effective_from_ms DESC, policy_id DESC
|
ORDER BY effective_from_ms DESC, policy_id DESC
|
||||||
LIMIT 1`, modeClause)
|
LIMIT 1`, modeClause)
|
||||||
var policy ledger.HostSalaryPolicy
|
var policy ledger.HostSalaryPolicy
|
||||||
err := tx.QueryRowContext(ctx, query, args...).Scan(&policy.PolicyID, &policy.Name, &policy.RegionID, &policy.Status, &policy.SettlementMode, &policy.SettlementTriggerMode, &policy.GiftCoinToDiamondRatio, &policy.ResidualDiamondToUSDRate, &policy.EffectiveFromMs, &policy.EffectiveToMs)
|
err := q.QueryRowContext(ctx, query, args...).Scan(&policy.PolicyID, &policy.Name, &policy.RegionID, &policy.Status, &policy.SettlementMode, &policy.SettlementTriggerMode, &policy.GiftCoinToDiamondRatio, &policy.ResidualDiamondToUSDRate, &policy.EffectiveFromMs, &policy.EffectiveToMs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return ledger.HostSalaryPolicy{}, false, nil
|
return ledger.HostSalaryPolicy{}, false, nil
|
||||||
}
|
}
|
||||||
return ledger.HostSalaryPolicy{}, false, err
|
return ledger.HostSalaryPolicy{}, false, err
|
||||||
}
|
}
|
||||||
levels, err := r.listHostSalaryPolicyLevels(ctx, tx, policy.PolicyID)
|
levels, err := r.listHostSalaryPolicyLevels(ctx, q, policy.PolicyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ledger.HostSalaryPolicy{}, false, err
|
return ledger.HostSalaryPolicy{}, false, err
|
||||||
}
|
}
|
||||||
@ -384,9 +429,9 @@ func (r *Repository) resolveHostSalaryPolicy(ctx context.Context, tx *sql.Tx, re
|
|||||||
return policy, true, nil
|
return policy, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) listHostSalaryPolicyLevels(ctx context.Context, tx *sql.Tx, policyID uint64) ([]ledger.HostSalaryPolicyLevel, error) {
|
func (r *Repository) listHostSalaryPolicyLevels(ctx context.Context, q hostSalaryPolicyQuerier, policyID uint64) ([]ledger.HostSalaryPolicyLevel, error) {
|
||||||
// 等级必须按 required_diamonds 升序返回,highestHostSalaryLevel 依赖这个顺序找到最高已达档位。
|
// 等级必须按 required_diamonds 升序返回,highestHostSalaryLevel 依赖这个顺序找到最高已达档位。
|
||||||
rows, err := tx.QueryContext(ctx, `
|
rows, err := q.QueryContext(ctx, `
|
||||||
SELECT level_no, required_diamonds, host_salary_usd_minor, host_coin_reward,
|
SELECT level_no, required_diamonds, host_salary_usd_minor, host_coin_reward,
|
||||||
agency_salary_usd_minor, status, sort_order
|
agency_salary_usd_minor, status, sort_order
|
||||||
FROM host_agency_salary_policy_levels
|
FROM host_agency_salary_policy_levels
|
||||||
|
|||||||
@ -130,6 +130,74 @@ func (s *Server) GetBalances(ctx context.Context, req *walletv1.GetBalancesReque
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetActiveHostSalaryPolicy 读取当前生效的主播工资政策,供 gateway 按当前主播区域展示平台规则。
|
||||||
|
func (s *Server) GetActiveHostSalaryPolicy(ctx context.Context, req *walletv1.GetActiveHostSalaryPolicyRequest) (*walletv1.GetActiveHostSalaryPolicyResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||||
|
policy, found, err := s.svc.GetActiveHostSalaryPolicy(ctx, req.GetAppCode(), req.GetRegionId(), req.GetSettlementMode(), req.GetSettlementTriggerMode(), req.GetNowMs())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
response := &walletv1.GetActiveHostSalaryPolicyResponse{Found: found}
|
||||||
|
if found {
|
||||||
|
response.Policy = hostSalaryPolicyToProto(policy)
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHostSalaryProgress 读取主播当前工资周期钻石累计,gateway 用它计算工资政策等级进度。
|
||||||
|
func (s *Server) GetHostSalaryProgress(ctx context.Context, req *walletv1.GetHostSalaryProgressRequest) (*walletv1.GetHostSalaryProgressResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||||
|
progress, err := s.svc.GetHostSalaryProgress(ctx, req.GetAppCode(), req.GetHostUserId(), req.GetCycleKey(), req.GetNowMs())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &walletv1.GetHostSalaryProgressResponse{Progress: hostSalaryProgressToProto(progress)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostSalaryProgressToProto(progress ledger.HostSalaryProgress) *walletv1.HostSalaryProgress {
|
||||||
|
return &walletv1.HostSalaryProgress{
|
||||||
|
HostUserId: progress.HostUserID,
|
||||||
|
CycleKey: progress.CycleKey,
|
||||||
|
RegionId: progress.RegionID,
|
||||||
|
AgencyOwnerUserId: progress.AgencyOwnerUserID,
|
||||||
|
TotalDiamonds: progress.TotalDiamonds,
|
||||||
|
GiftDiamondTotal: progress.GiftDiamondTotal,
|
||||||
|
UpdatedAtMs: progress.UpdatedAtMS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostSalaryPolicyToProto(policy ledger.HostSalaryPolicy) *walletv1.HostSalaryPolicy {
|
||||||
|
levels := make([]*walletv1.HostSalaryPolicyLevel, 0, len(policy.Levels))
|
||||||
|
for _, level := range policy.Levels {
|
||||||
|
levels = append(levels, hostSalaryPolicyLevelToProto(level))
|
||||||
|
}
|
||||||
|
return &walletv1.HostSalaryPolicy{
|
||||||
|
PolicyId: policy.PolicyID,
|
||||||
|
Name: policy.Name,
|
||||||
|
RegionId: policy.RegionID,
|
||||||
|
Status: policy.Status,
|
||||||
|
SettlementMode: policy.SettlementMode,
|
||||||
|
SettlementTriggerMode: policy.SettlementTriggerMode,
|
||||||
|
GiftCoinToDiamondRatio: policy.GiftCoinToDiamondRatio,
|
||||||
|
ResidualDiamondToUsdRate: policy.ResidualDiamondToUSDRate,
|
||||||
|
EffectiveFromMs: policy.EffectiveFromMs,
|
||||||
|
EffectiveToMs: policy.EffectiveToMs,
|
||||||
|
Levels: levels,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostSalaryPolicyLevelToProto(level ledger.HostSalaryPolicyLevel) *walletv1.HostSalaryPolicyLevel {
|
||||||
|
return &walletv1.HostSalaryPolicyLevel{
|
||||||
|
LevelNo: int32(level.LevelNo),
|
||||||
|
RequiredDiamonds: level.RequiredDiamonds,
|
||||||
|
HostSalaryUsdMinor: level.HostSalaryUSDMinor,
|
||||||
|
HostCoinReward: level.HostCoinReward,
|
||||||
|
AgencySalaryUsdMinor: level.AgencySalaryUSDMinor,
|
||||||
|
Status: level.Status,
|
||||||
|
SortOrder: int32(level.SortOrder),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetWalletOverview 返回钱包首页摘要,供我的页和钱包二级页复用。
|
// GetWalletOverview 返回钱包首页摘要,供我的页和钱包二级页复用。
|
||||||
func (s *Server) GetWalletOverview(ctx context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error) {
|
func (s *Server) GetWalletOverview(ctx context.Context, req *walletv1.GetWalletOverviewRequest) (*walletv1.GetWalletOverviewResponse, error) {
|
||||||
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user