Compare commits
No commits in common. "main" and "test" have entirely different histories.
File diff suppressed because it is too large
Load Diff
@ -42,53 +42,6 @@ message RoomBackgroundChanged {
|
|||||||
int64 actor_user_id = 1;
|
int64 actor_user_id = 1;
|
||||||
int64 background_id = 2;
|
int64 background_id = 2;
|
||||||
string room_background_url = 3;
|
string room_background_url = 3;
|
||||||
RoomMediaSnapshot media = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomMediaSnapshot 固化已通过 gateway 文件头检测和 room-service 权限校验的媒体事实。
|
|
||||||
message RoomMediaSnapshot {
|
|
||||||
string url = 1;
|
|
||||||
string object_key = 2;
|
|
||||||
string content_type = 3;
|
|
||||||
string format = 4;
|
|
||||||
int64 size_bytes = 5;
|
|
||||||
int32 width = 6;
|
|
||||||
int32 height = 7;
|
|
||||||
bool animated = 8;
|
|
||||||
int32 frame_count = 9;
|
|
||||||
int64 duration_ms = 10;
|
|
||||||
string poster_url = 11;
|
|
||||||
string thumbnail_url = 12;
|
|
||||||
string sha256 = 13;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomDecorationChanged 表达房间级边框或彩色房名最终样式发生变化。
|
|
||||||
message RoomDecorationChanged {
|
|
||||||
int64 actor_user_id = 1;
|
|
||||||
string decoration_type = 2;
|
|
||||||
int64 resource_id = 3;
|
|
||||||
string resource_code = 4;
|
|
||||||
string resource_type = 5;
|
|
||||||
string name = 6;
|
|
||||||
string asset_url = 7;
|
|
||||||
string preview_url = 8;
|
|
||||||
string animation_url = 9;
|
|
||||||
string format = 10;
|
|
||||||
string metadata_json = 11;
|
|
||||||
repeated string text_colors = 12;
|
|
||||||
repeated double stops = 13;
|
|
||||||
double angle_degrees = 14;
|
|
||||||
string entitlement_id = 15;
|
|
||||||
int64 expires_at_ms = 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomImageMessageSent 是服务端权益校验后写入 outbox 的唯一房间图片消息事实。
|
|
||||||
// message_id/event_id/command_id 都保持稳定,Flutter 以 event_id 或 message_id 去重 at-least-once 投递。
|
|
||||||
message RoomImageMessageSent {
|
|
||||||
string message_id = 1;
|
|
||||||
string command_id = 2;
|
|
||||||
int64 sender_user_id = 3;
|
|
||||||
RoomMediaSnapshot image = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomUserJoined 表达用户业务态进房成功。
|
// RoomUserJoined 表达用户业务态进房成功。
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -40,8 +40,7 @@ message LuckyGiftMeta {
|
|||||||
int64 recharge_7d_coins = 19;
|
int64 recharge_7d_coins = 19;
|
||||||
int64 recharge_30d_coins = 20;
|
int64 recharge_30d_coins = 20;
|
||||||
int64 last_recharged_at_ms = 21;
|
int64 last_recharged_at_ms = 21;
|
||||||
// gift_income_coins 是滚动兼容保留的旧字段名,实际承载 wallet 按后台礼物比例配置结算的
|
// gift_income_coins 是 wallet 已实际入账给收礼人的收益金币;dynamic_v3 用它校验规则主播拆账,缺失或比例不一致均拒绝开奖。
|
||||||
// host_period_diamond_added。幸运礼物服务只能消费该 owner 回执,不能再用幸运礼物规则里的主播比例覆盖它。
|
|
||||||
int64 gift_income_coins = 22;
|
int64 gift_income_coins = 22;
|
||||||
// user_registered_at_ms 是 user-service 在送礼入口返回的账号创建事实快照。
|
// user_registered_at_ms 是 user-service 在送礼入口返回的账号创建事实快照。
|
||||||
// dynamic_v3 只用它判断闭窗时账号是否已满 48 小时;缺失时普通开奖继续,但补偿大奖资格 fail-close。
|
// dynamic_v3 只用它判断闭窗时账号是否已满 48 小时;缺失时普通开奖继续,但补偿大奖资格 fail-close。
|
||||||
@ -247,29 +246,6 @@ message ListLuckyGiftConfigsResponse {
|
|||||||
repeated LuckyGiftRuleConfig configs = 1;
|
repeated LuckyGiftRuleConfig configs = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListLuckyGiftConfigVersions 只读取同一 app+pool 的不可变历史快照;返回顺序为版本号倒序。
|
|
||||||
message ListLuckyGiftConfigVersionsRequest {
|
|
||||||
RequestMeta meta = 1;
|
|
||||||
string pool_id = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ListLuckyGiftConfigVersionsResponse {
|
|
||||||
repeated LuckyGiftRuleConfig configs = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RollbackLuckyGiftConfig 不覆盖历史行,而是把目标快照复制成一个新的最新版本并立即生效。
|
|
||||||
message RollbackLuckyGiftConfigRequest {
|
|
||||||
RequestMeta meta = 1;
|
|
||||||
string pool_id = 2;
|
|
||||||
int64 target_rule_version = 3;
|
|
||||||
int64 operator_admin_id = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RollbackLuckyGiftConfigResponse {
|
|
||||||
int64 source_rule_version = 1;
|
|
||||||
LuckyGiftRuleConfig config = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ListLuckyGiftDrawsRequest {
|
message ListLuckyGiftDrawsRequest {
|
||||||
RequestMeta meta = 1;
|
RequestMeta meta = 1;
|
||||||
string gift_id = 2;
|
string gift_id = 2;
|
||||||
@ -467,8 +443,6 @@ service AdminLuckyGiftService {
|
|||||||
rpc GetLuckyGiftConfig(GetLuckyGiftConfigRequest) returns (GetLuckyGiftConfigResponse);
|
rpc GetLuckyGiftConfig(GetLuckyGiftConfigRequest) returns (GetLuckyGiftConfigResponse);
|
||||||
rpc UpsertLuckyGiftConfig(UpsertLuckyGiftConfigRequest) returns (UpsertLuckyGiftConfigResponse);
|
rpc UpsertLuckyGiftConfig(UpsertLuckyGiftConfigRequest) returns (UpsertLuckyGiftConfigResponse);
|
||||||
rpc ListLuckyGiftConfigs(ListLuckyGiftConfigsRequest) returns (ListLuckyGiftConfigsResponse);
|
rpc ListLuckyGiftConfigs(ListLuckyGiftConfigsRequest) returns (ListLuckyGiftConfigsResponse);
|
||||||
rpc ListLuckyGiftConfigVersions(ListLuckyGiftConfigVersionsRequest) returns (ListLuckyGiftConfigVersionsResponse);
|
|
||||||
rpc RollbackLuckyGiftConfig(RollbackLuckyGiftConfigRequest) returns (RollbackLuckyGiftConfigResponse);
|
|
||||||
rpc ListLuckyGiftDraws(ListLuckyGiftDrawsRequest) returns (ListLuckyGiftDrawsResponse);
|
rpc ListLuckyGiftDraws(ListLuckyGiftDrawsRequest) returns (ListLuckyGiftDrawsResponse);
|
||||||
rpc GetLuckyGiftDrawSummary(GetLuckyGiftDrawSummaryRequest) returns (GetLuckyGiftDrawSummaryResponse);
|
rpc GetLuckyGiftDrawSummary(GetLuckyGiftDrawSummaryRequest) returns (GetLuckyGiftDrawSummaryResponse);
|
||||||
rpc ListLuckyGiftPoolBalances(ListLuckyGiftPoolBalancesRequest) returns (ListLuckyGiftPoolBalancesResponse);
|
rpc ListLuckyGiftPoolBalances(ListLuckyGiftPoolBalancesRequest) returns (ListLuckyGiftPoolBalancesResponse);
|
||||||
|
|||||||
@ -238,8 +238,6 @@ const (
|
|||||||
AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftConfig"
|
AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftConfig"
|
||||||
AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/UpsertLuckyGiftConfig"
|
AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/UpsertLuckyGiftConfig"
|
||||||
AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftConfigs"
|
AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftConfigs"
|
||||||
AdminLuckyGiftService_ListLuckyGiftConfigVersions_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftConfigVersions"
|
|
||||||
AdminLuckyGiftService_RollbackLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/RollbackLuckyGiftConfig"
|
|
||||||
AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftDraws"
|
AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftDraws"
|
||||||
AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftDrawSummary"
|
AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftDrawSummary"
|
||||||
AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftPoolBalances"
|
AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftPoolBalances"
|
||||||
@ -261,8 +259,6 @@ type AdminLuckyGiftServiceClient interface {
|
|||||||
GetLuckyGiftConfig(ctx context.Context, in *GetLuckyGiftConfigRequest, opts ...grpc.CallOption) (*GetLuckyGiftConfigResponse, error)
|
GetLuckyGiftConfig(ctx context.Context, in *GetLuckyGiftConfigRequest, opts ...grpc.CallOption) (*GetLuckyGiftConfigResponse, error)
|
||||||
UpsertLuckyGiftConfig(ctx context.Context, in *UpsertLuckyGiftConfigRequest, opts ...grpc.CallOption) (*UpsertLuckyGiftConfigResponse, error)
|
UpsertLuckyGiftConfig(ctx context.Context, in *UpsertLuckyGiftConfigRequest, opts ...grpc.CallOption) (*UpsertLuckyGiftConfigResponse, error)
|
||||||
ListLuckyGiftConfigs(ctx context.Context, in *ListLuckyGiftConfigsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigsResponse, error)
|
ListLuckyGiftConfigs(ctx context.Context, in *ListLuckyGiftConfigsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigsResponse, error)
|
||||||
ListLuckyGiftConfigVersions(ctx context.Context, in *ListLuckyGiftConfigVersionsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigVersionsResponse, error)
|
|
||||||
RollbackLuckyGiftConfig(ctx context.Context, in *RollbackLuckyGiftConfigRequest, opts ...grpc.CallOption) (*RollbackLuckyGiftConfigResponse, error)
|
|
||||||
ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error)
|
ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error)
|
||||||
GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error)
|
GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error)
|
||||||
ListLuckyGiftPoolBalances(ctx context.Context, in *ListLuckyGiftPoolBalancesRequest, opts ...grpc.CallOption) (*ListLuckyGiftPoolBalancesResponse, error)
|
ListLuckyGiftPoolBalances(ctx context.Context, in *ListLuckyGiftPoolBalancesRequest, opts ...grpc.CallOption) (*ListLuckyGiftPoolBalancesResponse, error)
|
||||||
@ -315,26 +311,6 @@ func (c *adminLuckyGiftServiceClient) ListLuckyGiftConfigs(ctx context.Context,
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *adminLuckyGiftServiceClient) ListLuckyGiftConfigVersions(ctx context.Context, in *ListLuckyGiftConfigVersionsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigVersionsResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(ListLuckyGiftConfigVersionsResponse)
|
|
||||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_ListLuckyGiftConfigVersions_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *adminLuckyGiftServiceClient) RollbackLuckyGiftConfig(ctx context.Context, in *RollbackLuckyGiftConfigRequest, opts ...grpc.CallOption) (*RollbackLuckyGiftConfigResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(RollbackLuckyGiftConfigResponse)
|
|
||||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_RollbackLuckyGiftConfig_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *adminLuckyGiftServiceClient) ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error) {
|
func (c *adminLuckyGiftServiceClient) ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(ListLuckyGiftDrawsResponse)
|
out := new(ListLuckyGiftDrawsResponse)
|
||||||
@ -462,8 +438,6 @@ type AdminLuckyGiftServiceServer interface {
|
|||||||
GetLuckyGiftConfig(context.Context, *GetLuckyGiftConfigRequest) (*GetLuckyGiftConfigResponse, error)
|
GetLuckyGiftConfig(context.Context, *GetLuckyGiftConfigRequest) (*GetLuckyGiftConfigResponse, error)
|
||||||
UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error)
|
UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error)
|
||||||
ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error)
|
ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error)
|
||||||
ListLuckyGiftConfigVersions(context.Context, *ListLuckyGiftConfigVersionsRequest) (*ListLuckyGiftConfigVersionsResponse, error)
|
|
||||||
RollbackLuckyGiftConfig(context.Context, *RollbackLuckyGiftConfigRequest) (*RollbackLuckyGiftConfigResponse, error)
|
|
||||||
ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error)
|
ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error)
|
||||||
GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error)
|
GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error)
|
||||||
ListLuckyGiftPoolBalances(context.Context, *ListLuckyGiftPoolBalancesRequest) (*ListLuckyGiftPoolBalancesResponse, error)
|
ListLuckyGiftPoolBalances(context.Context, *ListLuckyGiftPoolBalancesRequest) (*ListLuckyGiftPoolBalancesResponse, error)
|
||||||
@ -495,12 +469,6 @@ func (UnimplementedAdminLuckyGiftServiceServer) UpsertLuckyGiftConfig(context.Co
|
|||||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error) {
|
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftConfigs not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftConfigs not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftConfigVersions(context.Context, *ListLuckyGiftConfigVersionsRequest) (*ListLuckyGiftConfigVersionsResponse, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftConfigVersions not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedAdminLuckyGiftServiceServer) RollbackLuckyGiftConfig(context.Context, *RollbackLuckyGiftConfigRequest) (*RollbackLuckyGiftConfigResponse, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method RollbackLuckyGiftConfig not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error) {
|
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftDraws not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftDraws not implemented")
|
||||||
}
|
}
|
||||||
@ -612,42 +580,6 @@ func _AdminLuckyGiftService_ListLuckyGiftConfigs_Handler(srv interface{}, ctx co
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _AdminLuckyGiftService_ListLuckyGiftConfigVersions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(ListLuckyGiftConfigVersionsRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftConfigVersions(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: AdminLuckyGiftService_ListLuckyGiftConfigVersions_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftConfigVersions(ctx, req.(*ListLuckyGiftConfigVersionsRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _AdminLuckyGiftService_RollbackLuckyGiftConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(RollbackLuckyGiftConfigRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(AdminLuckyGiftServiceServer).RollbackLuckyGiftConfig(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: AdminLuckyGiftService_RollbackLuckyGiftConfig_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(AdminLuckyGiftServiceServer).RollbackLuckyGiftConfig(ctx, req.(*RollbackLuckyGiftConfigRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _AdminLuckyGiftService_ListLuckyGiftDraws_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _AdminLuckyGiftService_ListLuckyGiftDraws_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ListLuckyGiftDrawsRequest)
|
in := new(ListLuckyGiftDrawsRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -883,14 +815,6 @@ var AdminLuckyGiftService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ListLuckyGiftConfigs",
|
MethodName: "ListLuckyGiftConfigs",
|
||||||
Handler: _AdminLuckyGiftService_ListLuckyGiftConfigs_Handler,
|
Handler: _AdminLuckyGiftService_ListLuckyGiftConfigs_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "ListLuckyGiftConfigVersions",
|
|
||||||
Handler: _AdminLuckyGiftService_ListLuckyGiftConfigVersions_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "RollbackLuckyGiftConfig",
|
|
||||||
Handler: _AdminLuckyGiftService_RollbackLuckyGiftConfig_Handler,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
MethodName: "ListLuckyGiftDraws",
|
MethodName: "ListLuckyGiftDraws",
|
||||||
Handler: _AdminLuckyGiftService_ListLuckyGiftDraws_Handler,
|
Handler: _AdminLuckyGiftService_ListLuckyGiftDraws_Handler,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -552,30 +552,6 @@ message RoomSnapshot {
|
|||||||
repeated RoomBanState ban_states = 21;
|
repeated RoomBanState ban_states = 21;
|
||||||
// online_count 允许 lite 响应不携带 online_users 明细仍保留准确在线人数。
|
// online_count 允许 lite 响应不携带 online_users 明细仍保留准确在线人数。
|
||||||
int32 online_count = 22;
|
int32 online_count = 22;
|
||||||
// room_border 与 room_name_style 是房间级最终装扮快照;客户端不得再按房主 VIP 等级推导样式。
|
|
||||||
RoomDecorationResource room_border = 23;
|
|
||||||
RoomDecorationResource room_name_style = 24;
|
|
||||||
// active_background 固化当前背景的真实媒体属性;room_ext.background_url 仅保留旧客户端兼容。
|
|
||||||
RoomBackgroundImage active_background = 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomMedia 是房间背景和房间图片消息共同使用的可信媒体快照。
|
|
||||||
// URL 只能由 gateway 的受信对象存储上传入口生成;room-service 会再次校验 object_key 归属和媒体边界。
|
|
||||||
message RoomMedia {
|
|
||||||
string url = 1;
|
|
||||||
string object_key = 2;
|
|
||||||
string content_type = 3;
|
|
||||||
string format = 4;
|
|
||||||
int64 size_bytes = 5;
|
|
||||||
int32 width = 6;
|
|
||||||
int32 height = 7;
|
|
||||||
bool animated = 8;
|
|
||||||
int32 frame_count = 9;
|
|
||||||
int64 duration_ms = 10;
|
|
||||||
string poster_url = 11;
|
|
||||||
string thumbnail_url = 12;
|
|
||||||
string sha256 = 13;
|
|
||||||
string status = 14;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomBackgroundImage 是房间维度保存过的背景图素材。
|
// RoomBackgroundImage 是房间维度保存过的背景图素材。
|
||||||
@ -587,16 +563,12 @@ message RoomBackgroundImage {
|
|||||||
int64 created_by_user_id = 4;
|
int64 created_by_user_id = 4;
|
||||||
int64 created_at_ms = 5;
|
int64 created_at_ms = 5;
|
||||||
int64 updated_at_ms = 6;
|
int64 updated_at_ms = 6;
|
||||||
RoomMedia media = 7;
|
|
||||||
string command_id = 8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SaveRoomBackgroundRequest {
|
message SaveRoomBackgroundRequest {
|
||||||
RequestMeta meta = 1;
|
RequestMeta meta = 1;
|
||||||
string room_id = 2;
|
string room_id = 2;
|
||||||
// image_url 仅兼容旧内部调用;新版专用上传必须传完整 media,保存时不按 URL 后缀猜格式。
|
|
||||||
string image_url = 3;
|
string image_url = 3;
|
||||||
RoomMedia media = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SaveRoomBackgroundResponse {
|
message SaveRoomBackgroundResponse {
|
||||||
@ -614,7 +586,6 @@ message ListRoomBackgroundsResponse {
|
|||||||
repeated RoomBackgroundImage backgrounds = 1;
|
repeated RoomBackgroundImage backgrounds = 1;
|
||||||
string selected_background_url = 2;
|
string selected_background_url = 2;
|
||||||
int64 server_time_ms = 3;
|
int64 server_time_ms = 3;
|
||||||
RoomBackgroundImage selected_background = 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message SetRoomBackgroundRequest {
|
message SetRoomBackgroundRequest {
|
||||||
@ -628,85 +599,6 @@ message SetRoomBackgroundResponse {
|
|||||||
RoomBackgroundImage background = 3;
|
RoomBackgroundImage background = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomDecorationResource 是 wallet 资源目录和有效 entitlement 在房间应用时固化的展示快照。
|
|
||||||
// 资源后续编辑、下架或 VIP 到期不会篡改历史 command log;新的读取只返回当前 Room Cell 已应用结果。
|
|
||||||
message RoomDecorationResource {
|
|
||||||
int64 resource_id = 1;
|
|
||||||
string resource_code = 2;
|
|
||||||
string resource_type = 3;
|
|
||||||
string name = 4;
|
|
||||||
string asset_url = 5;
|
|
||||||
string preview_url = 6;
|
|
||||||
string animation_url = 7;
|
|
||||||
string format = 8;
|
|
||||||
string metadata_json = 9;
|
|
||||||
string entitlement_id = 10;
|
|
||||||
repeated string text_colors = 11;
|
|
||||||
repeated double stops = 12;
|
|
||||||
double angle_degrees = 13;
|
|
||||||
int64 expires_at_ms = 14;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ApplyRoomDecorationRequest {
|
|
||||||
RequestMeta meta = 1;
|
|
||||||
// decoration_type 仅接受 room_border/colored_room_name;资源类型分别固定为 room_border/room_name_style。
|
|
||||||
string decoration_type = 2;
|
|
||||||
int64 resource_id = 3;
|
|
||||||
string entitlement_id = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ApplyRoomDecorationResponse {
|
|
||||||
CommandResult result = 1;
|
|
||||||
RoomSnapshot room = 2;
|
|
||||||
string decoration_type = 3;
|
|
||||||
RoomDecorationResource resource = 4;
|
|
||||||
int64 server_time_ms = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AuthorizeRoomMediaUploadRequest {
|
|
||||||
RequestMeta meta = 1;
|
|
||||||
// purpose 仅接受 room_background/room_image;授权发生在 COS PutObject 之前。
|
|
||||||
string purpose = 2;
|
|
||||||
RoomMedia media = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AuthorizeRoomMediaUploadResponse {
|
|
||||||
bool allowed = 1;
|
|
||||||
string object_key_prefix = 2;
|
|
||||||
int64 room_version = 3;
|
|
||||||
int64 server_time_ms = 4;
|
|
||||||
// object_key 由 room owner 按首次 command_id+媒体哈希确定,gateway 不能自行换 key。
|
|
||||||
string object_key = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CompleteRoomMediaUploadRequest {
|
|
||||||
RequestMeta meta = 1;
|
|
||||||
string purpose = 2;
|
|
||||||
// media 必须携带 PutObject 成功后的 url/object_key,并与首次 Authorize 的解析事实完全一致。
|
|
||||||
RoomMedia media = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CompleteRoomMediaUploadResponse {
|
|
||||||
bool active = 1;
|
|
||||||
RoomMedia media = 2;
|
|
||||||
int64 server_time_ms = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SendRoomImageRequest {
|
|
||||||
RequestMeta meta = 1;
|
|
||||||
RoomMedia image = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SendRoomImageResponse {
|
|
||||||
CommandResult result = 1;
|
|
||||||
bool accepted = 2;
|
|
||||||
string message_id = 3;
|
|
||||||
string room_id = 4;
|
|
||||||
int64 sender_user_id = 5;
|
|
||||||
int64 server_time_ms = 6;
|
|
||||||
RoomMedia image = 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateRoomRequest 创建一个新的房间执行单元。
|
// CreateRoomRequest 创建一个新的房间执行单元。
|
||||||
// 必填语义:meta.room_id、meta.command_id、meta.actor_user_id、mode、room_name 和 room_avatar。
|
// 必填语义:meta.room_id、meta.command_id、meta.actor_user_id、mode、room_name 和 room_avatar。
|
||||||
// 同一个 meta.actor_user_id 只能作为 owner 创建一个房间;重复创建返回 Conflict。
|
// 同一个 meta.actor_user_id 只能作为 owner 创建一个房间;重复创建返回 Conflict。
|
||||||
@ -1417,8 +1309,6 @@ message RoomListItem {
|
|||||||
string room_short_id = 14;
|
string room_short_id = 14;
|
||||||
bool locked = 15;
|
bool locked = 15;
|
||||||
string country_code = 16;
|
string country_code = 16;
|
||||||
RoomDecorationResource room_border = 17;
|
|
||||||
RoomDecorationResource room_name_style = 18;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListRoomsResponse 返回一页房间卡片和下一页不透明 cursor。
|
// ListRoomsResponse 返回一页房间卡片和下一页不透明 cursor。
|
||||||
@ -1501,47 +1391,6 @@ message GetCurrentRoomResponse {
|
|||||||
int64 server_time_ms = 9;
|
int64 server_time_ms = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchGetUserVoiceRoomPresencesRequest 查询一组用户当前可向 viewer 展示的上麦入口。
|
|
||||||
// user_ids 只能由 gateway 从外部查询参数解析;room-service 仍会去重并限制最多 100 个。
|
|
||||||
message BatchGetUserVoiceRoomPresencesRequest {
|
|
||||||
RequestMeta meta = 1;
|
|
||||||
int64 viewer_user_id = 2;
|
|
||||||
repeated int64 user_ids = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserVoiceRoomPresenceRoom 是个人资料页 Live 入口需要的最小房间投影。
|
|
||||||
// 它不携带在线用户和完整麦位集合,避免 batch 查询把 RoomSnapshot 明细扩散到用户域接口。
|
|
||||||
message UserVoiceRoomPresenceRoom {
|
|
||||||
string room_id = 1;
|
|
||||||
string room_short_id = 2;
|
|
||||||
int64 owner_user_id = 3;
|
|
||||||
// 当前房间模型没有独立主持人字段,host_user_id 与 owner_user_id 保持一致。
|
|
||||||
int64 host_user_id = 4;
|
|
||||||
string title = 5;
|
|
||||||
string cover_url = 6;
|
|
||||||
string background_url = 7;
|
|
||||||
string mode = 8;
|
|
||||||
int32 online_count = 9;
|
|
||||||
int32 seat_count = 10;
|
|
||||||
bool locked = 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserVoiceRoomPresence 对每个去重后的请求用户返回一条稳定结果。
|
|
||||||
// 不可见、不可进入和未上麦统一返回 not_on_mic,避免通过差异状态探测房间位置。
|
|
||||||
message UserVoiceRoomPresence {
|
|
||||||
int64 user_id = 1;
|
|
||||||
string state = 2;
|
|
||||||
int32 seat_no = 3;
|
|
||||||
bool joinable = 4;
|
|
||||||
int64 observed_at_ms = 5;
|
|
||||||
UserVoiceRoomPresenceRoom room = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message BatchGetUserVoiceRoomPresencesResponse {
|
|
||||||
repeated UserVoiceRoomPresence items = 1;
|
|
||||||
int64 server_time_ms = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRoomSnapshotRequest 主动读取当前房间完整快照。
|
// GetRoomSnapshotRequest 主动读取当前房间完整快照。
|
||||||
// 该查询必须由 gateway 写入鉴权后的 viewer_user_id,客户端不能提交 viewer_user_id。
|
// 该查询必须由 gateway 写入鉴权后的 viewer_user_id,客户端不能提交 viewer_user_id。
|
||||||
message GetRoomSnapshotRequest {
|
message GetRoomSnapshotRequest {
|
||||||
@ -1652,8 +1501,6 @@ service RoomCommandService {
|
|||||||
rpc UpdateRoomProfile(UpdateRoomProfileRequest) returns (UpdateRoomProfileResponse);
|
rpc UpdateRoomProfile(UpdateRoomProfileRequest) returns (UpdateRoomProfileResponse);
|
||||||
rpc SaveRoomBackground(SaveRoomBackgroundRequest) returns (SaveRoomBackgroundResponse);
|
rpc SaveRoomBackground(SaveRoomBackgroundRequest) returns (SaveRoomBackgroundResponse);
|
||||||
rpc SetRoomBackground(SetRoomBackgroundRequest) returns (SetRoomBackgroundResponse);
|
rpc SetRoomBackground(SetRoomBackgroundRequest) returns (SetRoomBackgroundResponse);
|
||||||
rpc ApplyRoomDecoration(ApplyRoomDecorationRequest) returns (ApplyRoomDecorationResponse);
|
|
||||||
rpc SendRoomImage(SendRoomImageRequest) returns (SendRoomImageResponse);
|
|
||||||
rpc JoinRoom(JoinRoomRequest) returns (JoinRoomResponse);
|
rpc JoinRoom(JoinRoomRequest) returns (JoinRoomResponse);
|
||||||
rpc RoomHeartbeat(RoomHeartbeatRequest) returns (RoomHeartbeatResponse);
|
rpc RoomHeartbeat(RoomHeartbeatRequest) returns (RoomHeartbeatResponse);
|
||||||
rpc LeaveRoom(LeaveRoomRequest) returns (LeaveRoomResponse);
|
rpc LeaveRoom(LeaveRoomRequest) returns (LeaveRoomResponse);
|
||||||
@ -1693,8 +1540,6 @@ service RoomGuardService {
|
|||||||
rpc CheckSpeakPermission(CheckSpeakPermissionRequest) returns (CheckSpeakPermissionResponse);
|
rpc CheckSpeakPermission(CheckSpeakPermissionRequest) returns (CheckSpeakPermissionResponse);
|
||||||
rpc VerifyRoomPresence(VerifyRoomPresenceRequest) returns (VerifyRoomPresenceResponse);
|
rpc VerifyRoomPresence(VerifyRoomPresenceRequest) returns (VerifyRoomPresenceResponse);
|
||||||
rpc ResolveRoomAppCode(ResolveRoomAppCodeRequest) returns (ResolveRoomAppCodeResponse);
|
rpc ResolveRoomAppCode(ResolveRoomAppCodeRequest) returns (ResolveRoomAppCodeResponse);
|
||||||
rpc AuthorizeRoomMediaUpload(AuthorizeRoomMediaUploadRequest) returns (AuthorizeRoomMediaUploadResponse);
|
|
||||||
rpc CompleteRoomMediaUpload(CompleteRoomMediaUploadRequest) returns (CompleteRoomMediaUploadResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomQueryService 承载不会改变 Room Cell 状态的读模型查询。
|
// RoomQueryService 承载不会改变 Room Cell 状态的读模型查询。
|
||||||
@ -1714,7 +1559,6 @@ service RoomQueryService {
|
|||||||
rpc ListRoomContributionRank(ListRoomContributionRankRequest) returns (ListRoomContributionRankResponse);
|
rpc ListRoomContributionRank(ListRoomContributionRankRequest) returns (ListRoomContributionRankResponse);
|
||||||
rpc GetMyRoom(GetMyRoomRequest) returns (GetMyRoomResponse);
|
rpc GetMyRoom(GetMyRoomRequest) returns (GetMyRoomResponse);
|
||||||
rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse);
|
rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse);
|
||||||
rpc BatchGetUserVoiceRoomPresences(BatchGetUserVoiceRoomPresencesRequest) returns (BatchGetUserVoiceRoomPresencesResponse);
|
|
||||||
rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse);
|
rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse);
|
||||||
rpc ListRoomBackgrounds(ListRoomBackgroundsRequest) returns (ListRoomBackgroundsResponse);
|
rpc ListRoomBackgrounds(ListRoomBackgroundsRequest) returns (ListRoomBackgroundsResponse);
|
||||||
rpc GetRoomRocket(GetRoomRocketRequest) returns (GetRoomRocketResponse);
|
rpc GetRoomRocket(GetRoomRocketRequest) returns (GetRoomRocketResponse);
|
||||||
|
|||||||
@ -23,8 +23,6 @@ const (
|
|||||||
RoomCommandService_UpdateRoomProfile_FullMethodName = "/hyapp.room.v1.RoomCommandService/UpdateRoomProfile"
|
RoomCommandService_UpdateRoomProfile_FullMethodName = "/hyapp.room.v1.RoomCommandService/UpdateRoomProfile"
|
||||||
RoomCommandService_SaveRoomBackground_FullMethodName = "/hyapp.room.v1.RoomCommandService/SaveRoomBackground"
|
RoomCommandService_SaveRoomBackground_FullMethodName = "/hyapp.room.v1.RoomCommandService/SaveRoomBackground"
|
||||||
RoomCommandService_SetRoomBackground_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomBackground"
|
RoomCommandService_SetRoomBackground_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomBackground"
|
||||||
RoomCommandService_ApplyRoomDecoration_FullMethodName = "/hyapp.room.v1.RoomCommandService/ApplyRoomDecoration"
|
|
||||||
RoomCommandService_SendRoomImage_FullMethodName = "/hyapp.room.v1.RoomCommandService/SendRoomImage"
|
|
||||||
RoomCommandService_JoinRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/JoinRoom"
|
RoomCommandService_JoinRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/JoinRoom"
|
||||||
RoomCommandService_RoomHeartbeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/RoomHeartbeat"
|
RoomCommandService_RoomHeartbeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/RoomHeartbeat"
|
||||||
RoomCommandService_LeaveRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/LeaveRoom"
|
RoomCommandService_LeaveRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/LeaveRoom"
|
||||||
@ -69,8 +67,6 @@ type RoomCommandServiceClient interface {
|
|||||||
UpdateRoomProfile(ctx context.Context, in *UpdateRoomProfileRequest, opts ...grpc.CallOption) (*UpdateRoomProfileResponse, error)
|
UpdateRoomProfile(ctx context.Context, in *UpdateRoomProfileRequest, opts ...grpc.CallOption) (*UpdateRoomProfileResponse, error)
|
||||||
SaveRoomBackground(ctx context.Context, in *SaveRoomBackgroundRequest, opts ...grpc.CallOption) (*SaveRoomBackgroundResponse, error)
|
SaveRoomBackground(ctx context.Context, in *SaveRoomBackgroundRequest, opts ...grpc.CallOption) (*SaveRoomBackgroundResponse, error)
|
||||||
SetRoomBackground(ctx context.Context, in *SetRoomBackgroundRequest, opts ...grpc.CallOption) (*SetRoomBackgroundResponse, error)
|
SetRoomBackground(ctx context.Context, in *SetRoomBackgroundRequest, opts ...grpc.CallOption) (*SetRoomBackgroundResponse, error)
|
||||||
ApplyRoomDecoration(ctx context.Context, in *ApplyRoomDecorationRequest, opts ...grpc.CallOption) (*ApplyRoomDecorationResponse, error)
|
|
||||||
SendRoomImage(ctx context.Context, in *SendRoomImageRequest, opts ...grpc.CallOption) (*SendRoomImageResponse, error)
|
|
||||||
JoinRoom(ctx context.Context, in *JoinRoomRequest, opts ...grpc.CallOption) (*JoinRoomResponse, error)
|
JoinRoom(ctx context.Context, in *JoinRoomRequest, opts ...grpc.CallOption) (*JoinRoomResponse, error)
|
||||||
RoomHeartbeat(ctx context.Context, in *RoomHeartbeatRequest, opts ...grpc.CallOption) (*RoomHeartbeatResponse, error)
|
RoomHeartbeat(ctx context.Context, in *RoomHeartbeatRequest, opts ...grpc.CallOption) (*RoomHeartbeatResponse, error)
|
||||||
LeaveRoom(ctx context.Context, in *LeaveRoomRequest, opts ...grpc.CallOption) (*LeaveRoomResponse, error)
|
LeaveRoom(ctx context.Context, in *LeaveRoomRequest, opts ...grpc.CallOption) (*LeaveRoomResponse, error)
|
||||||
@ -153,26 +149,6 @@ func (c *roomCommandServiceClient) SetRoomBackground(ctx context.Context, in *Se
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *roomCommandServiceClient) ApplyRoomDecoration(ctx context.Context, in *ApplyRoomDecorationRequest, opts ...grpc.CallOption) (*ApplyRoomDecorationResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(ApplyRoomDecorationResponse)
|
|
||||||
err := c.cc.Invoke(ctx, RoomCommandService_ApplyRoomDecoration_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *roomCommandServiceClient) SendRoomImage(ctx context.Context, in *SendRoomImageRequest, opts ...grpc.CallOption) (*SendRoomImageResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(SendRoomImageResponse)
|
|
||||||
err := c.cc.Invoke(ctx, RoomCommandService_SendRoomImage_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *roomCommandServiceClient) JoinRoom(ctx context.Context, in *JoinRoomRequest, opts ...grpc.CallOption) (*JoinRoomResponse, error) {
|
func (c *roomCommandServiceClient) JoinRoom(ctx context.Context, in *JoinRoomRequest, opts ...grpc.CallOption) (*JoinRoomResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(JoinRoomResponse)
|
out := new(JoinRoomResponse)
|
||||||
@ -503,8 +479,6 @@ type RoomCommandServiceServer interface {
|
|||||||
UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error)
|
UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error)
|
||||||
SaveRoomBackground(context.Context, *SaveRoomBackgroundRequest) (*SaveRoomBackgroundResponse, error)
|
SaveRoomBackground(context.Context, *SaveRoomBackgroundRequest) (*SaveRoomBackgroundResponse, error)
|
||||||
SetRoomBackground(context.Context, *SetRoomBackgroundRequest) (*SetRoomBackgroundResponse, error)
|
SetRoomBackground(context.Context, *SetRoomBackgroundRequest) (*SetRoomBackgroundResponse, error)
|
||||||
ApplyRoomDecoration(context.Context, *ApplyRoomDecorationRequest) (*ApplyRoomDecorationResponse, error)
|
|
||||||
SendRoomImage(context.Context, *SendRoomImageRequest) (*SendRoomImageResponse, error)
|
|
||||||
JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error)
|
JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error)
|
||||||
RoomHeartbeat(context.Context, *RoomHeartbeatRequest) (*RoomHeartbeatResponse, error)
|
RoomHeartbeat(context.Context, *RoomHeartbeatRequest) (*RoomHeartbeatResponse, error)
|
||||||
LeaveRoom(context.Context, *LeaveRoomRequest) (*LeaveRoomResponse, error)
|
LeaveRoom(context.Context, *LeaveRoomRequest) (*LeaveRoomResponse, error)
|
||||||
@ -559,12 +533,6 @@ func (UnimplementedRoomCommandServiceServer) SaveRoomBackground(context.Context,
|
|||||||
func (UnimplementedRoomCommandServiceServer) SetRoomBackground(context.Context, *SetRoomBackgroundRequest) (*SetRoomBackgroundResponse, error) {
|
func (UnimplementedRoomCommandServiceServer) SetRoomBackground(context.Context, *SetRoomBackgroundRequest) (*SetRoomBackgroundResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method SetRoomBackground not implemented")
|
return nil, status.Error(codes.Unimplemented, "method SetRoomBackground not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedRoomCommandServiceServer) ApplyRoomDecoration(context.Context, *ApplyRoomDecorationRequest) (*ApplyRoomDecorationResponse, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method ApplyRoomDecoration not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedRoomCommandServiceServer) SendRoomImage(context.Context, *SendRoomImageRequest) (*SendRoomImageResponse, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method SendRoomImage not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedRoomCommandServiceServer) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error) {
|
func (UnimplementedRoomCommandServiceServer) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method JoinRoom not implemented")
|
return nil, status.Error(codes.Unimplemented, "method JoinRoom not implemented")
|
||||||
}
|
}
|
||||||
@ -754,42 +722,6 @@ func _RoomCommandService_SetRoomBackground_Handler(srv interface{}, ctx context.
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _RoomCommandService_ApplyRoomDecoration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(ApplyRoomDecorationRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(RoomCommandServiceServer).ApplyRoomDecoration(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: RoomCommandService_ApplyRoomDecoration_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(RoomCommandServiceServer).ApplyRoomDecoration(ctx, req.(*ApplyRoomDecorationRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _RoomCommandService_SendRoomImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(SendRoomImageRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(RoomCommandServiceServer).SendRoomImage(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: RoomCommandService_SendRoomImage_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(RoomCommandServiceServer).SendRoomImage(ctx, req.(*SendRoomImageRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _RoomCommandService_JoinRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _RoomCommandService_JoinRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(JoinRoomRequest)
|
in := new(JoinRoomRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -1389,14 +1321,6 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "SetRoomBackground",
|
MethodName: "SetRoomBackground",
|
||||||
Handler: _RoomCommandService_SetRoomBackground_Handler,
|
Handler: _RoomCommandService_SetRoomBackground_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "ApplyRoomDecoration",
|
|
||||||
Handler: _RoomCommandService_ApplyRoomDecoration_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "SendRoomImage",
|
|
||||||
Handler: _RoomCommandService_SendRoomImage_Handler,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
MethodName: "JoinRoom",
|
MethodName: "JoinRoom",
|
||||||
Handler: _RoomCommandService_JoinRoom_Handler,
|
Handler: _RoomCommandService_JoinRoom_Handler,
|
||||||
@ -1534,8 +1458,6 @@ const (
|
|||||||
RoomGuardService_CheckSpeakPermission_FullMethodName = "/hyapp.room.v1.RoomGuardService/CheckSpeakPermission"
|
RoomGuardService_CheckSpeakPermission_FullMethodName = "/hyapp.room.v1.RoomGuardService/CheckSpeakPermission"
|
||||||
RoomGuardService_VerifyRoomPresence_FullMethodName = "/hyapp.room.v1.RoomGuardService/VerifyRoomPresence"
|
RoomGuardService_VerifyRoomPresence_FullMethodName = "/hyapp.room.v1.RoomGuardService/VerifyRoomPresence"
|
||||||
RoomGuardService_ResolveRoomAppCode_FullMethodName = "/hyapp.room.v1.RoomGuardService/ResolveRoomAppCode"
|
RoomGuardService_ResolveRoomAppCode_FullMethodName = "/hyapp.room.v1.RoomGuardService/ResolveRoomAppCode"
|
||||||
RoomGuardService_AuthorizeRoomMediaUpload_FullMethodName = "/hyapp.room.v1.RoomGuardService/AuthorizeRoomMediaUpload"
|
|
||||||
RoomGuardService_CompleteRoomMediaUpload_FullMethodName = "/hyapp.room.v1.RoomGuardService/CompleteRoomMediaUpload"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RoomGuardServiceClient is the client API for RoomGuardService service.
|
// RoomGuardServiceClient is the client API for RoomGuardService service.
|
||||||
@ -1547,8 +1469,6 @@ type RoomGuardServiceClient interface {
|
|||||||
CheckSpeakPermission(ctx context.Context, in *CheckSpeakPermissionRequest, opts ...grpc.CallOption) (*CheckSpeakPermissionResponse, error)
|
CheckSpeakPermission(ctx context.Context, in *CheckSpeakPermissionRequest, opts ...grpc.CallOption) (*CheckSpeakPermissionResponse, error)
|
||||||
VerifyRoomPresence(ctx context.Context, in *VerifyRoomPresenceRequest, opts ...grpc.CallOption) (*VerifyRoomPresenceResponse, error)
|
VerifyRoomPresence(ctx context.Context, in *VerifyRoomPresenceRequest, opts ...grpc.CallOption) (*VerifyRoomPresenceResponse, error)
|
||||||
ResolveRoomAppCode(ctx context.Context, in *ResolveRoomAppCodeRequest, opts ...grpc.CallOption) (*ResolveRoomAppCodeResponse, error)
|
ResolveRoomAppCode(ctx context.Context, in *ResolveRoomAppCodeRequest, opts ...grpc.CallOption) (*ResolveRoomAppCodeResponse, error)
|
||||||
AuthorizeRoomMediaUpload(ctx context.Context, in *AuthorizeRoomMediaUploadRequest, opts ...grpc.CallOption) (*AuthorizeRoomMediaUploadResponse, error)
|
|
||||||
CompleteRoomMediaUpload(ctx context.Context, in *CompleteRoomMediaUploadRequest, opts ...grpc.CallOption) (*CompleteRoomMediaUploadResponse, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type roomGuardServiceClient struct {
|
type roomGuardServiceClient struct {
|
||||||
@ -1589,26 +1509,6 @@ func (c *roomGuardServiceClient) ResolveRoomAppCode(ctx context.Context, in *Res
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *roomGuardServiceClient) AuthorizeRoomMediaUpload(ctx context.Context, in *AuthorizeRoomMediaUploadRequest, opts ...grpc.CallOption) (*AuthorizeRoomMediaUploadResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(AuthorizeRoomMediaUploadResponse)
|
|
||||||
err := c.cc.Invoke(ctx, RoomGuardService_AuthorizeRoomMediaUpload_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *roomGuardServiceClient) CompleteRoomMediaUpload(ctx context.Context, in *CompleteRoomMediaUploadRequest, opts ...grpc.CallOption) (*CompleteRoomMediaUploadResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(CompleteRoomMediaUploadResponse)
|
|
||||||
err := c.cc.Invoke(ctx, RoomGuardService_CompleteRoomMediaUpload_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomGuardServiceServer is the server API for RoomGuardService service.
|
// RoomGuardServiceServer is the server API for RoomGuardService service.
|
||||||
// All implementations must embed UnimplementedRoomGuardServiceServer
|
// All implementations must embed UnimplementedRoomGuardServiceServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@ -1618,8 +1518,6 @@ type RoomGuardServiceServer interface {
|
|||||||
CheckSpeakPermission(context.Context, *CheckSpeakPermissionRequest) (*CheckSpeakPermissionResponse, error)
|
CheckSpeakPermission(context.Context, *CheckSpeakPermissionRequest) (*CheckSpeakPermissionResponse, error)
|
||||||
VerifyRoomPresence(context.Context, *VerifyRoomPresenceRequest) (*VerifyRoomPresenceResponse, error)
|
VerifyRoomPresence(context.Context, *VerifyRoomPresenceRequest) (*VerifyRoomPresenceResponse, error)
|
||||||
ResolveRoomAppCode(context.Context, *ResolveRoomAppCodeRequest) (*ResolveRoomAppCodeResponse, error)
|
ResolveRoomAppCode(context.Context, *ResolveRoomAppCodeRequest) (*ResolveRoomAppCodeResponse, error)
|
||||||
AuthorizeRoomMediaUpload(context.Context, *AuthorizeRoomMediaUploadRequest) (*AuthorizeRoomMediaUploadResponse, error)
|
|
||||||
CompleteRoomMediaUpload(context.Context, *CompleteRoomMediaUploadRequest) (*CompleteRoomMediaUploadResponse, error)
|
|
||||||
mustEmbedUnimplementedRoomGuardServiceServer()
|
mustEmbedUnimplementedRoomGuardServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1639,12 +1537,6 @@ func (UnimplementedRoomGuardServiceServer) VerifyRoomPresence(context.Context, *
|
|||||||
func (UnimplementedRoomGuardServiceServer) ResolveRoomAppCode(context.Context, *ResolveRoomAppCodeRequest) (*ResolveRoomAppCodeResponse, error) {
|
func (UnimplementedRoomGuardServiceServer) ResolveRoomAppCode(context.Context, *ResolveRoomAppCodeRequest) (*ResolveRoomAppCodeResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method ResolveRoomAppCode not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ResolveRoomAppCode not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedRoomGuardServiceServer) AuthorizeRoomMediaUpload(context.Context, *AuthorizeRoomMediaUploadRequest) (*AuthorizeRoomMediaUploadResponse, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method AuthorizeRoomMediaUpload not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedRoomGuardServiceServer) CompleteRoomMediaUpload(context.Context, *CompleteRoomMediaUploadRequest) (*CompleteRoomMediaUploadResponse, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method CompleteRoomMediaUpload not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedRoomGuardServiceServer) mustEmbedUnimplementedRoomGuardServiceServer() {}
|
func (UnimplementedRoomGuardServiceServer) mustEmbedUnimplementedRoomGuardServiceServer() {}
|
||||||
func (UnimplementedRoomGuardServiceServer) testEmbeddedByValue() {}
|
func (UnimplementedRoomGuardServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@ -1720,42 +1612,6 @@ func _RoomGuardService_ResolveRoomAppCode_Handler(srv interface{}, ctx context.C
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _RoomGuardService_AuthorizeRoomMediaUpload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(AuthorizeRoomMediaUploadRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(RoomGuardServiceServer).AuthorizeRoomMediaUpload(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: RoomGuardService_AuthorizeRoomMediaUpload_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(RoomGuardServiceServer).AuthorizeRoomMediaUpload(ctx, req.(*AuthorizeRoomMediaUploadRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _RoomGuardService_CompleteRoomMediaUpload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(CompleteRoomMediaUploadRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(RoomGuardServiceServer).CompleteRoomMediaUpload(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: RoomGuardService_CompleteRoomMediaUpload_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(RoomGuardServiceServer).CompleteRoomMediaUpload(ctx, req.(*CompleteRoomMediaUploadRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomGuardService_ServiceDesc is the grpc.ServiceDesc for RoomGuardService service.
|
// RoomGuardService_ServiceDesc is the grpc.ServiceDesc for RoomGuardService service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@ -1775,14 +1631,6 @@ var RoomGuardService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ResolveRoomAppCode",
|
MethodName: "ResolveRoomAppCode",
|
||||||
Handler: _RoomGuardService_ResolveRoomAppCode_Handler,
|
Handler: _RoomGuardService_ResolveRoomAppCode_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "AuthorizeRoomMediaUpload",
|
|
||||||
Handler: _RoomGuardService_AuthorizeRoomMediaUpload_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "CompleteRoomMediaUpload",
|
|
||||||
Handler: _RoomGuardService_CompleteRoomMediaUpload_Handler,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "proto/room/v1/room.proto",
|
Metadata: "proto/room/v1/room.proto",
|
||||||
@ -1804,7 +1652,6 @@ const (
|
|||||||
RoomQueryService_ListRoomContributionRank_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomContributionRank"
|
RoomQueryService_ListRoomContributionRank_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomContributionRank"
|
||||||
RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom"
|
RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom"
|
||||||
RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom"
|
RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom"
|
||||||
RoomQueryService_BatchGetUserVoiceRoomPresences_FullMethodName = "/hyapp.room.v1.RoomQueryService/BatchGetUserVoiceRoomPresences"
|
|
||||||
RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot"
|
RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot"
|
||||||
RoomQueryService_ListRoomBackgrounds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBackgrounds"
|
RoomQueryService_ListRoomBackgrounds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBackgrounds"
|
||||||
RoomQueryService_GetRoomRocket_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomRocket"
|
RoomQueryService_GetRoomRocket_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomRocket"
|
||||||
@ -1833,7 +1680,6 @@ type RoomQueryServiceClient interface {
|
|||||||
ListRoomContributionRank(ctx context.Context, in *ListRoomContributionRankRequest, opts ...grpc.CallOption) (*ListRoomContributionRankResponse, error)
|
ListRoomContributionRank(ctx context.Context, in *ListRoomContributionRankRequest, opts ...grpc.CallOption) (*ListRoomContributionRankResponse, error)
|
||||||
GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error)
|
GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error)
|
||||||
GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error)
|
GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error)
|
||||||
BatchGetUserVoiceRoomPresences(ctx context.Context, in *BatchGetUserVoiceRoomPresencesRequest, opts ...grpc.CallOption) (*BatchGetUserVoiceRoomPresencesResponse, error)
|
|
||||||
GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error)
|
GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error)
|
||||||
ListRoomBackgrounds(ctx context.Context, in *ListRoomBackgroundsRequest, opts ...grpc.CallOption) (*ListRoomBackgroundsResponse, error)
|
ListRoomBackgrounds(ctx context.Context, in *ListRoomBackgroundsRequest, opts ...grpc.CallOption) (*ListRoomBackgroundsResponse, error)
|
||||||
GetRoomRocket(ctx context.Context, in *GetRoomRocketRequest, opts ...grpc.CallOption) (*GetRoomRocketResponse, error)
|
GetRoomRocket(ctx context.Context, in *GetRoomRocketRequest, opts ...grpc.CallOption) (*GetRoomRocketResponse, error)
|
||||||
@ -1999,16 +1845,6 @@ func (c *roomQueryServiceClient) GetCurrentRoom(ctx context.Context, in *GetCurr
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *roomQueryServiceClient) BatchGetUserVoiceRoomPresences(ctx context.Context, in *BatchGetUserVoiceRoomPresencesRequest, opts ...grpc.CallOption) (*BatchGetUserVoiceRoomPresencesResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(BatchGetUserVoiceRoomPresencesResponse)
|
|
||||||
err := c.cc.Invoke(ctx, RoomQueryService_BatchGetUserVoiceRoomPresences_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *roomQueryServiceClient) GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error) {
|
func (c *roomQueryServiceClient) GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(GetRoomSnapshotResponse)
|
out := new(GetRoomSnapshotResponse)
|
||||||
@ -2080,7 +1916,6 @@ type RoomQueryServiceServer interface {
|
|||||||
ListRoomContributionRank(context.Context, *ListRoomContributionRankRequest) (*ListRoomContributionRankResponse, error)
|
ListRoomContributionRank(context.Context, *ListRoomContributionRankRequest) (*ListRoomContributionRankResponse, error)
|
||||||
GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error)
|
GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error)
|
||||||
GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error)
|
GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error)
|
||||||
BatchGetUserVoiceRoomPresences(context.Context, *BatchGetUserVoiceRoomPresencesRequest) (*BatchGetUserVoiceRoomPresencesResponse, error)
|
|
||||||
GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error)
|
GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error)
|
||||||
ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error)
|
ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error)
|
||||||
GetRoomRocket(context.Context, *GetRoomRocketRequest) (*GetRoomRocketResponse, error)
|
GetRoomRocket(context.Context, *GetRoomRocketRequest) (*GetRoomRocketResponse, error)
|
||||||
@ -2141,9 +1976,6 @@ func (UnimplementedRoomQueryServiceServer) GetMyRoom(context.Context, *GetMyRoom
|
|||||||
func (UnimplementedRoomQueryServiceServer) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) {
|
func (UnimplementedRoomQueryServiceServer) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetCurrentRoom not implemented")
|
return nil, status.Error(codes.Unimplemented, "method GetCurrentRoom not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedRoomQueryServiceServer) BatchGetUserVoiceRoomPresences(context.Context, *BatchGetUserVoiceRoomPresencesRequest) (*BatchGetUserVoiceRoomPresencesResponse, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method BatchGetUserVoiceRoomPresences not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedRoomQueryServiceServer) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) {
|
func (UnimplementedRoomQueryServiceServer) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetRoomSnapshot not implemented")
|
return nil, status.Error(codes.Unimplemented, "method GetRoomSnapshot not implemented")
|
||||||
}
|
}
|
||||||
@ -2450,24 +2282,6 @@ func _RoomQueryService_GetCurrentRoom_Handler(srv interface{}, ctx context.Conte
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _RoomQueryService_BatchGetUserVoiceRoomPresences_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(BatchGetUserVoiceRoomPresencesRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(RoomQueryServiceServer).BatchGetUserVoiceRoomPresences(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: RoomQueryService_BatchGetUserVoiceRoomPresences_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(RoomQueryServiceServer).BatchGetUserVoiceRoomPresences(ctx, req.(*BatchGetUserVoiceRoomPresencesRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _RoomQueryService_GetRoomSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _RoomQueryService_GetRoomSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(GetRoomSnapshotRequest)
|
in := new(GetRoomSnapshotRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -2625,10 +2439,6 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "GetCurrentRoom",
|
MethodName: "GetCurrentRoom",
|
||||||
Handler: _RoomQueryService_GetCurrentRoom_Handler,
|
Handler: _RoomQueryService_GetCurrentRoom_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "BatchGetUserVoiceRoomPresences",
|
|
||||||
Handler: _RoomQueryService_BatchGetUserVoiceRoomPresences_Handler,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
MethodName: "GetRoomSnapshot",
|
MethodName: "GetRoomSnapshot",
|
||||||
Handler: _RoomQueryService_GetRoomSnapshot_Handler,
|
Handler: _RoomQueryService_GetRoomSnapshot_Handler,
|
||||||
|
|||||||
@ -2214,9 +2214,6 @@ type RecordProfileVisitResponse struct {
|
|||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Recorded bool `protobuf:"varint,1,opt,name=recorded,proto3" json:"recorded,omitempty"`
|
Recorded bool `protobuf:"varint,1,opt,name=recorded,proto3" json:"recorded,omitempty"`
|
||||||
TargetStats *UserProfileStats `protobuf:"bytes,2,opt,name=target_stats,json=targetStats,proto3" json:"target_stats,omitempty"`
|
TargetStats *UserProfileStats `protobuf:"bytes,2,opt,name=target_stats,json=targetStats,proto3" json:"target_stats,omitempty"`
|
||||||
// target_stats_hidden=true 时 target_stats 必须为空;该标志由 user-service
|
|
||||||
// 基于目标用户当前 hide_profile_data 权益实时判定,客户端不能自行推断。
|
|
||||||
TargetStatsHidden bool `protobuf:"varint,3,opt,name=target_stats_hidden,json=targetStatsHidden,proto3" json:"target_stats_hidden,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -2265,25 +2262,12 @@ func (x *RecordProfileVisitResponse) GetTargetStats() *UserProfileStats {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RecordProfileVisitResponse) GetTargetStatsHidden() bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.TargetStatsHidden
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProfileVisitRecord struct {
|
type ProfileVisitRecord struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// 隐藏访客身份的记录不返回真实 visitor_user_id,字段保持 0;客户端必须使用
|
|
||||||
// visitor_identity_hidden/anonymous_visit_id 分支渲染匿名访客,不能把 0 当作用户 ID。
|
|
||||||
VisitorUserId int64 `protobuf:"varint,1,opt,name=visitor_user_id,json=visitorUserId,proto3" json:"visitor_user_id,omitempty"`
|
VisitorUserId int64 `protobuf:"varint,1,opt,name=visitor_user_id,json=visitorUserId,proto3" json:"visitor_user_id,omitempty"`
|
||||||
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
||||||
VisitCount int64 `protobuf:"varint,3,opt,name=visit_count,json=visitCount,proto3" json:"visit_count,omitempty"`
|
VisitCount int64 `protobuf:"varint,3,opt,name=visit_count,json=visitCount,proto3" json:"visit_count,omitempty"`
|
||||||
LastVisitedAtMs int64 `protobuf:"varint,4,opt,name=last_visited_at_ms,json=lastVisitedAtMs,proto3" json:"last_visited_at_ms,omitempty"`
|
LastVisitedAtMs int64 `protobuf:"varint,4,opt,name=last_visited_at_ms,json=lastVisitedAtMs,proto3" json:"last_visited_at_ms,omitempty"`
|
||||||
VisitorIdentityHidden bool `protobuf:"varint,5,opt,name=visitor_identity_hidden,json=visitorIdentityHidden,proto3" json:"visitor_identity_hidden,omitempty"`
|
|
||||||
// anonymous_visit_id 是首次匿名访问时随机生成、仅在同一 target 下稳定的展示 ID,
|
|
||||||
// 不包含真实 user_id,也不能用于查询用户资料。
|
|
||||||
AnonymousVisitId string `protobuf:"bytes,6,opt,name=anonymous_visit_id,json=anonymousVisitId,proto3" json:"anonymous_visit_id,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -2346,20 +2330,6 @@ func (x *ProfileVisitRecord) GetLastVisitedAtMs() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ProfileVisitRecord) GetVisitorIdentityHidden() bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.VisitorIdentityHidden
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ProfileVisitRecord) GetAnonymousVisitId() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.AnonymousVisitId
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListProfileVisitorsRequest struct {
|
type ListProfileVisitorsRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||||
@ -13680,19 +13650,16 @@ const file_proto_user_v1_user_proto_rawDesc = "" +
|
|||||||
"\x19RecordProfileVisitRequest\x12.\n" +
|
"\x19RecordProfileVisitRequest\x12.\n" +
|
||||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12&\n" +
|
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12&\n" +
|
||||||
"\x0fvisitor_user_id\x18\x02 \x01(\x03R\rvisitorUserId\x12$\n" +
|
"\x0fvisitor_user_id\x18\x02 \x01(\x03R\rvisitorUserId\x12$\n" +
|
||||||
"\x0etarget_user_id\x18\x03 \x01(\x03R\ftargetUserId\"\xac\x01\n" +
|
"\x0etarget_user_id\x18\x03 \x01(\x03R\ftargetUserId\"|\n" +
|
||||||
"\x1aRecordProfileVisitResponse\x12\x1a\n" +
|
"\x1aRecordProfileVisitResponse\x12\x1a\n" +
|
||||||
"\brecorded\x18\x01 \x01(\bR\brecorded\x12B\n" +
|
"\brecorded\x18\x01 \x01(\bR\brecorded\x12B\n" +
|
||||||
"\ftarget_stats\x18\x02 \x01(\v2\x1f.hyapp.user.v1.UserProfileStatsR\vtargetStats\x12.\n" +
|
"\ftarget_stats\x18\x02 \x01(\v2\x1f.hyapp.user.v1.UserProfileStatsR\vtargetStats\"\xb0\x01\n" +
|
||||||
"\x13target_stats_hidden\x18\x03 \x01(\bR\x11targetStatsHidden\"\x96\x02\n" +
|
|
||||||
"\x12ProfileVisitRecord\x12&\n" +
|
"\x12ProfileVisitRecord\x12&\n" +
|
||||||
"\x0fvisitor_user_id\x18\x01 \x01(\x03R\rvisitorUserId\x12$\n" +
|
"\x0fvisitor_user_id\x18\x01 \x01(\x03R\rvisitorUserId\x12$\n" +
|
||||||
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x1f\n" +
|
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x1f\n" +
|
||||||
"\vvisit_count\x18\x03 \x01(\x03R\n" +
|
"\vvisit_count\x18\x03 \x01(\x03R\n" +
|
||||||
"visitCount\x12+\n" +
|
"visitCount\x12+\n" +
|
||||||
"\x12last_visited_at_ms\x18\x04 \x01(\x03R\x0flastVisitedAtMs\x126\n" +
|
"\x12last_visited_at_ms\x18\x04 \x01(\x03R\x0flastVisitedAtMs\"\x96\x01\n" +
|
||||||
"\x17visitor_identity_hidden\x18\x05 \x01(\bR\x15visitorIdentityHidden\x12,\n" +
|
|
||||||
"\x12anonymous_visit_id\x18\x06 \x01(\tR\x10anonymousVisitId\"\x96\x01\n" +
|
|
||||||
"\x1aListProfileVisitorsRequest\x12.\n" +
|
"\x1aListProfileVisitorsRequest\x12.\n" +
|
||||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x17\n" +
|
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x17\n" +
|
||||||
"\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x12\n" +
|
"\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x12\n" +
|
||||||
|
|||||||
@ -262,22 +262,13 @@ message RecordProfileVisitRequest {
|
|||||||
message RecordProfileVisitResponse {
|
message RecordProfileVisitResponse {
|
||||||
bool recorded = 1;
|
bool recorded = 1;
|
||||||
UserProfileStats target_stats = 2;
|
UserProfileStats target_stats = 2;
|
||||||
// target_stats_hidden=true 时 target_stats 必须为空;该标志由 user-service
|
|
||||||
// 基于目标用户当前 hide_profile_data 权益实时判定,客户端不能自行推断。
|
|
||||||
bool target_stats_hidden = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ProfileVisitRecord {
|
message ProfileVisitRecord {
|
||||||
// 隐藏访客身份的记录不返回真实 visitor_user_id,字段保持 0;客户端必须使用
|
|
||||||
// visitor_identity_hidden/anonymous_visit_id 分支渲染匿名访客,不能把 0 当作用户 ID。
|
|
||||||
int64 visitor_user_id = 1;
|
int64 visitor_user_id = 1;
|
||||||
int64 target_user_id = 2;
|
int64 target_user_id = 2;
|
||||||
int64 visit_count = 3;
|
int64 visit_count = 3;
|
||||||
int64 last_visited_at_ms = 4;
|
int64 last_visited_at_ms = 4;
|
||||||
bool visitor_identity_hidden = 5;
|
|
||||||
// anonymous_visit_id 是首次匿名访问时随机生成、仅在同一 target 下稳定的展示 ID,
|
|
||||||
// 不包含真实 user_id,也不能用于查询用户资料。
|
|
||||||
string anonymous_visit_id = 6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListProfileVisitorsRequest {
|
message ListProfileVisitorsRequest {
|
||||||
|
|||||||
@ -1385,8 +1385,6 @@ type HostSalaryPolicy struct {
|
|||||||
EffectiveFromMs int64 `protobuf:"varint,9,opt,name=effective_from_ms,json=effectiveFromMs,proto3" json:"effective_from_ms,omitempty"`
|
EffectiveFromMs int64 `protobuf:"varint,9,opt,name=effective_from_ms,json=effectiveFromMs,proto3" json:"effective_from_ms,omitempty"`
|
||||||
EffectiveToMs int64 `protobuf:"varint,10,opt,name=effective_to_ms,json=effectiveToMs,proto3" json:"effective_to_ms,omitempty"`
|
EffectiveToMs int64 `protobuf:"varint,10,opt,name=effective_to_ms,json=effectiveToMs,proto3" json:"effective_to_ms,omitempty"`
|
||||||
Levels []*HostSalaryPolicyLevel `protobuf:"bytes,11,rep,name=levels,proto3" json:"levels,omitempty"`
|
Levels []*HostSalaryPolicyLevel `protobuf:"bytes,11,rep,name=levels,proto3" json:"levels,omitempty"`
|
||||||
CycleKey string `protobuf:"bytes,12,opt,name=cycle_key,json=cycleKey,proto3" json:"cycle_key,omitempty"`
|
|
||||||
PolicyVersion uint64 `protobuf:"varint,13,opt,name=policy_version,json=policyVersion,proto3" json:"policy_version,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -1498,20 +1496,6 @@ func (x *HostSalaryPolicy) GetLevels() []*HostSalaryPolicyLevel {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *HostSalaryPolicy) GetCycleKey() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.CycleKey
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *HostSalaryPolicy) GetPolicyVersion() uint64 {
|
|
||||||
if x != nil {
|
|
||||||
return x.PolicyVersion
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetActiveHostSalaryPolicyRequest struct {
|
type GetActiveHostSalaryPolicyRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
|
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
|
||||||
@ -1520,8 +1504,6 @@ type GetActiveHostSalaryPolicyRequest struct {
|
|||||||
SettlementMode string `protobuf:"bytes,4,opt,name=settlement_mode,json=settlementMode,proto3" json:"settlement_mode,omitempty"`
|
SettlementMode string `protobuf:"bytes,4,opt,name=settlement_mode,json=settlementMode,proto3" json:"settlement_mode,omitempty"`
|
||||||
SettlementTriggerMode string `protobuf:"bytes,5,opt,name=settlement_trigger_mode,json=settlementTriggerMode,proto3" json:"settlement_trigger_mode,omitempty"`
|
SettlementTriggerMode string `protobuf:"bytes,5,opt,name=settlement_trigger_mode,json=settlementTriggerMode,proto3" json:"settlement_trigger_mode,omitempty"`
|
||||||
NowMs int64 `protobuf:"varint,6,opt,name=now_ms,json=nowMs,proto3" json:"now_ms,omitempty"`
|
NowMs int64 `protobuf:"varint,6,opt,name=now_ms,json=nowMs,proto3" json:"now_ms,omitempty"`
|
||||||
// cycle_key 精确选择该工资周期已绑定的不可变政策版本;空值仅供旧客户端回退当前 UTC 月。
|
|
||||||
CycleKey string `protobuf:"bytes,7,opt,name=cycle_key,json=cycleKey,proto3" json:"cycle_key,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -1598,13 +1580,6 @@ func (x *GetActiveHostSalaryPolicyRequest) GetNowMs() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GetActiveHostSalaryPolicyRequest) GetCycleKey() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.CycleKey
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetActiveHostSalaryPolicyResponse struct {
|
type GetActiveHostSalaryPolicyResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Found bool `protobuf:"varint,1,opt,name=found,proto3" json:"found,omitempty"`
|
Found bool `protobuf:"varint,1,opt,name=found,proto3" json:"found,omitempty"`
|
||||||
@ -9690,8 +9665,6 @@ type EquipUserResourceRequest struct {
|
|||||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||||
ResourceId int64 `protobuf:"varint,4,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"`
|
ResourceId int64 `protobuf:"varint,4,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"`
|
||||||
EntitlementId string `protobuf:"bytes,5,opt,name=entitlement_id,json=entitlementId,proto3" json:"entitlement_id,omitempty"`
|
EntitlementId string `protobuf:"bytes,5,opt,name=entitlement_id,json=entitlementId,proto3" json:"entitlement_id,omitempty"`
|
||||||
// command_id 是装备状态变更的业务幂等键;request_id 只用于链路追踪。
|
|
||||||
CommandId string `protobuf:"bytes,6,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -9761,13 +9734,6 @@ func (x *EquipUserResourceRequest) GetEntitlementId() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *EquipUserResourceRequest) GetCommandId() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.CommandId
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type EquipUserResourceResponse struct {
|
type EquipUserResourceResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Resource *UserResourceEntitlement `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"`
|
Resource *UserResourceEntitlement `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"`
|
||||||
@ -17495,9 +17461,6 @@ type VipBenefit struct {
|
|||||||
CreatedAtMs int64 `protobuf:"varint,13,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
CreatedAtMs int64 `protobuf:"varint,13,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||||
UpdatedAtMs int64 `protobuf:"varint,14,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
|
UpdatedAtMs int64 `protobuf:"varint,14,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
|
||||||
Presentation *VipBenefitPresentation `protobuf:"bytes,15,opt,name=presentation,proto3" json:"presentation,omitempty"`
|
Presentation *VipBenefitPresentation `protobuf:"bytes,15,opt,name=presentation,proto3" json:"presentation,omitempty"`
|
||||||
// resource 仅在 resource_id 指向同 App、active 且 resource_type 匹配的真实资源时返回。
|
|
||||||
// 运营尚未配置素材的权益保留 resource_id/resource_type 事实,但该对象为空,客户端不得猜 URL。
|
|
||||||
Resource *Resource `protobuf:"bytes,16,opt,name=resource,proto3" json:"resource,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -17637,13 +17600,6 @@ func (x *VipBenefit) GetPresentation() *VipBenefitPresentation {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *VipBenefit) GetResource() *Resource {
|
|
||||||
if x != nil {
|
|
||||||
return x.Resource
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type VipLevel struct {
|
type VipLevel struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Level int32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"`
|
Level int32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"`
|
||||||
@ -20900,10 +20856,6 @@ type CheckVipBenefitResponse struct {
|
|||||||
State *VipState `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"`
|
State *VipState `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"`
|
||||||
DenialReason string `protobuf:"bytes,4,opt,name=denial_reason,json=denialReason,proto3" json:"denial_reason,omitempty"`
|
DenialReason string `protobuf:"bytes,4,opt,name=denial_reason,json=denialReason,proto3" json:"denial_reason,omitempty"`
|
||||||
EvaluatedAtMs int64 `protobuf:"varint,5,opt,name=evaluated_at_ms,json=evaluatedAtMs,proto3" json:"evaluated_at_ms,omitempty"`
|
EvaluatedAtMs int64 `protobuf:"varint,5,opt,name=evaluated_at_ms,json=evaluatedAtMs,proto3" json:"evaluated_at_ms,omitempty"`
|
||||||
// required_level 是当前 App 所有 active 等级中首次显式配置该权益的等级;0 表示未配置。
|
|
||||||
RequiredLevel int32 `protobuf:"varint,6,opt,name=required_level,json=requiredLevel,proto3" json:"required_level,omitempty"`
|
|
||||||
// current_effective_level 来自 wallet 合并付费会员与体验卡后的最终状态。
|
|
||||||
CurrentEffectiveLevel int32 `protobuf:"varint,7,opt,name=current_effective_level,json=currentEffectiveLevel,proto3" json:"current_effective_level,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -20973,20 +20925,6 @@ func (x *CheckVipBenefitResponse) GetEvaluatedAtMs() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *CheckVipBenefitResponse) GetRequiredLevel() int32 {
|
|
||||||
if x != nil {
|
|
||||||
return x.RequiredLevel
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *CheckVipBenefitResponse) GetCurrentEffectiveLevel() int32 {
|
|
||||||
if x != nil {
|
|
||||||
return x.CurrentEffectiveLevel
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminVipLevelInput struct {
|
type AdminVipLevelInput struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Level int32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"`
|
Level int32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"`
|
||||||
@ -26468,7 +26406,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"\x17agency_salary_usd_minor\x18\x05 \x01(\x03R\x14agencySalaryUsdMinor\x12\x16\n" +
|
"\x17agency_salary_usd_minor\x18\x05 \x01(\x03R\x14agencySalaryUsdMinor\x12\x16\n" +
|
||||||
"\x06status\x18\x06 \x01(\tR\x06status\x12\x1d\n" +
|
"\x06status\x18\x06 \x01(\tR\x06status\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"sort_order\x18\a \x01(\x05R\tsortOrder\"\xad\x04\n" +
|
"sort_order\x18\a \x01(\x05R\tsortOrder\"\xe9\x03\n" +
|
||||||
"\x10HostSalaryPolicy\x12\x1b\n" +
|
"\x10HostSalaryPolicy\x12\x1b\n" +
|
||||||
"\tpolicy_id\x18\x01 \x01(\x04R\bpolicyId\x12\x12\n" +
|
"\tpolicy_id\x18\x01 \x01(\x04R\bpolicyId\x12\x12\n" +
|
||||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" +
|
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1b\n" +
|
||||||
@ -26481,9 +26419,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"\x11effective_from_ms\x18\t \x01(\x03R\x0feffectiveFromMs\x12&\n" +
|
"\x11effective_from_ms\x18\t \x01(\x03R\x0feffectiveFromMs\x12&\n" +
|
||||||
"\x0feffective_to_ms\x18\n" +
|
"\x0feffective_to_ms\x18\n" +
|
||||||
" \x01(\x03R\reffectiveToMs\x12>\n" +
|
" \x01(\x03R\reffectiveToMs\x12>\n" +
|
||||||
"\x06levels\x18\v \x03(\v2&.hyapp.wallet.v1.HostSalaryPolicyLevelR\x06levels\x12\x1b\n" +
|
"\x06levels\x18\v \x03(\v2&.hyapp.wallet.v1.HostSalaryPolicyLevelR\x06levels\"\xf1\x01\n" +
|
||||||
"\tcycle_key\x18\f \x01(\tR\bcycleKey\x12%\n" +
|
|
||||||
"\x0epolicy_version\x18\r \x01(\x04R\rpolicyVersion\"\x8e\x02\n" +
|
|
||||||
" GetActiveHostSalaryPolicyRequest\x12\x1d\n" +
|
" GetActiveHostSalaryPolicyRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
|
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
|
||||||
@ -26491,8 +26427,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"\tregion_id\x18\x03 \x01(\x03R\bregionId\x12'\n" +
|
"\tregion_id\x18\x03 \x01(\x03R\bregionId\x12'\n" +
|
||||||
"\x0fsettlement_mode\x18\x04 \x01(\tR\x0esettlementMode\x126\n" +
|
"\x0fsettlement_mode\x18\x04 \x01(\tR\x0esettlementMode\x126\n" +
|
||||||
"\x17settlement_trigger_mode\x18\x05 \x01(\tR\x15settlementTriggerMode\x12\x15\n" +
|
"\x17settlement_trigger_mode\x18\x05 \x01(\tR\x15settlementTriggerMode\x12\x15\n" +
|
||||||
"\x06now_ms\x18\x06 \x01(\x03R\x05nowMs\x12\x1b\n" +
|
"\x06now_ms\x18\x06 \x01(\x03R\x05nowMs\"t\n" +
|
||||||
"\tcycle_key\x18\a \x01(\tR\bcycleKey\"t\n" +
|
|
||||||
"!GetActiveHostSalaryPolicyResponse\x12\x14\n" +
|
"!GetActiveHostSalaryPolicyResponse\x12\x14\n" +
|
||||||
"\x05found\x18\x01 \x01(\bR\x05found\x129\n" +
|
"\x05found\x18\x01 \x01(\bR\x05found\x129\n" +
|
||||||
"\x06policy\x18\x02 \x01(\v2!.hyapp.wallet.v1.HostSalaryPolicyR\x06policy\"\x9a\x02\n" +
|
"\x06policy\x18\x02 \x01(\v2!.hyapp.wallet.v1.HostSalaryPolicyR\x06policy\"\x9a\x02\n" +
|
||||||
@ -27345,7 +27280,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"\vactive_only\x18\x05 \x01(\bR\n" +
|
"\vactive_only\x18\x05 \x01(\bR\n" +
|
||||||
"activeOnly\"c\n" +
|
"activeOnly\"c\n" +
|
||||||
"\x19ListUserResourcesResponse\x12F\n" +
|
"\x19ListUserResourcesResponse\x12F\n" +
|
||||||
"\tresources\x18\x01 \x03(\v2(.hyapp.wallet.v1.UserResourceEntitlementR\tresources\"\xd4\x01\n" +
|
"\tresources\x18\x01 \x03(\v2(.hyapp.wallet.v1.UserResourceEntitlementR\tresources\"\xb5\x01\n" +
|
||||||
"\x18EquipUserResourceRequest\x12\x1d\n" +
|
"\x18EquipUserResourceRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
|
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
|
||||||
@ -27353,9 +27288,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"\auser_id\x18\x03 \x01(\x03R\x06userId\x12\x1f\n" +
|
"\auser_id\x18\x03 \x01(\x03R\x06userId\x12\x1f\n" +
|
||||||
"\vresource_id\x18\x04 \x01(\x03R\n" +
|
"\vresource_id\x18\x04 \x01(\x03R\n" +
|
||||||
"resourceId\x12%\n" +
|
"resourceId\x12%\n" +
|
||||||
"\x0eentitlement_id\x18\x05 \x01(\tR\rentitlementId\x12\x1d\n" +
|
"\x0eentitlement_id\x18\x05 \x01(\tR\rentitlementId\"a\n" +
|
||||||
"\n" +
|
|
||||||
"command_id\x18\x06 \x01(\tR\tcommandId\"a\n" +
|
|
||||||
"\x19EquipUserResourceResponse\x12D\n" +
|
"\x19EquipUserResourceResponse\x12D\n" +
|
||||||
"\bresource\x18\x01 \x01(\v2(.hyapp.wallet.v1.UserResourceEntitlementR\bresource\"\x94\x01\n" +
|
"\bresource\x18\x01 \x01(\v2(.hyapp.wallet.v1.UserResourceEntitlementR\bresource\"\x94\x01\n" +
|
||||||
"\x1aUnequipUserResourceRequest\x12\x1d\n" +
|
"\x1aUnequipUserResourceRequest\x12\x1d\n" +
|
||||||
@ -28122,7 +28055,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"\x0econfig_version\x18\v \x01(\x03R\rconfigVersion\x12-\n" +
|
"\x0econfig_version\x18\v \x01(\x03R\rconfigVersion\x12-\n" +
|
||||||
"\x13updated_by_admin_id\x18\f \x01(\x03R\x10updatedByAdminId\x12\"\n" +
|
"\x13updated_by_admin_id\x18\f \x01(\x03R\x10updatedByAdminId\x12\"\n" +
|
||||||
"\rcreated_at_ms\x18\r \x01(\x03R\vcreatedAtMs\x12\"\n" +
|
"\rcreated_at_ms\x18\r \x01(\x03R\vcreatedAtMs\x12\"\n" +
|
||||||
"\rupdated_at_ms\x18\x0e \x01(\x03R\vupdatedAtMs\"\xe4\x04\n" +
|
"\rupdated_at_ms\x18\x0e \x01(\x03R\vupdatedAtMs\"\xad\x04\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"VipBenefit\x12!\n" +
|
"VipBenefit\x12!\n" +
|
||||||
"\fbenefit_code\x18\x01 \x01(\tR\vbenefitCode\x12\x12\n" +
|
"\fbenefit_code\x18\x01 \x01(\tR\vbenefitCode\x12\x12\n" +
|
||||||
@ -28143,8 +28076,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"\rmetadata_json\x18\f \x01(\tR\fmetadataJson\x12\"\n" +
|
"\rmetadata_json\x18\f \x01(\tR\fmetadataJson\x12\"\n" +
|
||||||
"\rcreated_at_ms\x18\r \x01(\x03R\vcreatedAtMs\x12\"\n" +
|
"\rcreated_at_ms\x18\r \x01(\x03R\vcreatedAtMs\x12\"\n" +
|
||||||
"\rupdated_at_ms\x18\x0e \x01(\x03R\vupdatedAtMs\x12K\n" +
|
"\rupdated_at_ms\x18\x0e \x01(\x03R\vupdatedAtMs\x12K\n" +
|
||||||
"\fpresentation\x18\x0f \x01(\v2'.hyapp.wallet.v1.VipBenefitPresentationR\fpresentation\x125\n" +
|
"\fpresentation\x18\x0f \x01(\v2'.hyapp.wallet.v1.VipBenefitPresentationR\fpresentation\"\xdc\x05\n" +
|
||||||
"\bresource\x18\x10 \x01(\v2\x19.hyapp.wallet.v1.ResourceR\bresource\"\xdc\x05\n" +
|
|
||||||
"\bVipLevel\x12\x14\n" +
|
"\bVipLevel\x12\x14\n" +
|
||||||
"\x05level\x18\x01 \x01(\x05R\x05level\x12\x12\n" +
|
"\x05level\x18\x01 \x01(\x05R\x05level\x12\x12\n" +
|
||||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" +
|
"\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" +
|
||||||
@ -28456,15 +28388,13 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
|
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
|
||||||
"\bapp_code\x18\x02 \x01(\tR\aappCode\x12\x17\n" +
|
"\bapp_code\x18\x02 \x01(\tR\aappCode\x12\x17\n" +
|
||||||
"\auser_id\x18\x03 \x01(\x03R\x06userId\x12!\n" +
|
"\auser_id\x18\x03 \x01(\x03R\x06userId\x12!\n" +
|
||||||
"\fbenefit_code\x18\x04 \x01(\tR\vbenefitCode\"\xc7\x02\n" +
|
"\fbenefit_code\x18\x04 \x01(\tR\vbenefitCode\"\xe8\x01\n" +
|
||||||
"\x17CheckVipBenefitResponse\x12\x18\n" +
|
"\x17CheckVipBenefitResponse\x12\x18\n" +
|
||||||
"\aallowed\x18\x01 \x01(\bR\aallowed\x125\n" +
|
"\aallowed\x18\x01 \x01(\bR\aallowed\x125\n" +
|
||||||
"\abenefit\x18\x02 \x01(\v2\x1b.hyapp.wallet.v1.VipBenefitR\abenefit\x12/\n" +
|
"\abenefit\x18\x02 \x01(\v2\x1b.hyapp.wallet.v1.VipBenefitR\abenefit\x12/\n" +
|
||||||
"\x05state\x18\x03 \x01(\v2\x19.hyapp.wallet.v1.VipStateR\x05state\x12#\n" +
|
"\x05state\x18\x03 \x01(\v2\x19.hyapp.wallet.v1.VipStateR\x05state\x12#\n" +
|
||||||
"\rdenial_reason\x18\x04 \x01(\tR\fdenialReason\x12&\n" +
|
"\rdenial_reason\x18\x04 \x01(\tR\fdenialReason\x12&\n" +
|
||||||
"\x0fevaluated_at_ms\x18\x05 \x01(\x03R\revaluatedAtMs\x12%\n" +
|
"\x0fevaluated_at_ms\x18\x05 \x01(\x03R\revaluatedAtMs\"\xea\x02\n" +
|
||||||
"\x0erequired_level\x18\x06 \x01(\x05R\rrequiredLevel\x126\n" +
|
|
||||||
"\x17current_effective_level\x18\a \x01(\x05R\x15currentEffectiveLevel\"\xea\x02\n" +
|
|
||||||
"\x12AdminVipLevelInput\x12\x14\n" +
|
"\x12AdminVipLevelInput\x12\x14\n" +
|
||||||
"\x05level\x18\x01 \x01(\x05R\x05level\x12\x12\n" +
|
"\x05level\x18\x01 \x01(\x05R\x05level\x12\x12\n" +
|
||||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" +
|
"\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" +
|
||||||
@ -29504,344 +29434,343 @@ var file_proto_wallet_v1_wallet_proto_depIdxs = []int32{
|
|||||||
184, // 83: hyapp.wallet.v1.GetDiamondExchangeConfigResponse.rules:type_name -> hyapp.wallet.v1.DiamondExchangeRule
|
184, // 83: hyapp.wallet.v1.GetDiamondExchangeConfigResponse.rules:type_name -> hyapp.wallet.v1.DiamondExchangeRule
|
||||||
187, // 84: hyapp.wallet.v1.ListWalletTransactionsResponse.transactions:type_name -> hyapp.wallet.v1.WalletTransaction
|
187, // 84: hyapp.wallet.v1.ListWalletTransactionsResponse.transactions:type_name -> hyapp.wallet.v1.WalletTransaction
|
||||||
297, // 85: hyapp.wallet.v1.VipBenefit.presentation:type_name -> hyapp.wallet.v1.VipBenefitPresentation
|
297, // 85: hyapp.wallet.v1.VipBenefit.presentation:type_name -> hyapp.wallet.v1.VipBenefitPresentation
|
||||||
48, // 86: hyapp.wallet.v1.VipBenefit.resource:type_name -> hyapp.wallet.v1.Resource
|
190, // 86: hyapp.wallet.v1.VipLevel.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem
|
||||||
190, // 87: hyapp.wallet.v1.VipLevel.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem
|
192, // 87: hyapp.wallet.v1.VipLevel.benefits:type_name -> hyapp.wallet.v1.VipBenefit
|
||||||
192, // 88: hyapp.wallet.v1.VipLevel.benefits:type_name -> hyapp.wallet.v1.VipBenefit
|
194, // 88: hyapp.wallet.v1.VipState.paid_vip:type_name -> hyapp.wallet.v1.UserVip
|
||||||
194, // 89: hyapp.wallet.v1.VipState.paid_vip:type_name -> hyapp.wallet.v1.UserVip
|
195, // 89: hyapp.wallet.v1.VipState.equipped_trial_card:type_name -> hyapp.wallet.v1.VipTrialCard
|
||||||
195, // 90: hyapp.wallet.v1.VipState.equipped_trial_card:type_name -> hyapp.wallet.v1.VipTrialCard
|
194, // 90: hyapp.wallet.v1.VipState.effective_vip:type_name -> hyapp.wallet.v1.UserVip
|
||||||
194, // 91: hyapp.wallet.v1.VipState.effective_vip:type_name -> hyapp.wallet.v1.UserVip
|
192, // 91: hyapp.wallet.v1.VipState.effective_benefits:type_name -> hyapp.wallet.v1.VipBenefit
|
||||||
192, // 92: hyapp.wallet.v1.VipState.effective_benefits:type_name -> hyapp.wallet.v1.VipBenefit
|
191, // 92: hyapp.wallet.v1.VipState.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
||||||
191, // 93: hyapp.wallet.v1.VipState.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
196, // 93: hyapp.wallet.v1.VipState.user_settings:type_name -> hyapp.wallet.v1.VipUserSettings
|
||||||
196, // 94: hyapp.wallet.v1.VipState.user_settings:type_name -> hyapp.wallet.v1.VipUserSettings
|
194, // 94: hyapp.wallet.v1.ListVipPackagesResponse.current_vip:type_name -> hyapp.wallet.v1.UserVip
|
||||||
194, // 95: hyapp.wallet.v1.ListVipPackagesResponse.current_vip:type_name -> hyapp.wallet.v1.UserVip
|
193, // 95: hyapp.wallet.v1.ListVipPackagesResponse.packages:type_name -> hyapp.wallet.v1.VipLevel
|
||||||
193, // 96: hyapp.wallet.v1.ListVipPackagesResponse.packages:type_name -> hyapp.wallet.v1.VipLevel
|
197, // 96: hyapp.wallet.v1.ListVipPackagesResponse.state:type_name -> hyapp.wallet.v1.VipState
|
||||||
197, // 97: hyapp.wallet.v1.ListVipPackagesResponse.state:type_name -> hyapp.wallet.v1.VipState
|
191, // 97: hyapp.wallet.v1.ListVipPackagesResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
||||||
191, // 98: hyapp.wallet.v1.ListVipPackagesResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
194, // 98: hyapp.wallet.v1.GetMyVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip
|
||||||
194, // 99: hyapp.wallet.v1.GetMyVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip
|
197, // 99: hyapp.wallet.v1.GetMyVipResponse.state:type_name -> hyapp.wallet.v1.VipState
|
||||||
197, // 100: hyapp.wallet.v1.GetMyVipResponse.state:type_name -> hyapp.wallet.v1.VipState
|
194, // 100: hyapp.wallet.v1.PurchaseVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip
|
||||||
194, // 101: hyapp.wallet.v1.PurchaseVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip
|
190, // 101: hyapp.wallet.v1.PurchaseVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem
|
||||||
190, // 102: hyapp.wallet.v1.PurchaseVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem
|
197, // 102: hyapp.wallet.v1.PurchaseVipResponse.state:type_name -> hyapp.wallet.v1.VipState
|
||||||
197, // 103: hyapp.wallet.v1.PurchaseVipResponse.state:type_name -> hyapp.wallet.v1.VipState
|
8, // 103: hyapp.wallet.v1.PurchaseVipResponse.coin_balance:type_name -> hyapp.wallet.v1.AssetBalance
|
||||||
8, // 104: hyapp.wallet.v1.PurchaseVipResponse.coin_balance:type_name -> hyapp.wallet.v1.AssetBalance
|
8, // 104: hyapp.wallet.v1.DebitCPBreakupFeeResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
||||||
8, // 105: hyapp.wallet.v1.DebitCPBreakupFeeResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
8, // 105: hyapp.wallet.v1.DebitWheelDrawResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
||||||
8, // 106: hyapp.wallet.v1.DebitWheelDrawResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
194, // 106: hyapp.wallet.v1.GrantVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip
|
||||||
194, // 107: hyapp.wallet.v1.GrantVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip
|
190, // 107: hyapp.wallet.v1.GrantVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem
|
||||||
190, // 108: hyapp.wallet.v1.GrantVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem
|
197, // 108: hyapp.wallet.v1.GrantVipResponse.state:type_name -> hyapp.wallet.v1.VipState
|
||||||
197, // 109: hyapp.wallet.v1.GrantVipResponse.state:type_name -> hyapp.wallet.v1.VipState
|
191, // 109: hyapp.wallet.v1.GetVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
||||||
191, // 110: hyapp.wallet.v1.GetVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
192, // 110: hyapp.wallet.v1.GetVipProgramConfigResponse.benefits:type_name -> hyapp.wallet.v1.VipBenefit
|
||||||
192, // 111: hyapp.wallet.v1.GetVipProgramConfigResponse.benefits:type_name -> hyapp.wallet.v1.VipBenefit
|
191, // 111: hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest.config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
||||||
191, // 112: hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest.config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
191, // 112: hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
||||||
191, // 113: hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
196, // 113: hyapp.wallet.v1.UpdateMyVipSettingsResponse.settings:type_name -> hyapp.wallet.v1.VipUserSettings
|
||||||
196, // 114: hyapp.wallet.v1.UpdateMyVipSettingsResponse.settings:type_name -> hyapp.wallet.v1.VipUserSettings
|
216, // 114: hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse.run:type_name -> hyapp.wallet.v1.VipDailyCoinRebateRun
|
||||||
216, // 115: hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse.run:type_name -> hyapp.wallet.v1.VipDailyCoinRebateRun
|
217, // 115: hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate
|
||||||
217, // 116: hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate
|
217, // 116: hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse.rebates:type_name -> hyapp.wallet.v1.VipDailyCoinRebate
|
||||||
217, // 117: hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse.rebates:type_name -> hyapp.wallet.v1.VipDailyCoinRebate
|
217, // 117: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate
|
||||||
217, // 118: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate
|
8, // 118: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.coin_balance:type_name -> hyapp.wallet.v1.AssetBalance
|
||||||
8, // 119: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.coin_balance:type_name -> hyapp.wallet.v1.AssetBalance
|
195, // 119: hyapp.wallet.v1.GrantVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard
|
||||||
195, // 120: hyapp.wallet.v1.GrantVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard
|
197, // 120: hyapp.wallet.v1.GrantVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState
|
||||||
197, // 121: hyapp.wallet.v1.GrantVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState
|
195, // 121: hyapp.wallet.v1.EquipVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard
|
||||||
195, // 122: hyapp.wallet.v1.EquipVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard
|
197, // 122: hyapp.wallet.v1.EquipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState
|
||||||
197, // 123: hyapp.wallet.v1.EquipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState
|
197, // 123: hyapp.wallet.v1.UnequipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState
|
||||||
197, // 124: hyapp.wallet.v1.UnequipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState
|
192, // 124: hyapp.wallet.v1.CheckVipBenefitResponse.benefit:type_name -> hyapp.wallet.v1.VipBenefit
|
||||||
192, // 125: hyapp.wallet.v1.CheckVipBenefitResponse.benefit:type_name -> hyapp.wallet.v1.VipBenefit
|
197, // 125: hyapp.wallet.v1.CheckVipBenefitResponse.state:type_name -> hyapp.wallet.v1.VipState
|
||||||
197, // 126: hyapp.wallet.v1.CheckVipBenefitResponse.state:type_name -> hyapp.wallet.v1.VipState
|
192, // 126: hyapp.wallet.v1.AdminVipLevelInput.benefits:type_name -> hyapp.wallet.v1.VipBenefit
|
||||||
192, // 127: hyapp.wallet.v1.AdminVipLevelInput.benefits:type_name -> hyapp.wallet.v1.VipBenefit
|
193, // 127: hyapp.wallet.v1.ListAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel
|
||||||
193, // 128: hyapp.wallet.v1.ListAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel
|
191, // 128: hyapp.wallet.v1.ListAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
||||||
191, // 129: hyapp.wallet.v1.ListAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
234, // 129: hyapp.wallet.v1.UpdateAdminVipLevelsRequest.levels:type_name -> hyapp.wallet.v1.AdminVipLevelInput
|
||||||
234, // 130: hyapp.wallet.v1.UpdateAdminVipLevelsRequest.levels:type_name -> hyapp.wallet.v1.AdminVipLevelInput
|
193, // 130: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel
|
||||||
193, // 131: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel
|
191, // 131: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
||||||
191, // 132: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig
|
8, // 132: hyapp.wallet.v1.CreditTaskRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
||||||
8, // 133: hyapp.wallet.v1.CreditTaskRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
8, // 133: hyapp.wallet.v1.CreditLuckyGiftRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
||||||
8, // 134: hyapp.wallet.v1.CreditLuckyGiftRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
8, // 134: hyapp.wallet.v1.CreditWheelRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
||||||
8, // 135: hyapp.wallet.v1.CreditWheelRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
8, // 135: hyapp.wallet.v1.CreditRoomTurnoverRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
||||||
8, // 136: hyapp.wallet.v1.CreditRoomTurnoverRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
8, // 136: hyapp.wallet.v1.CreditInviteActivityRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
||||||
8, // 137: hyapp.wallet.v1.CreditInviteActivityRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
8, // 137: hyapp.wallet.v1.CreditAgencyOpeningRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
||||||
8, // 138: hyapp.wallet.v1.CreditAgencyOpeningRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance
|
254, // 138: hyapp.wallet.v1.RedPacket.claims:type_name -> hyapp.wallet.v1.RedPacketClaim
|
||||||
254, // 139: hyapp.wallet.v1.RedPacket.claims:type_name -> hyapp.wallet.v1.RedPacketClaim
|
253, // 139: hyapp.wallet.v1.GetRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig
|
||||||
253, // 140: hyapp.wallet.v1.GetRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig
|
253, // 140: hyapp.wallet.v1.UpdateRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig
|
||||||
253, // 141: hyapp.wallet.v1.UpdateRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig
|
255, // 141: hyapp.wallet.v1.CreateRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
|
||||||
255, // 142: hyapp.wallet.v1.CreateRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
|
254, // 142: hyapp.wallet.v1.ClaimRedPacketResponse.claim:type_name -> hyapp.wallet.v1.RedPacketClaim
|
||||||
254, // 143: hyapp.wallet.v1.ClaimRedPacketResponse.claim:type_name -> hyapp.wallet.v1.RedPacketClaim
|
255, // 143: hyapp.wallet.v1.ClaimRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
|
||||||
255, // 144: hyapp.wallet.v1.ClaimRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
|
255, // 144: hyapp.wallet.v1.ListRedPacketsResponse.packets:type_name -> hyapp.wallet.v1.RedPacket
|
||||||
255, // 145: hyapp.wallet.v1.ListRedPacketsResponse.packets:type_name -> hyapp.wallet.v1.RedPacket
|
255, // 145: hyapp.wallet.v1.GetRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
|
||||||
255, // 146: hyapp.wallet.v1.GetRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
|
255, // 146: hyapp.wallet.v1.RetryRedPacketRefundResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
|
||||||
255, // 147: hyapp.wallet.v1.RetryRedPacketRefundResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
|
48, // 147: hyapp.wallet.v1.ResourceShopPurchaseOrder.resource:type_name -> hyapp.wallet.v1.Resource
|
||||||
48, // 148: hyapp.wallet.v1.ResourceShopPurchaseOrder.resource:type_name -> hyapp.wallet.v1.Resource
|
274, // 148: hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse.orders:type_name -> hyapp.wallet.v1.ResourceShopPurchaseOrder
|
||||||
274, // 149: hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse.orders:type_name -> hyapp.wallet.v1.ResourceShopPurchaseOrder
|
279, // 149: hyapp.wallet.v1.GetHostRevenueStatsResponse.stats:type_name -> hyapp.wallet.v1.HostRevenueStats
|
||||||
279, // 150: hyapp.wallet.v1.GetHostRevenueStatsResponse.stats:type_name -> hyapp.wallet.v1.HostRevenueStats
|
282, // 150: hyapp.wallet.v1.GetAgencyPointShareStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyPointShareStats
|
||||||
282, // 151: hyapp.wallet.v1.GetAgencyPointShareStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyPointShareStats
|
285, // 151: hyapp.wallet.v1.GetAgencyHostGiftStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyHostGiftStats
|
||||||
285, // 152: hyapp.wallet.v1.GetAgencyHostGiftStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyHostGiftStats
|
288, // 152: hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse.sellers:type_name -> hyapp.wallet.v1.PointWithdrawalCoinSellerConfig
|
||||||
288, // 153: hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse.sellers:type_name -> hyapp.wallet.v1.PointWithdrawalCoinSellerConfig
|
295, // 153: hyapp.wallet.v1.VipBenefitPresentation.preview_items:type_name -> hyapp.wallet.v1.VipBenefitPreviewItem
|
||||||
295, // 154: hyapp.wallet.v1.VipBenefitPresentation.preview_items:type_name -> hyapp.wallet.v1.VipBenefitPreviewItem
|
296, // 154: hyapp.wallet.v1.VipBenefitPresentation.numeric_reward:type_name -> hyapp.wallet.v1.VipBenefitNumericReward
|
||||||
296, // 155: hyapp.wallet.v1.VipBenefitPresentation.numeric_reward:type_name -> hyapp.wallet.v1.VipBenefitNumericReward
|
57, // 155: hyapp.wallet.v1.RevokeUserResourceResponse.resource:type_name -> hyapp.wallet.v1.UserResourceEntitlement
|
||||||
57, // 156: hyapp.wallet.v1.RevokeUserResourceResponse.resource:type_name -> hyapp.wallet.v1.UserResourceEntitlement
|
272, // 156: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest
|
||||||
272, // 157: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest
|
272, // 157: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest
|
||||||
272, // 158: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest
|
272, // 158: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:input_type -> hyapp.wallet.v1.CronBatchRequest
|
||||||
272, // 159: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:input_type -> hyapp.wallet.v1.CronBatchRequest
|
218, // 159: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:input_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchRequest
|
||||||
218, // 160: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:input_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchRequest
|
0, // 160: hyapp.wallet.v1.WalletService.DebitGift:input_type -> hyapp.wallet.v1.DebitGiftRequest
|
||||||
0, // 161: hyapp.wallet.v1.WalletService.DebitGift:input_type -> hyapp.wallet.v1.DebitGiftRequest
|
1, // 161: hyapp.wallet.v1.WalletService.DebitDirectGift:input_type -> hyapp.wallet.v1.DebitDirectGiftRequest
|
||||||
1, // 162: hyapp.wallet.v1.WalletService.DebitDirectGift:input_type -> hyapp.wallet.v1.DebitDirectGiftRequest
|
4, // 162: hyapp.wallet.v1.WalletService.BatchDebitGift:input_type -> hyapp.wallet.v1.BatchDebitGiftRequest
|
||||||
4, // 163: hyapp.wallet.v1.WalletService.BatchDebitGift:input_type -> hyapp.wallet.v1.BatchDebitGiftRequest
|
7, // 163: hyapp.wallet.v1.WalletService.DebitRobotGift:input_type -> hyapp.wallet.v1.DebitRobotGiftRequest
|
||||||
7, // 164: hyapp.wallet.v1.WalletService.DebitRobotGift:input_type -> hyapp.wallet.v1.DebitRobotGiftRequest
|
9, // 164: hyapp.wallet.v1.WalletService.GetBalances:input_type -> hyapp.wallet.v1.GetBalancesRequest
|
||||||
9, // 165: hyapp.wallet.v1.WalletService.GetBalances:input_type -> hyapp.wallet.v1.GetBalancesRequest
|
13, // 165: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:input_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyRequest
|
||||||
13, // 166: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:input_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyRequest
|
16, // 166: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:input_type -> hyapp.wallet.v1.GetHostSalaryProgressRequest
|
||||||
16, // 167: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:input_type -> hyapp.wallet.v1.GetHostSalaryProgressRequest
|
280, // 167: hyapp.wallet.v1.WalletService.GetHostRevenueStats:input_type -> hyapp.wallet.v1.GetHostRevenueStatsRequest
|
||||||
280, // 168: hyapp.wallet.v1.WalletService.GetHostRevenueStats:input_type -> hyapp.wallet.v1.GetHostRevenueStatsRequest
|
283, // 168: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:input_type -> hyapp.wallet.v1.GetAgencyPointShareStatsRequest
|
||||||
283, // 169: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:input_type -> hyapp.wallet.v1.GetAgencyPointShareStatsRequest
|
286, // 169: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:input_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsRequest
|
||||||
286, // 170: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:input_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsRequest
|
19, // 170: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:input_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsRequest
|
||||||
19, // 171: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:input_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsRequest
|
21, // 171: hyapp.wallet.v1.WalletService.AdminCreditAsset:input_type -> hyapp.wallet.v1.AdminCreditAssetRequest
|
||||||
21, // 172: hyapp.wallet.v1.WalletService.AdminCreditAsset:input_type -> hyapp.wallet.v1.AdminCreditAssetRequest
|
23, // 172: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:input_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockRequest
|
||||||
23, // 173: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:input_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockRequest
|
25, // 173: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:input_type -> hyapp.wallet.v1.TransferCoinFromSellerRequest
|
||||||
25, // 174: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:input_type -> hyapp.wallet.v1.TransferCoinFromSellerRequest
|
27, // 174: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:input_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildRequest
|
||||||
27, // 175: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:input_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildRequest
|
30, // 175: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:input_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersRequest
|
||||||
30, // 176: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:input_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersRequest
|
32, // 176: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:input_type -> hyapp.wallet.v1.ExchangeSalaryToCoinRequest
|
||||||
32, // 177: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:input_type -> hyapp.wallet.v1.ExchangeSalaryToCoinRequest
|
34, // 177: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:input_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerRequest
|
||||||
34, // 178: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:input_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerRequest
|
289, // 178: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:input_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersRequest
|
||||||
289, // 179: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:input_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersRequest
|
291, // 179: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:input_type -> hyapp.wallet.v1.TransferPointToCoinSellerRequest
|
||||||
291, // 180: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:input_type -> hyapp.wallet.v1.TransferPointToCoinSellerRequest
|
293, // 180: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:input_type -> hyapp.wallet.v1.GetPointWithdrawalConfigRequest
|
||||||
293, // 181: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:input_type -> hyapp.wallet.v1.GetPointWithdrawalConfigRequest
|
36, // 181: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:input_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalRequest
|
||||||
36, // 182: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:input_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalRequest
|
38, // 182: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:input_type -> hyapp.wallet.v1.SettleSalaryWithdrawalRequest
|
||||||
38, // 183: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:input_type -> hyapp.wallet.v1.SettleSalaryWithdrawalRequest
|
40, // 183: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:input_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalRequest
|
||||||
40, // 184: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:input_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalRequest
|
42, // 184: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:input_type -> hyapp.wallet.v1.FreezePointWithdrawalRequest
|
||||||
42, // 185: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:input_type -> hyapp.wallet.v1.FreezePointWithdrawalRequest
|
44, // 185: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:input_type -> hyapp.wallet.v1.SettlePointWithdrawalRequest
|
||||||
44, // 186: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:input_type -> hyapp.wallet.v1.SettlePointWithdrawalRequest
|
46, // 186: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:input_type -> hyapp.wallet.v1.ReleasePointWithdrawalRequest
|
||||||
46, // 187: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:input_type -> hyapp.wallet.v1.ReleasePointWithdrawalRequest
|
61, // 187: hyapp.wallet.v1.WalletService.ListResources:input_type -> hyapp.wallet.v1.ListResourcesRequest
|
||||||
61, // 188: hyapp.wallet.v1.WalletService.ListResources:input_type -> hyapp.wallet.v1.ListResourcesRequest
|
63, // 188: hyapp.wallet.v1.WalletService.GetResource:input_type -> hyapp.wallet.v1.GetResourceRequest
|
||||||
63, // 189: hyapp.wallet.v1.WalletService.GetResource:input_type -> hyapp.wallet.v1.GetResourceRequest
|
65, // 189: hyapp.wallet.v1.WalletService.CreateResource:input_type -> hyapp.wallet.v1.CreateResourceRequest
|
||||||
65, // 190: hyapp.wallet.v1.WalletService.CreateResource:input_type -> hyapp.wallet.v1.CreateResourceRequest
|
66, // 190: hyapp.wallet.v1.WalletService.UpdateResource:input_type -> hyapp.wallet.v1.UpdateResourceRequest
|
||||||
66, // 191: hyapp.wallet.v1.WalletService.UpdateResource:input_type -> hyapp.wallet.v1.UpdateResourceRequest
|
67, // 191: hyapp.wallet.v1.WalletService.SetResourceStatus:input_type -> hyapp.wallet.v1.SetResourceStatusRequest
|
||||||
67, // 192: hyapp.wallet.v1.WalletService.SetResourceStatus:input_type -> hyapp.wallet.v1.SetResourceStatusRequest
|
68, // 192: hyapp.wallet.v1.WalletService.DeleteResource:input_type -> hyapp.wallet.v1.DeleteResourceRequest
|
||||||
68, // 193: hyapp.wallet.v1.WalletService.DeleteResource:input_type -> hyapp.wallet.v1.DeleteResourceRequest
|
69, // 193: hyapp.wallet.v1.WalletService.BatchDeleteResources:input_type -> hyapp.wallet.v1.BatchDeleteResourcesRequest
|
||||||
69, // 194: hyapp.wallet.v1.WalletService.BatchDeleteResources:input_type -> hyapp.wallet.v1.BatchDeleteResourcesRequest
|
72, // 194: hyapp.wallet.v1.WalletService.ListResourceGroups:input_type -> hyapp.wallet.v1.ListResourceGroupsRequest
|
||||||
72, // 195: hyapp.wallet.v1.WalletService.ListResourceGroups:input_type -> hyapp.wallet.v1.ListResourceGroupsRequest
|
74, // 195: hyapp.wallet.v1.WalletService.GetResourceGroup:input_type -> hyapp.wallet.v1.GetResourceGroupRequest
|
||||||
74, // 196: hyapp.wallet.v1.WalletService.GetResourceGroup:input_type -> hyapp.wallet.v1.GetResourceGroupRequest
|
76, // 196: hyapp.wallet.v1.WalletService.CreateResourceGroup:input_type -> hyapp.wallet.v1.CreateResourceGroupRequest
|
||||||
76, // 197: hyapp.wallet.v1.WalletService.CreateResourceGroup:input_type -> hyapp.wallet.v1.CreateResourceGroupRequest
|
77, // 197: hyapp.wallet.v1.WalletService.UpdateResourceGroup:input_type -> hyapp.wallet.v1.UpdateResourceGroupRequest
|
||||||
77, // 198: hyapp.wallet.v1.WalletService.UpdateResourceGroup:input_type -> hyapp.wallet.v1.UpdateResourceGroupRequest
|
78, // 198: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:input_type -> hyapp.wallet.v1.SetResourceGroupStatusRequest
|
||||||
78, // 199: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:input_type -> hyapp.wallet.v1.SetResourceGroupStatusRequest
|
52, // 199: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:input_type -> hyapp.wallet.v1.PinResourceGroupSnapshotRequest
|
||||||
52, // 200: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:input_type -> hyapp.wallet.v1.PinResourceGroupSnapshotRequest
|
80, // 200: hyapp.wallet.v1.WalletService.ListGiftConfigs:input_type -> hyapp.wallet.v1.ListGiftConfigsRequest
|
||||||
80, // 201: hyapp.wallet.v1.WalletService.ListGiftConfigs:input_type -> hyapp.wallet.v1.ListGiftConfigsRequest
|
82, // 201: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:input_type -> hyapp.wallet.v1.ListGiftTypeConfigsRequest
|
||||||
82, // 202: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:input_type -> hyapp.wallet.v1.ListGiftTypeConfigsRequest
|
277, // 202: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:input_type -> hyapp.wallet.v1.GetGiftCatalogVersionRequest
|
||||||
277, // 203: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:input_type -> hyapp.wallet.v1.GetGiftCatalogVersionRequest
|
86, // 203: hyapp.wallet.v1.WalletService.CreateGiftConfig:input_type -> hyapp.wallet.v1.CreateGiftConfigRequest
|
||||||
86, // 204: hyapp.wallet.v1.WalletService.CreateGiftConfig:input_type -> hyapp.wallet.v1.CreateGiftConfigRequest
|
87, // 204: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:input_type -> hyapp.wallet.v1.BatchCreateGiftConfigsRequest
|
||||||
87, // 205: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:input_type -> hyapp.wallet.v1.BatchCreateGiftConfigsRequest
|
89, // 205: hyapp.wallet.v1.WalletService.UpdateGiftConfig:input_type -> hyapp.wallet.v1.UpdateGiftConfigRequest
|
||||||
89, // 206: hyapp.wallet.v1.WalletService.UpdateGiftConfig:input_type -> hyapp.wallet.v1.UpdateGiftConfigRequest
|
90, // 206: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:input_type -> hyapp.wallet.v1.SetGiftConfigStatusRequest
|
||||||
90, // 207: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:input_type -> hyapp.wallet.v1.SetGiftConfigStatusRequest
|
91, // 207: hyapp.wallet.v1.WalletService.DeleteGiftConfig:input_type -> hyapp.wallet.v1.DeleteGiftConfigRequest
|
||||||
91, // 208: hyapp.wallet.v1.WalletService.DeleteGiftConfig:input_type -> hyapp.wallet.v1.DeleteGiftConfigRequest
|
84, // 208: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:input_type -> hyapp.wallet.v1.UpsertGiftTypeConfigRequest
|
||||||
84, // 209: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:input_type -> hyapp.wallet.v1.UpsertGiftTypeConfigRequest
|
93, // 209: hyapp.wallet.v1.WalletService.GrantResource:input_type -> hyapp.wallet.v1.GrantResourceRequest
|
||||||
93, // 210: hyapp.wallet.v1.WalletService.GrantResource:input_type -> hyapp.wallet.v1.GrantResourceRequest
|
94, // 210: hyapp.wallet.v1.WalletService.GrantResourceGroup:input_type -> hyapp.wallet.v1.GrantResourceGroupRequest
|
||||||
94, // 211: hyapp.wallet.v1.WalletService.GrantResourceGroup:input_type -> hyapp.wallet.v1.GrantResourceGroupRequest
|
95, // 211: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:input_type -> hyapp.wallet.v1.GrantPinnedResourceGroupRequest
|
||||||
95, // 212: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:input_type -> hyapp.wallet.v1.GrantPinnedResourceGroupRequest
|
96, // 212: hyapp.wallet.v1.WalletService.RevokeResourceGrant:input_type -> hyapp.wallet.v1.RevokeResourceGrantRequest
|
||||||
96, // 213: hyapp.wallet.v1.WalletService.RevokeResourceGrant:input_type -> hyapp.wallet.v1.RevokeResourceGrantRequest
|
298, // 213: hyapp.wallet.v1.WalletService.RevokeUserResource:input_type -> hyapp.wallet.v1.RevokeUserResourceRequest
|
||||||
298, // 214: hyapp.wallet.v1.WalletService.RevokeUserResource:input_type -> hyapp.wallet.v1.RevokeUserResourceRequest
|
98, // 214: hyapp.wallet.v1.WalletService.ListUserResources:input_type -> hyapp.wallet.v1.ListUserResourcesRequest
|
||||||
98, // 215: hyapp.wallet.v1.WalletService.ListUserResources:input_type -> hyapp.wallet.v1.ListUserResourcesRequest
|
100, // 215: hyapp.wallet.v1.WalletService.EquipUserResource:input_type -> hyapp.wallet.v1.EquipUserResourceRequest
|
||||||
100, // 216: hyapp.wallet.v1.WalletService.EquipUserResource:input_type -> hyapp.wallet.v1.EquipUserResourceRequest
|
102, // 216: hyapp.wallet.v1.WalletService.UnequipUserResource:input_type -> hyapp.wallet.v1.UnequipUserResourceRequest
|
||||||
102, // 217: hyapp.wallet.v1.WalletService.UnequipUserResource:input_type -> hyapp.wallet.v1.UnequipUserResourceRequest
|
104, // 217: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:input_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesRequest
|
||||||
104, // 218: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:input_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesRequest
|
107, // 218: hyapp.wallet.v1.WalletService.ListResourceGrants:input_type -> hyapp.wallet.v1.ListResourceGrantsRequest
|
||||||
107, // 219: hyapp.wallet.v1.WalletService.ListResourceGrants:input_type -> hyapp.wallet.v1.ListResourceGrantsRequest
|
110, // 219: hyapp.wallet.v1.WalletService.ListResourceShopItems:input_type -> hyapp.wallet.v1.ListResourceShopItemsRequest
|
||||||
110, // 220: hyapp.wallet.v1.WalletService.ListResourceShopItems:input_type -> hyapp.wallet.v1.ListResourceShopItemsRequest
|
112, // 220: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:input_type -> hyapp.wallet.v1.UpsertResourceShopItemsRequest
|
||||||
112, // 221: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:input_type -> hyapp.wallet.v1.UpsertResourceShopItemsRequest
|
114, // 221: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:input_type -> hyapp.wallet.v1.SetResourceShopItemStatusRequest
|
||||||
114, // 222: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:input_type -> hyapp.wallet.v1.SetResourceShopItemStatusRequest
|
275, // 222: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:input_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersRequest
|
||||||
275, // 223: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:input_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersRequest
|
116, // 223: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:input_type -> hyapp.wallet.v1.PurchaseResourceShopItemRequest
|
||||||
116, // 224: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:input_type -> hyapp.wallet.v1.PurchaseResourceShopItemRequest
|
119, // 224: hyapp.wallet.v1.WalletService.ListRechargeBills:input_type -> hyapp.wallet.v1.ListRechargeBillsRequest
|
||||||
119, // 225: hyapp.wallet.v1.WalletService.ListRechargeBills:input_type -> hyapp.wallet.v1.ListRechargeBillsRequest
|
121, // 225: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:input_type -> hyapp.wallet.v1.GetRechargeBillSummaryRequest
|
||||||
121, // 226: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:input_type -> hyapp.wallet.v1.GetRechargeBillSummaryRequest
|
125, // 226: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:input_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsRequest
|
||||||
125, // 227: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:input_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsRequest
|
127, // 227: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:input_type -> hyapp.wallet.v1.GetRechargeBillOverviewRequest
|
||||||
127, // 228: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:input_type -> hyapp.wallet.v1.GetRechargeBillOverviewRequest
|
133, // 228: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:input_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesRequest
|
||||||
133, // 229: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:input_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesRequest
|
137, // 229: hyapp.wallet.v1.WalletService.GetWalletOverview:input_type -> hyapp.wallet.v1.GetWalletOverviewRequest
|
||||||
137, // 230: hyapp.wallet.v1.WalletService.GetWalletOverview:input_type -> hyapp.wallet.v1.GetWalletOverviewRequest
|
140, // 230: hyapp.wallet.v1.WalletService.GetWalletValueSummary:input_type -> hyapp.wallet.v1.GetWalletValueSummaryRequest
|
||||||
140, // 231: hyapp.wallet.v1.WalletService.GetWalletValueSummary:input_type -> hyapp.wallet.v1.GetWalletValueSummaryRequest
|
143, // 231: hyapp.wallet.v1.WalletService.GetUserGiftWall:input_type -> hyapp.wallet.v1.GetUserGiftWallRequest
|
||||||
143, // 232: hyapp.wallet.v1.WalletService.GetUserGiftWall:input_type -> hyapp.wallet.v1.GetUserGiftWallRequest
|
146, // 232: hyapp.wallet.v1.WalletService.ListRechargeProducts:input_type -> hyapp.wallet.v1.ListRechargeProductsRequest
|
||||||
146, // 233: hyapp.wallet.v1.WalletService.ListRechargeProducts:input_type -> hyapp.wallet.v1.ListRechargeProductsRequest
|
148, // 233: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:input_type -> hyapp.wallet.v1.ConfirmGooglePaymentRequest
|
||||||
148, // 234: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:input_type -> hyapp.wallet.v1.ConfirmGooglePaymentRequest
|
150, // 234: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:input_type -> hyapp.wallet.v1.ListAdminRechargeProductsRequest
|
||||||
150, // 235: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:input_type -> hyapp.wallet.v1.ListAdminRechargeProductsRequest
|
152, // 235: hyapp.wallet.v1.WalletService.CreateRechargeProduct:input_type -> hyapp.wallet.v1.CreateRechargeProductRequest
|
||||||
152, // 236: hyapp.wallet.v1.WalletService.CreateRechargeProduct:input_type -> hyapp.wallet.v1.CreateRechargeProductRequest
|
153, // 236: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:input_type -> hyapp.wallet.v1.UpdateRechargeProductRequest
|
||||||
153, // 237: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:input_type -> hyapp.wallet.v1.UpdateRechargeProductRequest
|
154, // 237: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:input_type -> hyapp.wallet.v1.DeleteRechargeProductRequest
|
||||||
154, // 238: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:input_type -> hyapp.wallet.v1.DeleteRechargeProductRequest
|
159, // 238: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:input_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsRequest
|
||||||
159, // 239: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:input_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsRequest
|
161, // 239: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:input_type -> hyapp.wallet.v1.SetThirdPartyPaymentMethodStatusRequest
|
||||||
161, // 240: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:input_type -> hyapp.wallet.v1.SetThirdPartyPaymentMethodStatusRequest
|
163, // 240: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:input_type -> hyapp.wallet.v1.UpdateThirdPartyPaymentRateRequest
|
||||||
163, // 241: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:input_type -> hyapp.wallet.v1.UpdateThirdPartyPaymentRateRequest
|
165, // 241: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:input_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsRequest
|
||||||
165, // 242: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:input_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsRequest
|
167, // 242: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:input_type -> hyapp.wallet.v1.H5RechargeOptionsRequest
|
||||||
167, // 243: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:input_type -> hyapp.wallet.v1.H5RechargeOptionsRequest
|
170, // 243: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:input_type -> hyapp.wallet.v1.CreateH5RechargeOrderRequest
|
||||||
170, // 244: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:input_type -> hyapp.wallet.v1.CreateH5RechargeOrderRequest
|
171, // 244: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.CreateTemporaryRechargeOrderRequest
|
||||||
171, // 245: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.CreateTemporaryRechargeOrderRequest
|
172, // 245: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.GetTemporaryRechargeOrderRequest
|
||||||
172, // 246: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.GetTemporaryRechargeOrderRequest
|
173, // 246: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:input_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersRequest
|
||||||
173, // 247: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:input_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersRequest
|
175, // 247: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:input_type -> hyapp.wallet.v1.SubmitH5RechargeTxRequest
|
||||||
175, // 248: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:input_type -> hyapp.wallet.v1.SubmitH5RechargeTxRequest
|
176, // 248: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:input_type -> hyapp.wallet.v1.GetH5RechargeOrderRequest
|
||||||
176, // 249: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:input_type -> hyapp.wallet.v1.GetH5RechargeOrderRequest
|
178, // 249: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:input_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptRequest
|
||||||
178, // 250: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:input_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptRequest
|
180, // 250: hyapp.wallet.v1.WalletService.HandleMifapayNotify:input_type -> hyapp.wallet.v1.HandleMifapayNotifyRequest
|
||||||
180, // 251: hyapp.wallet.v1.WalletService.HandleMifapayNotify:input_type -> hyapp.wallet.v1.HandleMifapayNotifyRequest
|
182, // 251: hyapp.wallet.v1.WalletService.HandleV5PayNotify:input_type -> hyapp.wallet.v1.HandleV5PayNotifyRequest
|
||||||
182, // 252: hyapp.wallet.v1.WalletService.HandleV5PayNotify:input_type -> hyapp.wallet.v1.HandleV5PayNotifyRequest
|
185, // 252: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:input_type -> hyapp.wallet.v1.GetDiamondExchangeConfigRequest
|
||||||
185, // 253: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:input_type -> hyapp.wallet.v1.GetDiamondExchangeConfigRequest
|
188, // 253: hyapp.wallet.v1.WalletService.ListWalletTransactions:input_type -> hyapp.wallet.v1.ListWalletTransactionsRequest
|
||||||
188, // 254: hyapp.wallet.v1.WalletService.ListWalletTransactions:input_type -> hyapp.wallet.v1.ListWalletTransactionsRequest
|
198, // 254: hyapp.wallet.v1.WalletService.ListVipPackages:input_type -> hyapp.wallet.v1.ListVipPackagesRequest
|
||||||
198, // 255: hyapp.wallet.v1.WalletService.ListVipPackages:input_type -> hyapp.wallet.v1.ListVipPackagesRequest
|
210, // 255: hyapp.wallet.v1.WalletService.GetVipProgramConfig:input_type -> hyapp.wallet.v1.GetVipProgramConfigRequest
|
||||||
210, // 256: hyapp.wallet.v1.WalletService.GetVipProgramConfig:input_type -> hyapp.wallet.v1.GetVipProgramConfigRequest
|
200, // 256: hyapp.wallet.v1.WalletService.GetMyVip:input_type -> hyapp.wallet.v1.GetMyVipRequest
|
||||||
200, // 257: hyapp.wallet.v1.WalletService.GetMyVip:input_type -> hyapp.wallet.v1.GetMyVipRequest
|
232, // 257: hyapp.wallet.v1.WalletService.CheckVipBenefit:input_type -> hyapp.wallet.v1.CheckVipBenefitRequest
|
||||||
232, // 258: hyapp.wallet.v1.WalletService.CheckVipBenefit:input_type -> hyapp.wallet.v1.CheckVipBenefitRequest
|
202, // 258: hyapp.wallet.v1.WalletService.PurchaseVip:input_type -> hyapp.wallet.v1.PurchaseVipRequest
|
||||||
202, // 259: hyapp.wallet.v1.WalletService.PurchaseVip:input_type -> hyapp.wallet.v1.PurchaseVipRequest
|
204, // 259: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:input_type -> hyapp.wallet.v1.DebitCPBreakupFeeRequest
|
||||||
204, // 260: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:input_type -> hyapp.wallet.v1.DebitCPBreakupFeeRequest
|
206, // 260: hyapp.wallet.v1.WalletService.DebitWheelDraw:input_type -> hyapp.wallet.v1.DebitWheelDrawRequest
|
||||||
206, // 261: hyapp.wallet.v1.WalletService.DebitWheelDraw:input_type -> hyapp.wallet.v1.DebitWheelDrawRequest
|
208, // 261: hyapp.wallet.v1.WalletService.GrantVip:input_type -> hyapp.wallet.v1.GrantVipRequest
|
||||||
208, // 262: hyapp.wallet.v1.WalletService.GrantVip:input_type -> hyapp.wallet.v1.GrantVipRequest
|
226, // 262: hyapp.wallet.v1.WalletService.GrantVipTrialCard:input_type -> hyapp.wallet.v1.GrantVipTrialCardRequest
|
||||||
226, // 263: hyapp.wallet.v1.WalletService.GrantVipTrialCard:input_type -> hyapp.wallet.v1.GrantVipTrialCardRequest
|
228, // 263: hyapp.wallet.v1.WalletService.EquipVipTrialCard:input_type -> hyapp.wallet.v1.EquipVipTrialCardRequest
|
||||||
228, // 264: hyapp.wallet.v1.WalletService.EquipVipTrialCard:input_type -> hyapp.wallet.v1.EquipVipTrialCardRequest
|
230, // 264: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:input_type -> hyapp.wallet.v1.UnequipVipTrialCardRequest
|
||||||
230, // 265: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:input_type -> hyapp.wallet.v1.UnequipVipTrialCardRequest
|
235, // 265: hyapp.wallet.v1.WalletService.ListAdminVipLevels:input_type -> hyapp.wallet.v1.ListAdminVipLevelsRequest
|
||||||
235, // 266: hyapp.wallet.v1.WalletService.ListAdminVipLevels:input_type -> hyapp.wallet.v1.ListAdminVipLevelsRequest
|
237, // 266: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:input_type -> hyapp.wallet.v1.UpdateAdminVipLevelsRequest
|
||||||
237, // 267: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:input_type -> hyapp.wallet.v1.UpdateAdminVipLevelsRequest
|
212, // 267: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:input_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest
|
||||||
212, // 268: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:input_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest
|
214, // 268: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:input_type -> hyapp.wallet.v1.UpdateMyVipSettingsRequest
|
||||||
214, // 269: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:input_type -> hyapp.wallet.v1.UpdateMyVipSettingsRequest
|
220, // 269: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:input_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateRequest
|
||||||
220, // 270: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:input_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateRequest
|
222, // 270: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:input_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesRequest
|
||||||
222, // 271: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:input_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesRequest
|
224, // 271: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:input_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateRequest
|
||||||
224, // 272: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:input_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateRequest
|
239, // 272: hyapp.wallet.v1.WalletService.CreditTaskReward:input_type -> hyapp.wallet.v1.CreditTaskRewardRequest
|
||||||
239, // 273: hyapp.wallet.v1.WalletService.CreditTaskReward:input_type -> hyapp.wallet.v1.CreditTaskRewardRequest
|
241, // 273: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:input_type -> hyapp.wallet.v1.CreditLuckyGiftRewardRequest
|
||||||
241, // 274: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:input_type -> hyapp.wallet.v1.CreditLuckyGiftRewardRequest
|
243, // 274: hyapp.wallet.v1.WalletService.CreditWheelReward:input_type -> hyapp.wallet.v1.CreditWheelRewardRequest
|
||||||
243, // 275: hyapp.wallet.v1.WalletService.CreditWheelReward:input_type -> hyapp.wallet.v1.CreditWheelRewardRequest
|
245, // 275: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:input_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardRequest
|
||||||
245, // 276: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:input_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardRequest
|
247, // 276: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:input_type -> hyapp.wallet.v1.CreditInviteActivityRewardRequest
|
||||||
247, // 277: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:input_type -> hyapp.wallet.v1.CreditInviteActivityRewardRequest
|
249, // 277: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:input_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardRequest
|
||||||
249, // 278: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:input_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardRequest
|
251, // 278: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:input_type -> hyapp.wallet.v1.ApplyGameCoinChangeRequest
|
||||||
251, // 279: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:input_type -> hyapp.wallet.v1.ApplyGameCoinChangeRequest
|
256, // 279: hyapp.wallet.v1.WalletService.GetRedPacketConfig:input_type -> hyapp.wallet.v1.GetRedPacketConfigRequest
|
||||||
256, // 280: hyapp.wallet.v1.WalletService.GetRedPacketConfig:input_type -> hyapp.wallet.v1.GetRedPacketConfigRequest
|
258, // 280: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:input_type -> hyapp.wallet.v1.UpdateRedPacketConfigRequest
|
||||||
258, // 281: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:input_type -> hyapp.wallet.v1.UpdateRedPacketConfigRequest
|
260, // 281: hyapp.wallet.v1.WalletService.CreateRedPacket:input_type -> hyapp.wallet.v1.CreateRedPacketRequest
|
||||||
260, // 282: hyapp.wallet.v1.WalletService.CreateRedPacket:input_type -> hyapp.wallet.v1.CreateRedPacketRequest
|
262, // 282: hyapp.wallet.v1.WalletService.ClaimRedPacket:input_type -> hyapp.wallet.v1.ClaimRedPacketRequest
|
||||||
262, // 283: hyapp.wallet.v1.WalletService.ClaimRedPacket:input_type -> hyapp.wallet.v1.ClaimRedPacketRequest
|
264, // 283: hyapp.wallet.v1.WalletService.ListRedPackets:input_type -> hyapp.wallet.v1.ListRedPacketsRequest
|
||||||
264, // 284: hyapp.wallet.v1.WalletService.ListRedPackets:input_type -> hyapp.wallet.v1.ListRedPacketsRequest
|
266, // 284: hyapp.wallet.v1.WalletService.GetRedPacket:input_type -> hyapp.wallet.v1.GetRedPacketRequest
|
||||||
266, // 285: hyapp.wallet.v1.WalletService.GetRedPacket:input_type -> hyapp.wallet.v1.GetRedPacketRequest
|
268, // 285: hyapp.wallet.v1.WalletService.ExpireRedPackets:input_type -> hyapp.wallet.v1.ExpireRedPacketsRequest
|
||||||
268, // 286: hyapp.wallet.v1.WalletService.ExpireRedPackets:input_type -> hyapp.wallet.v1.ExpireRedPacketsRequest
|
270, // 286: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:input_type -> hyapp.wallet.v1.RetryRedPacketRefundRequest
|
||||||
270, // 287: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:input_type -> hyapp.wallet.v1.RetryRedPacketRefundRequest
|
273, // 287: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse
|
||||||
273, // 288: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse
|
273, // 288: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse
|
||||||
273, // 289: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse
|
273, // 289: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:output_type -> hyapp.wallet.v1.CronBatchResponse
|
||||||
273, // 290: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:output_type -> hyapp.wallet.v1.CronBatchResponse
|
219, // 290: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:output_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse
|
||||||
219, // 291: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:output_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse
|
2, // 291: hyapp.wallet.v1.WalletService.DebitGift:output_type -> hyapp.wallet.v1.DebitGiftResponse
|
||||||
2, // 292: hyapp.wallet.v1.WalletService.DebitGift:output_type -> hyapp.wallet.v1.DebitGiftResponse
|
2, // 292: hyapp.wallet.v1.WalletService.DebitDirectGift:output_type -> hyapp.wallet.v1.DebitGiftResponse
|
||||||
2, // 293: hyapp.wallet.v1.WalletService.DebitDirectGift:output_type -> hyapp.wallet.v1.DebitGiftResponse
|
6, // 293: hyapp.wallet.v1.WalletService.BatchDebitGift:output_type -> hyapp.wallet.v1.BatchDebitGiftResponse
|
||||||
6, // 294: hyapp.wallet.v1.WalletService.BatchDebitGift:output_type -> hyapp.wallet.v1.BatchDebitGiftResponse
|
2, // 294: hyapp.wallet.v1.WalletService.DebitRobotGift:output_type -> hyapp.wallet.v1.DebitGiftResponse
|
||||||
2, // 295: hyapp.wallet.v1.WalletService.DebitRobotGift:output_type -> hyapp.wallet.v1.DebitGiftResponse
|
10, // 295: hyapp.wallet.v1.WalletService.GetBalances:output_type -> hyapp.wallet.v1.GetBalancesResponse
|
||||||
10, // 296: hyapp.wallet.v1.WalletService.GetBalances:output_type -> hyapp.wallet.v1.GetBalancesResponse
|
14, // 296: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:output_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyResponse
|
||||||
14, // 297: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:output_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyResponse
|
17, // 297: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:output_type -> hyapp.wallet.v1.GetHostSalaryProgressResponse
|
||||||
17, // 298: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:output_type -> hyapp.wallet.v1.GetHostSalaryProgressResponse
|
281, // 298: hyapp.wallet.v1.WalletService.GetHostRevenueStats:output_type -> hyapp.wallet.v1.GetHostRevenueStatsResponse
|
||||||
281, // 299: hyapp.wallet.v1.WalletService.GetHostRevenueStats:output_type -> hyapp.wallet.v1.GetHostRevenueStatsResponse
|
284, // 299: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:output_type -> hyapp.wallet.v1.GetAgencyPointShareStatsResponse
|
||||||
284, // 300: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:output_type -> hyapp.wallet.v1.GetAgencyPointShareStatsResponse
|
287, // 300: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:output_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsResponse
|
||||||
287, // 301: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:output_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsResponse
|
20, // 301: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:output_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsResponse
|
||||||
20, // 302: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:output_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsResponse
|
22, // 302: hyapp.wallet.v1.WalletService.AdminCreditAsset:output_type -> hyapp.wallet.v1.AdminCreditAssetResponse
|
||||||
22, // 303: hyapp.wallet.v1.WalletService.AdminCreditAsset:output_type -> hyapp.wallet.v1.AdminCreditAssetResponse
|
24, // 303: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:output_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockResponse
|
||||||
24, // 304: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:output_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockResponse
|
26, // 304: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:output_type -> hyapp.wallet.v1.TransferCoinFromSellerResponse
|
||||||
26, // 305: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:output_type -> hyapp.wallet.v1.TransferCoinFromSellerResponse
|
28, // 305: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:output_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildResponse
|
||||||
28, // 306: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:output_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildResponse
|
31, // 306: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:output_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersResponse
|
||||||
31, // 307: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:output_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersResponse
|
33, // 307: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:output_type -> hyapp.wallet.v1.ExchangeSalaryToCoinResponse
|
||||||
33, // 308: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:output_type -> hyapp.wallet.v1.ExchangeSalaryToCoinResponse
|
35, // 308: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:output_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerResponse
|
||||||
35, // 309: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:output_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerResponse
|
290, // 309: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:output_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse
|
||||||
290, // 310: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:output_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse
|
292, // 310: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:output_type -> hyapp.wallet.v1.TransferPointToCoinSellerResponse
|
||||||
292, // 311: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:output_type -> hyapp.wallet.v1.TransferPointToCoinSellerResponse
|
294, // 311: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:output_type -> hyapp.wallet.v1.GetPointWithdrawalConfigResponse
|
||||||
294, // 312: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:output_type -> hyapp.wallet.v1.GetPointWithdrawalConfigResponse
|
37, // 312: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:output_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalResponse
|
||||||
37, // 313: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:output_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalResponse
|
39, // 313: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:output_type -> hyapp.wallet.v1.SettleSalaryWithdrawalResponse
|
||||||
39, // 314: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:output_type -> hyapp.wallet.v1.SettleSalaryWithdrawalResponse
|
41, // 314: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:output_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalResponse
|
||||||
41, // 315: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:output_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalResponse
|
43, // 315: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:output_type -> hyapp.wallet.v1.FreezePointWithdrawalResponse
|
||||||
43, // 316: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:output_type -> hyapp.wallet.v1.FreezePointWithdrawalResponse
|
45, // 316: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:output_type -> hyapp.wallet.v1.SettlePointWithdrawalResponse
|
||||||
45, // 317: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:output_type -> hyapp.wallet.v1.SettlePointWithdrawalResponse
|
47, // 317: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:output_type -> hyapp.wallet.v1.ReleasePointWithdrawalResponse
|
||||||
47, // 318: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:output_type -> hyapp.wallet.v1.ReleasePointWithdrawalResponse
|
62, // 318: hyapp.wallet.v1.WalletService.ListResources:output_type -> hyapp.wallet.v1.ListResourcesResponse
|
||||||
62, // 319: hyapp.wallet.v1.WalletService.ListResources:output_type -> hyapp.wallet.v1.ListResourcesResponse
|
64, // 319: hyapp.wallet.v1.WalletService.GetResource:output_type -> hyapp.wallet.v1.GetResourceResponse
|
||||||
64, // 320: hyapp.wallet.v1.WalletService.GetResource:output_type -> hyapp.wallet.v1.GetResourceResponse
|
71, // 320: hyapp.wallet.v1.WalletService.CreateResource:output_type -> hyapp.wallet.v1.ResourceResponse
|
||||||
71, // 321: hyapp.wallet.v1.WalletService.CreateResource:output_type -> hyapp.wallet.v1.ResourceResponse
|
71, // 321: hyapp.wallet.v1.WalletService.UpdateResource:output_type -> hyapp.wallet.v1.ResourceResponse
|
||||||
71, // 322: hyapp.wallet.v1.WalletService.UpdateResource:output_type -> hyapp.wallet.v1.ResourceResponse
|
71, // 322: hyapp.wallet.v1.WalletService.SetResourceStatus:output_type -> hyapp.wallet.v1.ResourceResponse
|
||||||
71, // 323: hyapp.wallet.v1.WalletService.SetResourceStatus:output_type -> hyapp.wallet.v1.ResourceResponse
|
71, // 323: hyapp.wallet.v1.WalletService.DeleteResource:output_type -> hyapp.wallet.v1.ResourceResponse
|
||||||
71, // 324: hyapp.wallet.v1.WalletService.DeleteResource:output_type -> hyapp.wallet.v1.ResourceResponse
|
70, // 324: hyapp.wallet.v1.WalletService.BatchDeleteResources:output_type -> hyapp.wallet.v1.BatchDeleteResourcesResponse
|
||||||
70, // 325: hyapp.wallet.v1.WalletService.BatchDeleteResources:output_type -> hyapp.wallet.v1.BatchDeleteResourcesResponse
|
73, // 325: hyapp.wallet.v1.WalletService.ListResourceGroups:output_type -> hyapp.wallet.v1.ListResourceGroupsResponse
|
||||||
73, // 326: hyapp.wallet.v1.WalletService.ListResourceGroups:output_type -> hyapp.wallet.v1.ListResourceGroupsResponse
|
75, // 326: hyapp.wallet.v1.WalletService.GetResourceGroup:output_type -> hyapp.wallet.v1.GetResourceGroupResponse
|
||||||
75, // 327: hyapp.wallet.v1.WalletService.GetResourceGroup:output_type -> hyapp.wallet.v1.GetResourceGroupResponse
|
79, // 327: hyapp.wallet.v1.WalletService.CreateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse
|
||||||
79, // 328: hyapp.wallet.v1.WalletService.CreateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse
|
79, // 328: hyapp.wallet.v1.WalletService.UpdateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse
|
||||||
79, // 329: hyapp.wallet.v1.WalletService.UpdateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse
|
79, // 329: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:output_type -> hyapp.wallet.v1.ResourceGroupResponse
|
||||||
79, // 330: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:output_type -> hyapp.wallet.v1.ResourceGroupResponse
|
53, // 330: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:output_type -> hyapp.wallet.v1.PinResourceGroupSnapshotResponse
|
||||||
53, // 331: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:output_type -> hyapp.wallet.v1.PinResourceGroupSnapshotResponse
|
81, // 331: hyapp.wallet.v1.WalletService.ListGiftConfigs:output_type -> hyapp.wallet.v1.ListGiftConfigsResponse
|
||||||
81, // 332: hyapp.wallet.v1.WalletService.ListGiftConfigs:output_type -> hyapp.wallet.v1.ListGiftConfigsResponse
|
83, // 332: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:output_type -> hyapp.wallet.v1.ListGiftTypeConfigsResponse
|
||||||
83, // 333: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:output_type -> hyapp.wallet.v1.ListGiftTypeConfigsResponse
|
278, // 333: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:output_type -> hyapp.wallet.v1.GetGiftCatalogVersionResponse
|
||||||
278, // 334: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:output_type -> hyapp.wallet.v1.GetGiftCatalogVersionResponse
|
92, // 334: hyapp.wallet.v1.WalletService.CreateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse
|
||||||
92, // 335: hyapp.wallet.v1.WalletService.CreateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse
|
88, // 335: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:output_type -> hyapp.wallet.v1.BatchCreateGiftConfigsResponse
|
||||||
88, // 336: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:output_type -> hyapp.wallet.v1.BatchCreateGiftConfigsResponse
|
92, // 336: hyapp.wallet.v1.WalletService.UpdateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse
|
||||||
92, // 337: hyapp.wallet.v1.WalletService.UpdateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse
|
92, // 337: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:output_type -> hyapp.wallet.v1.GiftConfigResponse
|
||||||
92, // 338: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:output_type -> hyapp.wallet.v1.GiftConfigResponse
|
92, // 338: hyapp.wallet.v1.WalletService.DeleteGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse
|
||||||
92, // 339: hyapp.wallet.v1.WalletService.DeleteGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse
|
85, // 339: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:output_type -> hyapp.wallet.v1.GiftTypeConfigResponse
|
||||||
85, // 340: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:output_type -> hyapp.wallet.v1.GiftTypeConfigResponse
|
97, // 340: hyapp.wallet.v1.WalletService.GrantResource:output_type -> hyapp.wallet.v1.ResourceGrantResponse
|
||||||
97, // 341: hyapp.wallet.v1.WalletService.GrantResource:output_type -> hyapp.wallet.v1.ResourceGrantResponse
|
97, // 341: hyapp.wallet.v1.WalletService.GrantResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse
|
||||||
97, // 342: hyapp.wallet.v1.WalletService.GrantResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse
|
97, // 342: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse
|
||||||
97, // 343: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse
|
97, // 343: hyapp.wallet.v1.WalletService.RevokeResourceGrant:output_type -> hyapp.wallet.v1.ResourceGrantResponse
|
||||||
97, // 344: hyapp.wallet.v1.WalletService.RevokeResourceGrant:output_type -> hyapp.wallet.v1.ResourceGrantResponse
|
299, // 344: hyapp.wallet.v1.WalletService.RevokeUserResource:output_type -> hyapp.wallet.v1.RevokeUserResourceResponse
|
||||||
299, // 345: hyapp.wallet.v1.WalletService.RevokeUserResource:output_type -> hyapp.wallet.v1.RevokeUserResourceResponse
|
99, // 345: hyapp.wallet.v1.WalletService.ListUserResources:output_type -> hyapp.wallet.v1.ListUserResourcesResponse
|
||||||
99, // 346: hyapp.wallet.v1.WalletService.ListUserResources:output_type -> hyapp.wallet.v1.ListUserResourcesResponse
|
101, // 346: hyapp.wallet.v1.WalletService.EquipUserResource:output_type -> hyapp.wallet.v1.EquipUserResourceResponse
|
||||||
101, // 347: hyapp.wallet.v1.WalletService.EquipUserResource:output_type -> hyapp.wallet.v1.EquipUserResourceResponse
|
103, // 347: hyapp.wallet.v1.WalletService.UnequipUserResource:output_type -> hyapp.wallet.v1.UnequipUserResourceResponse
|
||||||
103, // 348: hyapp.wallet.v1.WalletService.UnequipUserResource:output_type -> hyapp.wallet.v1.UnequipUserResourceResponse
|
106, // 348: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:output_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesResponse
|
||||||
106, // 349: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:output_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesResponse
|
108, // 349: hyapp.wallet.v1.WalletService.ListResourceGrants:output_type -> hyapp.wallet.v1.ListResourceGrantsResponse
|
||||||
108, // 350: hyapp.wallet.v1.WalletService.ListResourceGrants:output_type -> hyapp.wallet.v1.ListResourceGrantsResponse
|
111, // 350: hyapp.wallet.v1.WalletService.ListResourceShopItems:output_type -> hyapp.wallet.v1.ListResourceShopItemsResponse
|
||||||
111, // 351: hyapp.wallet.v1.WalletService.ListResourceShopItems:output_type -> hyapp.wallet.v1.ListResourceShopItemsResponse
|
113, // 351: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:output_type -> hyapp.wallet.v1.UpsertResourceShopItemsResponse
|
||||||
113, // 352: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:output_type -> hyapp.wallet.v1.UpsertResourceShopItemsResponse
|
115, // 352: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:output_type -> hyapp.wallet.v1.ResourceShopItemResponse
|
||||||
115, // 353: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:output_type -> hyapp.wallet.v1.ResourceShopItemResponse
|
276, // 353: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:output_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse
|
||||||
276, // 354: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:output_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse
|
117, // 354: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:output_type -> hyapp.wallet.v1.PurchaseResourceShopItemResponse
|
||||||
117, // 355: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:output_type -> hyapp.wallet.v1.PurchaseResourceShopItemResponse
|
120, // 355: hyapp.wallet.v1.WalletService.ListRechargeBills:output_type -> hyapp.wallet.v1.ListRechargeBillsResponse
|
||||||
120, // 356: hyapp.wallet.v1.WalletService.ListRechargeBills:output_type -> hyapp.wallet.v1.ListRechargeBillsResponse
|
123, // 356: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:output_type -> hyapp.wallet.v1.GetRechargeBillSummaryResponse
|
||||||
123, // 357: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:output_type -> hyapp.wallet.v1.GetRechargeBillSummaryResponse
|
126, // 357: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:output_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsResponse
|
||||||
126, // 358: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:output_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsResponse
|
132, // 358: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:output_type -> hyapp.wallet.v1.GetRechargeBillOverviewResponse
|
||||||
132, // 359: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:output_type -> hyapp.wallet.v1.GetRechargeBillOverviewResponse
|
135, // 359: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:output_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesResponse
|
||||||
135, // 360: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:output_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesResponse
|
138, // 360: hyapp.wallet.v1.WalletService.GetWalletOverview:output_type -> hyapp.wallet.v1.GetWalletOverviewResponse
|
||||||
138, // 361: hyapp.wallet.v1.WalletService.GetWalletOverview:output_type -> hyapp.wallet.v1.GetWalletOverviewResponse
|
141, // 361: hyapp.wallet.v1.WalletService.GetWalletValueSummary:output_type -> hyapp.wallet.v1.GetWalletValueSummaryResponse
|
||||||
141, // 362: hyapp.wallet.v1.WalletService.GetWalletValueSummary:output_type -> hyapp.wallet.v1.GetWalletValueSummaryResponse
|
144, // 362: hyapp.wallet.v1.WalletService.GetUserGiftWall:output_type -> hyapp.wallet.v1.GetUserGiftWallResponse
|
||||||
144, // 363: hyapp.wallet.v1.WalletService.GetUserGiftWall:output_type -> hyapp.wallet.v1.GetUserGiftWallResponse
|
147, // 363: hyapp.wallet.v1.WalletService.ListRechargeProducts:output_type -> hyapp.wallet.v1.ListRechargeProductsResponse
|
||||||
147, // 364: hyapp.wallet.v1.WalletService.ListRechargeProducts:output_type -> hyapp.wallet.v1.ListRechargeProductsResponse
|
149, // 364: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:output_type -> hyapp.wallet.v1.ConfirmGooglePaymentResponse
|
||||||
149, // 365: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:output_type -> hyapp.wallet.v1.ConfirmGooglePaymentResponse
|
151, // 365: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:output_type -> hyapp.wallet.v1.ListAdminRechargeProductsResponse
|
||||||
151, // 366: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:output_type -> hyapp.wallet.v1.ListAdminRechargeProductsResponse
|
155, // 366: hyapp.wallet.v1.WalletService.CreateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse
|
||||||
155, // 367: hyapp.wallet.v1.WalletService.CreateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse
|
155, // 367: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse
|
||||||
155, // 368: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse
|
156, // 368: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:output_type -> hyapp.wallet.v1.DeleteRechargeProductResponse
|
||||||
156, // 369: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:output_type -> hyapp.wallet.v1.DeleteRechargeProductResponse
|
160, // 369: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:output_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsResponse
|
||||||
160, // 370: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:output_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsResponse
|
162, // 370: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse
|
||||||
162, // 371: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse
|
162, // 371: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse
|
||||||
162, // 372: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse
|
166, // 372: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:output_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsResponse
|
||||||
166, // 373: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:output_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsResponse
|
168, // 373: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:output_type -> hyapp.wallet.v1.H5RechargeOptionsResponse
|
||||||
168, // 374: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:output_type -> hyapp.wallet.v1.H5RechargeOptionsResponse
|
177, // 374: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse
|
||||||
177, // 375: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse
|
177, // 375: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse
|
||||||
177, // 376: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse
|
177, // 376: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse
|
||||||
177, // 377: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse
|
174, // 377: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:output_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersResponse
|
||||||
174, // 378: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:output_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersResponse
|
177, // 378: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse
|
||||||
177, // 379: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse
|
177, // 379: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse
|
||||||
177, // 380: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse
|
179, // 380: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:output_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptResponse
|
||||||
179, // 381: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:output_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptResponse
|
181, // 381: hyapp.wallet.v1.WalletService.HandleMifapayNotify:output_type -> hyapp.wallet.v1.HandleMifapayNotifyResponse
|
||||||
181, // 382: hyapp.wallet.v1.WalletService.HandleMifapayNotify:output_type -> hyapp.wallet.v1.HandleMifapayNotifyResponse
|
183, // 382: hyapp.wallet.v1.WalletService.HandleV5PayNotify:output_type -> hyapp.wallet.v1.HandleV5PayNotifyResponse
|
||||||
183, // 383: hyapp.wallet.v1.WalletService.HandleV5PayNotify:output_type -> hyapp.wallet.v1.HandleV5PayNotifyResponse
|
186, // 383: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:output_type -> hyapp.wallet.v1.GetDiamondExchangeConfigResponse
|
||||||
186, // 384: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:output_type -> hyapp.wallet.v1.GetDiamondExchangeConfigResponse
|
189, // 384: hyapp.wallet.v1.WalletService.ListWalletTransactions:output_type -> hyapp.wallet.v1.ListWalletTransactionsResponse
|
||||||
189, // 385: hyapp.wallet.v1.WalletService.ListWalletTransactions:output_type -> hyapp.wallet.v1.ListWalletTransactionsResponse
|
199, // 385: hyapp.wallet.v1.WalletService.ListVipPackages:output_type -> hyapp.wallet.v1.ListVipPackagesResponse
|
||||||
199, // 386: hyapp.wallet.v1.WalletService.ListVipPackages:output_type -> hyapp.wallet.v1.ListVipPackagesResponse
|
211, // 386: hyapp.wallet.v1.WalletService.GetVipProgramConfig:output_type -> hyapp.wallet.v1.GetVipProgramConfigResponse
|
||||||
211, // 387: hyapp.wallet.v1.WalletService.GetVipProgramConfig:output_type -> hyapp.wallet.v1.GetVipProgramConfigResponse
|
201, // 387: hyapp.wallet.v1.WalletService.GetMyVip:output_type -> hyapp.wallet.v1.GetMyVipResponse
|
||||||
201, // 388: hyapp.wallet.v1.WalletService.GetMyVip:output_type -> hyapp.wallet.v1.GetMyVipResponse
|
233, // 388: hyapp.wallet.v1.WalletService.CheckVipBenefit:output_type -> hyapp.wallet.v1.CheckVipBenefitResponse
|
||||||
233, // 389: hyapp.wallet.v1.WalletService.CheckVipBenefit:output_type -> hyapp.wallet.v1.CheckVipBenefitResponse
|
203, // 389: hyapp.wallet.v1.WalletService.PurchaseVip:output_type -> hyapp.wallet.v1.PurchaseVipResponse
|
||||||
203, // 390: hyapp.wallet.v1.WalletService.PurchaseVip:output_type -> hyapp.wallet.v1.PurchaseVipResponse
|
205, // 390: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:output_type -> hyapp.wallet.v1.DebitCPBreakupFeeResponse
|
||||||
205, // 391: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:output_type -> hyapp.wallet.v1.DebitCPBreakupFeeResponse
|
207, // 391: hyapp.wallet.v1.WalletService.DebitWheelDraw:output_type -> hyapp.wallet.v1.DebitWheelDrawResponse
|
||||||
207, // 392: hyapp.wallet.v1.WalletService.DebitWheelDraw:output_type -> hyapp.wallet.v1.DebitWheelDrawResponse
|
209, // 392: hyapp.wallet.v1.WalletService.GrantVip:output_type -> hyapp.wallet.v1.GrantVipResponse
|
||||||
209, // 393: hyapp.wallet.v1.WalletService.GrantVip:output_type -> hyapp.wallet.v1.GrantVipResponse
|
227, // 393: hyapp.wallet.v1.WalletService.GrantVipTrialCard:output_type -> hyapp.wallet.v1.GrantVipTrialCardResponse
|
||||||
227, // 394: hyapp.wallet.v1.WalletService.GrantVipTrialCard:output_type -> hyapp.wallet.v1.GrantVipTrialCardResponse
|
229, // 394: hyapp.wallet.v1.WalletService.EquipVipTrialCard:output_type -> hyapp.wallet.v1.EquipVipTrialCardResponse
|
||||||
229, // 395: hyapp.wallet.v1.WalletService.EquipVipTrialCard:output_type -> hyapp.wallet.v1.EquipVipTrialCardResponse
|
231, // 395: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:output_type -> hyapp.wallet.v1.UnequipVipTrialCardResponse
|
||||||
231, // 396: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:output_type -> hyapp.wallet.v1.UnequipVipTrialCardResponse
|
236, // 396: hyapp.wallet.v1.WalletService.ListAdminVipLevels:output_type -> hyapp.wallet.v1.ListAdminVipLevelsResponse
|
||||||
236, // 397: hyapp.wallet.v1.WalletService.ListAdminVipLevels:output_type -> hyapp.wallet.v1.ListAdminVipLevelsResponse
|
238, // 397: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:output_type -> hyapp.wallet.v1.UpdateAdminVipLevelsResponse
|
||||||
238, // 398: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:output_type -> hyapp.wallet.v1.UpdateAdminVipLevelsResponse
|
213, // 398: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:output_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse
|
||||||
213, // 399: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:output_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse
|
215, // 399: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:output_type -> hyapp.wallet.v1.UpdateMyVipSettingsResponse
|
||||||
215, // 400: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:output_type -> hyapp.wallet.v1.UpdateMyVipSettingsResponse
|
221, // 400: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:output_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse
|
||||||
221, // 401: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:output_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse
|
223, // 401: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:output_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse
|
||||||
223, // 402: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:output_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse
|
225, // 402: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:output_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse
|
||||||
225, // 403: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:output_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse
|
240, // 403: hyapp.wallet.v1.WalletService.CreditTaskReward:output_type -> hyapp.wallet.v1.CreditTaskRewardResponse
|
||||||
240, // 404: hyapp.wallet.v1.WalletService.CreditTaskReward:output_type -> hyapp.wallet.v1.CreditTaskRewardResponse
|
242, // 404: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:output_type -> hyapp.wallet.v1.CreditLuckyGiftRewardResponse
|
||||||
242, // 405: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:output_type -> hyapp.wallet.v1.CreditLuckyGiftRewardResponse
|
244, // 405: hyapp.wallet.v1.WalletService.CreditWheelReward:output_type -> hyapp.wallet.v1.CreditWheelRewardResponse
|
||||||
244, // 406: hyapp.wallet.v1.WalletService.CreditWheelReward:output_type -> hyapp.wallet.v1.CreditWheelRewardResponse
|
246, // 406: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:output_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardResponse
|
||||||
246, // 407: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:output_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardResponse
|
248, // 407: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:output_type -> hyapp.wallet.v1.CreditInviteActivityRewardResponse
|
||||||
248, // 408: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:output_type -> hyapp.wallet.v1.CreditInviteActivityRewardResponse
|
250, // 408: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:output_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardResponse
|
||||||
250, // 409: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:output_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardResponse
|
252, // 409: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:output_type -> hyapp.wallet.v1.ApplyGameCoinChangeResponse
|
||||||
252, // 410: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:output_type -> hyapp.wallet.v1.ApplyGameCoinChangeResponse
|
257, // 410: hyapp.wallet.v1.WalletService.GetRedPacketConfig:output_type -> hyapp.wallet.v1.GetRedPacketConfigResponse
|
||||||
257, // 411: hyapp.wallet.v1.WalletService.GetRedPacketConfig:output_type -> hyapp.wallet.v1.GetRedPacketConfigResponse
|
259, // 411: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:output_type -> hyapp.wallet.v1.UpdateRedPacketConfigResponse
|
||||||
259, // 412: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:output_type -> hyapp.wallet.v1.UpdateRedPacketConfigResponse
|
261, // 412: hyapp.wallet.v1.WalletService.CreateRedPacket:output_type -> hyapp.wallet.v1.CreateRedPacketResponse
|
||||||
261, // 413: hyapp.wallet.v1.WalletService.CreateRedPacket:output_type -> hyapp.wallet.v1.CreateRedPacketResponse
|
263, // 413: hyapp.wallet.v1.WalletService.ClaimRedPacket:output_type -> hyapp.wallet.v1.ClaimRedPacketResponse
|
||||||
263, // 414: hyapp.wallet.v1.WalletService.ClaimRedPacket:output_type -> hyapp.wallet.v1.ClaimRedPacketResponse
|
265, // 414: hyapp.wallet.v1.WalletService.ListRedPackets:output_type -> hyapp.wallet.v1.ListRedPacketsResponse
|
||||||
265, // 415: hyapp.wallet.v1.WalletService.ListRedPackets:output_type -> hyapp.wallet.v1.ListRedPacketsResponse
|
267, // 415: hyapp.wallet.v1.WalletService.GetRedPacket:output_type -> hyapp.wallet.v1.GetRedPacketResponse
|
||||||
267, // 416: hyapp.wallet.v1.WalletService.GetRedPacket:output_type -> hyapp.wallet.v1.GetRedPacketResponse
|
269, // 416: hyapp.wallet.v1.WalletService.ExpireRedPackets:output_type -> hyapp.wallet.v1.ExpireRedPacketsResponse
|
||||||
269, // 417: hyapp.wallet.v1.WalletService.ExpireRedPackets:output_type -> hyapp.wallet.v1.ExpireRedPacketsResponse
|
271, // 417: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:output_type -> hyapp.wallet.v1.RetryRedPacketRefundResponse
|
||||||
271, // 418: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:output_type -> hyapp.wallet.v1.RetryRedPacketRefundResponse
|
287, // [287:418] is the sub-list for method output_type
|
||||||
288, // [288:419] is the sub-list for method output_type
|
156, // [156:287] is the sub-list for method input_type
|
||||||
157, // [157:288] is the sub-list for method input_type
|
156, // [156:156] is the sub-list for extension type_name
|
||||||
157, // [157:157] is the sub-list for extension type_name
|
156, // [156:156] is the sub-list for extension extendee
|
||||||
157, // [157:157] is the sub-list for extension extendee
|
0, // [0:156] is the sub-list for field type_name
|
||||||
0, // [0:157] is the sub-list for field type_name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_proto_wallet_v1_wallet_proto_init() }
|
func init() { file_proto_wallet_v1_wallet_proto_init() }
|
||||||
|
|||||||
@ -201,8 +201,6 @@ message HostSalaryPolicy {
|
|||||||
int64 effective_from_ms = 9;
|
int64 effective_from_ms = 9;
|
||||||
int64 effective_to_ms = 10;
|
int64 effective_to_ms = 10;
|
||||||
repeated HostSalaryPolicyLevel levels = 11;
|
repeated HostSalaryPolicyLevel levels = 11;
|
||||||
string cycle_key = 12;
|
|
||||||
uint64 policy_version = 13;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetActiveHostSalaryPolicyRequest {
|
message GetActiveHostSalaryPolicyRequest {
|
||||||
@ -212,8 +210,6 @@ message GetActiveHostSalaryPolicyRequest {
|
|||||||
string settlement_mode = 4;
|
string settlement_mode = 4;
|
||||||
string settlement_trigger_mode = 5;
|
string settlement_trigger_mode = 5;
|
||||||
int64 now_ms = 6;
|
int64 now_ms = 6;
|
||||||
// cycle_key 精确选择该工资周期已绑定的不可变政策版本;空值仅供旧客户端回退当前 UTC 月。
|
|
||||||
string cycle_key = 7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetActiveHostSalaryPolicyResponse {
|
message GetActiveHostSalaryPolicyResponse {
|
||||||
@ -1143,8 +1139,6 @@ message EquipUserResourceRequest {
|
|||||||
int64 user_id = 3;
|
int64 user_id = 3;
|
||||||
int64 resource_id = 4;
|
int64 resource_id = 4;
|
||||||
string entitlement_id = 5;
|
string entitlement_id = 5;
|
||||||
// command_id 是装备状态变更的业务幂等键;request_id 只用于链路追踪。
|
|
||||||
string command_id = 6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message EquipUserResourceResponse {
|
message EquipUserResourceResponse {
|
||||||
@ -2034,9 +2028,6 @@ message VipBenefit {
|
|||||||
int64 created_at_ms = 13;
|
int64 created_at_ms = 13;
|
||||||
int64 updated_at_ms = 14;
|
int64 updated_at_ms = 14;
|
||||||
VipBenefitPresentation presentation = 15;
|
VipBenefitPresentation presentation = 15;
|
||||||
// resource 仅在 resource_id 指向同 App、active 且 resource_type 匹配的真实资源时返回。
|
|
||||||
// 运营尚未配置素材的权益保留 resource_id/resource_type 事实,但该对象为空,客户端不得猜 URL。
|
|
||||||
Resource resource = 16;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message VipLevel {
|
message VipLevel {
|
||||||
@ -2413,10 +2404,6 @@ message CheckVipBenefitResponse {
|
|||||||
VipState state = 3;
|
VipState state = 3;
|
||||||
string denial_reason = 4;
|
string denial_reason = 4;
|
||||||
int64 evaluated_at_ms = 5;
|
int64 evaluated_at_ms = 5;
|
||||||
// required_level 是当前 App 所有 active 等级中首次显式配置该权益的等级;0 表示未配置。
|
|
||||||
int32 required_level = 6;
|
|
||||||
// current_effective_level 来自 wallet 合并付费会员与体验卡后的最终状态。
|
|
||||||
int32 current_effective_level = 7;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdminVipLevelInput {
|
message AdminVipLevelInput {
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
brokerClusterName=DefaultCluster
|
|
||||||
brokerName=broker-a
|
|
||||||
brokerId=0
|
|
||||||
listenPort=10911
|
|
||||||
brokerIP1=127.0.0.1
|
|
||||||
deleteWhen=04
|
|
||||||
fileReservedTime=48
|
|
||||||
brokerRole=ASYNC_MASTER
|
|
||||||
flushDiskType=ASYNC_FLUSH
|
|
||||||
autoCreateTopicEnable=true
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,163 +0,0 @@
|
|||||||
# 个人信息页用户上麦状态 Flutter 对接
|
|
||||||
|
|
||||||
## 1. 接口职责
|
|
||||||
|
|
||||||
个人信息页通过本接口判断被查看用户当前是否占用语音房麦位,并取得可直接交给统一进房入口的房间信息。
|
|
||||||
|
|
||||||
- 上麦事实只来自 `room-service` 当前 Room Cell 快照;不要用用户是否为房主、房间搜索结果、IM 本地状态或历史缓存推断。
|
|
||||||
- 接口返回的是带时间戳的瞬时快照;点击 Live 后仍以 `POST /api/v1/rooms/join` 的实时校验为准。
|
|
||||||
- 当前接口仅用于客态个人信息页。请求中包含当前登录用户本人时,该项固定返回 `not_on_mic`。
|
|
||||||
|
|
||||||
## 2. 请求
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/v1/users/voice-room-presence:batch?user_ids=163020,163021
|
|
||||||
Authorization: Bearer {access_token}
|
|
||||||
```
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `user_ids` | string | 是 | 逗号分隔的系统 `user_id`,不接受展示短号 |
|
|
||||||
|
|
||||||
约束:
|
|
||||||
|
|
||||||
- 去重后最多 100 个 ID,响应顺序按请求中第一次出现的顺序返回。
|
|
||||||
- 空值、非数字、`0`、负数、空分段或超过上限都会整批返回 `INVALID_USER_IDS`。
|
|
||||||
- `viewer_user_id`、`app_code` 和 `request_id` 由 gateway 从登录态和请求上下文注入,客户端不能提交。
|
|
||||||
- 接口要求有效登录态且用户已完成资料。
|
|
||||||
|
|
||||||
## 3. 成功响应
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "OK",
|
|
||||||
"message": "ok",
|
|
||||||
"request_id": "req_xxx",
|
|
||||||
"data": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"user_id": "163020",
|
|
||||||
"state": "on_mic",
|
|
||||||
"seat_no": 3,
|
|
||||||
"joinable": true,
|
|
||||||
"observed_at_ms": 1784265600000,
|
|
||||||
"room": {
|
|
||||||
"room_id": "100086",
|
|
||||||
"im_group_id": "100086",
|
|
||||||
"rtc_room_id": "100086",
|
|
||||||
"room_short_id": "886688",
|
|
||||||
"title": "Room name",
|
|
||||||
"owner_user_id": "12001",
|
|
||||||
"host_user_id": "12001",
|
|
||||||
"mode": "voice",
|
|
||||||
"cover_url": "https://cdn.example.com/room-cover.png",
|
|
||||||
"background_url": "https://cdn.example.com/room-bg.png",
|
|
||||||
"online_count": 56,
|
|
||||||
"seat_count": 10,
|
|
||||||
"locked": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"user_id": "163021",
|
|
||||||
"state": "not_on_mic",
|
|
||||||
"seat_no": 0,
|
|
||||||
"joinable": false,
|
|
||||||
"observed_at_ms": 1784265600000,
|
|
||||||
"room": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"server_time_ms": 1784265600123
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
字段说明:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `items[].user_id` | string | 系统用户 ID;JSON 中按字符串返回 |
|
|
||||||
| `items[].state` | string | `on_mic` 或 `not_on_mic` |
|
|
||||||
| `items[].seat_no` | int | 当前麦位编号;未上麦为 `0` |
|
|
||||||
| `items[].joinable` | bool | 当前 viewer 是否允许通过该入口尝试进房 |
|
|
||||||
| `items[].observed_at_ms` | int64 | 本批状态完成判定时的 UTC epoch ms |
|
|
||||||
| `items[].room` | object/null | 仅 `on_mic && joinable` 时非空 |
|
|
||||||
| `server_time_ms` | int64 | 服务端本批响应时间,UTC epoch ms |
|
|
||||||
|
|
||||||
房间字段可以直接构造现有 `VoiceRoomEntryArgs`:
|
|
||||||
|
|
||||||
- `room_id` 是业务房间主键。
|
|
||||||
- `im_group_id` 当前等于 `room_id`。
|
|
||||||
- `rtc_room_id` 当前使用 TRTC string room ID,等于 `room_id`。
|
|
||||||
- 当前房间模型没有独立主持人字段,`host_user_id` 与 `owner_user_id` 一致。
|
|
||||||
- `locked=true` 表示密码房;点击后继续复用现有密码输入和进房流程,不能在个人页自行索取或缓存密码。
|
|
||||||
|
|
||||||
## 4. Flutter 展示和刷新规则
|
|
||||||
|
|
||||||
只有下面四个条件全部成立时显示 Live:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final showLive = item.state == 'on_mic' &&
|
|
||||||
item.joinable &&
|
|
||||||
item.room != null &&
|
|
||||||
item.room!.roomId.isNotEmpty;
|
|
||||||
```
|
|
||||||
|
|
||||||
点击时:
|
|
||||||
|
|
||||||
1. 调用现有 `VoiceRoomNavigation.open()`。
|
|
||||||
2. 用 `room` 构造 `VoiceRoomEntryArgs`。
|
|
||||||
3. `entrySource` 使用 `profile_card_live_badge`。
|
|
||||||
4. 正常调用 `POST /api/v1/rooms/join`;如果用户已经下麦但房间仍允许进入,可以正常进入。
|
|
||||||
5. 进房返回房间关闭、封禁、密码错误等业务错误时,移除当前 Live 状态并按现有错误规则提示。
|
|
||||||
|
|
||||||
刷新时机:
|
|
||||||
|
|
||||||
- 首次进入客态个人信息页。
|
|
||||||
- App 从后台回到前台且个人信息页仍可见。
|
|
||||||
- 从语音房返回个人信息页。
|
|
||||||
- 用户手动刷新个人信息页。
|
|
||||||
|
|
||||||
不要轮询、持久化或使用上一次成功结果兜底。接口失败时只隐藏 Live,不影响个人信息页其他资料。
|
|
||||||
|
|
||||||
## 5. 后端状态边界
|
|
||||||
|
|
||||||
- `on_mic` 表示房间仍为 `active`、目标用户仍在房间在线集合中,并且占用一个当前有效麦位。`MicUp` 后的 `pending_publish` 和 RTC 已确认的 `publishing` 都属于已占麦。
|
|
||||||
- 房主或管理员只有实际占用麦位时才算上麦;身份本身不算。
|
|
||||||
- 同一 App 的 `room_user_presence` 使用 `(app_code, user_id)` 主键定位候选房间,最终状态再由 Room Cell 快照确认。
|
|
||||||
- 跨 `app_code` 房间不会返回。
|
|
||||||
- viewer 被目标房间封禁、房间已关闭、presence 已滞后或快照事实不完整时,统一返回 `not_on_mic + room:null`,不泄露旧房间 ID。
|
|
||||||
- 密码房允许展示 Live,返回 `locked=true`,正式进房仍校验密码。
|
|
||||||
- 当前房间产品模型没有“隐藏房/隐身上麦”字段,用户关系也没有资料位置黑名单能力;本版不做不存在的状态推断。以后增加此类产品能力时,必须在后端权限判断中收敛后再返回,Flutter 不得自行判断。
|
|
||||||
|
|
||||||
## 6. 错误响应
|
|
||||||
|
|
||||||
| HTTP | code | 处理方式 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 400 | `INVALID_USER_IDS` | 修正参数;不要重试同一请求 |
|
|
||||||
| 401 | `UNAUTHORIZED` / `ACCESS_TOKEN_EXPIRED` | 走现有登录或刷新 token 流程 |
|
|
||||||
| 403 | `PROFILE_REQUIRED` | 先完成用户资料 |
|
|
||||||
| 503 | `VOICE_ROOM_PRESENCE_UNAVAILABLE` | 隐藏 Live;页面其他内容继续展示 |
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "INVALID_USER_IDS",
|
|
||||||
"message": "invalid user ids",
|
|
||||||
"request_id": "req_xxx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
排障日志只记录接口路径、目标 ID 数量、响应业务码和 `request_id`;不要记录 access token 或密码房密码。
|
|
||||||
|
|
||||||
## 7. 联调清单
|
|
||||||
|
|
||||||
- 未进房、只在房内未上麦:不显示 Live。
|
|
||||||
- 在自己的房间或其他人的房间实际占麦:显示 Live,并返回实际所在房间。
|
|
||||||
- `pending_publish`、`publishing`:都显示 Live。
|
|
||||||
- 下麦、离房、被踢或关房后重新请求:返回 `not_on_mic`。
|
|
||||||
- 密码房:显示 Live,点击后进入既有密码流程。
|
|
||||||
- viewer 被目标房间封禁、跨 App、查询本人:不返回房间定位。
|
|
||||||
- 一批中混合上麦、未上麦和重复用户:每个去重用户一条结果,顺序稳定。
|
|
||||||
- 查询成功后立即下麦:点击仍以正式进房接口结果为准。
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -35,7 +35,6 @@
|
|||||||
| GET | `/api/v1/users/by-display-user-id/{display_user_id}` | users | `resolveDisplayUserID` | 通过当前展示短号解析用户 |
|
| GET | `/api/v1/users/by-display-user-id/{display_user_id}` | users | `resolveDisplayUserID` | 通过当前展示短号解析用户 |
|
||||||
| GET | `/api/v1/users/profiles:batch` | users | `batchUserProfiles` | 批量查询会话展示资料 |
|
| GET | `/api/v1/users/profiles:batch` | users | `batchUserProfiles` | 批量查询会话展示资料 |
|
||||||
| GET | `/api/v1/users/room-display-profiles:batch` | users | `batchRoomDisplayProfiles` | 批量查询房间和公屏展示资料 |
|
| GET | `/api/v1/users/room-display-profiles:batch` | users | `batchRoomDisplayProfiles` | 批量查询房间和公屏展示资料 |
|
||||||
| GET | `/api/v1/users/voice-room-presence:batch` | users | `batchUserVoiceRoomPresences` | 批量查询个人信息页可展示的用户上麦入口 |
|
|
||||||
| GET | `/api/v1/users/me/identity` | users | `getMyIdentity` | 查询当前用户展示短号状态 |
|
| GET | `/api/v1/users/me/identity` | users | `getMyIdentity` | 查询当前用户展示短号状态 |
|
||||||
| GET | `/api/v1/users/me/host-identity` | users | `getMyHostIdentity` | 查询当前用户 Host/Agency/BD 身份 |
|
| GET | `/api/v1/users/me/host-identity` | users | `getMyHostIdentity` | 查询当前用户 Host/Agency/BD 身份 |
|
||||||
| GET | `/api/v1/users/me/overview` | users | `getMyOverview` | 我的页首屏聚合摘要 |
|
| GET | `/api/v1/users/me/overview` | users | `getMyOverview` | 我的页首屏聚合摘要 |
|
||||||
|
|||||||
@ -49,9 +49,6 @@
|
|||||||
| `real_room_robot_total_gift_coin` | 真人房机器人总礼物 | 上面三类机器人礼物金币合计。 |
|
| `real_room_robot_total_gift_coin` | 真人房机器人总礼物 | 上面三类机器人礼物金币合计。 |
|
||||||
| `real_room_robot_gift_room_count` | 真人房机器人投放房间数 | 有真人房机器人礼物的房间去重数。 |
|
| `real_room_robot_gift_room_count` | 真人房机器人投放房间数 | 有真人房机器人礼物的房间去重数。 |
|
||||||
| `real_room_robot_avg_room_gift_coin` | 真人房机器人房均投放 | `real_room_robot_total_gift_coin / real_room_robot_gift_room_count`。 |
|
| `real_room_robot_avg_room_gift_coin` | 真人房机器人房均投放 | `real_room_robot_total_gift_coin / real_room_robot_gift_room_count`。 |
|
||||||
|
|
||||||
> 2026-07-17 起 robot 送礼改为纯腾讯云 IM 展示,不再写 room/wallet 数据库或发布 MQ。`real_room_robot_*` 字段只保留历史兼容;新事件不会再增长,不能继续作为当前机器人投放口径。
|
|
||||||
|
|
||||||
| `lucky_gift_turnover` | lucky 礼物流水 | `RoomGiftSent.pool_id` 非空的送礼金币。 |
|
| `lucky_gift_turnover` | lucky 礼物流水 | `RoomGiftSent.pool_id` 非空的送礼金币。 |
|
||||||
| `lucky_gift_payout` | lucky 礼物产出 | `WalletLuckyGiftRewardCredited.amount`,UTC 查询会优先用 activity 的 lucky pool 日表覆盖旧统计行。 |
|
| `lucky_gift_payout` | lucky 礼物产出 | `WalletLuckyGiftRewardCredited.amount`,UTC 查询会优先用 activity 的 lucky pool 日表覆盖旧统计行。 |
|
||||||
| `lucky_gift_payers` | lucky 礼物付费用户 | 发送 lucky 礼物的用户按日去重。 |
|
| `lucky_gift_payers` | lucky 礼物付费用户 | 发送 lucky 礼物的用户按日去重。 |
|
||||||
|
|||||||
@ -37,9 +37,11 @@
|
|||||||
|
|
||||||
### wallet-service
|
### wallet-service
|
||||||
|
|
||||||
- 只通过 `ListGiftConfigs` 提供 active、区域可用的礼物展示快照。
|
- 机器人礼物扣费入口:`DebitRobotGift`。
|
||||||
- `RobotSendGift` 不调用 `DebitRobotGift`,不创建机器人钱包账户、交易、分录或 wallet outbox。
|
- 使用专用资产 `ROBOT_COIN`。
|
||||||
- 礼物目录在 room-service 按 App/区域短缓存;缓存只保存配置,不保存任何送礼事实。
|
- 自动补足机器人专用金币后扣减。
|
||||||
|
- 不发出真实 `WalletGiftDebited` 事件。
|
||||||
|
- 不写主播周期钻石和真实礼物墙。
|
||||||
|
|
||||||
### activity/statistics
|
### activity/statistics
|
||||||
|
|
||||||
@ -99,7 +101,7 @@ room-service gRPC:
|
|||||||
|
|
||||||
wallet-service gRPC:
|
wallet-service gRPC:
|
||||||
|
|
||||||
- `ListGiftConfigs`(只读)
|
- `DebitRobotGift`
|
||||||
|
|
||||||
## 创建流程
|
## 创建流程
|
||||||
|
|
||||||
@ -153,22 +155,24 @@ room-service 启动时会扫描所有 active 机器人房间并恢复运行时
|
|||||||
|
|
||||||
room-service 在机器人送礼时强校验:
|
room-service 在机器人送礼时强校验:
|
||||||
|
|
||||||
- 专属机器人房必须带 `robot_room=true`;真人房机器人使用运行时机器人池校验,不把真人加入允许集合。
|
- 当前房间必须是机器人房间。
|
||||||
- sender 必须在该机器人房间 `robot_user_ids_json` 内。
|
- sender 必须在该机器人房间 `robot_user_ids_json` 内。
|
||||||
- target 必须在同一机器人房间 `robot_user_ids_json` 内。
|
- target 必须在同一机器人房间 `robot_user_ids_json` 内。
|
||||||
- sender 不能给真人送礼。
|
- sender 不能给真人送礼。
|
||||||
- 当前机器人礼物只允许单目标调用。
|
- 当前机器人礼物只允许单目标调用。
|
||||||
|
|
||||||
## 纯展示隔离
|
## 钱包隔离
|
||||||
|
|
||||||
`RobotSendGift` 不进入通用 `sendGift/mutateRoom`,只执行:
|
机器人礼物使用 `DebitRobotGift`:
|
||||||
|
|
||||||
1. 获得或续租 Redis owner lease,并只读当前 Room Cell(缺失时从 MySQL 只读恢复),校验 sender、target 和机器人池。
|
- 资产类型固定为 `ROBOT_COIN`。
|
||||||
2. 从 wallet 礼物目录读取并短缓存名称、图标、动画、类型和展示价值。
|
- 如果机器人 `ROBOT_COIN` 不足,同一事务内自动补足。
|
||||||
3. 在内存构造 `RoomGiftSent`;幸运礼物额外构造 `RoomRobotLuckyGiftDrawn`。
|
- 立即扣减本次礼物价格。
|
||||||
4. 经过 2 秒展示采样后,best-effort 直发腾讯云 IM。
|
- 交易类型为 `robot_gift_debit`。
|
||||||
|
- 不写真实 `gift_debit`。
|
||||||
这条链路不写 `room_gift_operations`、`room_command_log`、`room_snapshots`、`room_outbox`、房间贡献统计、钱包账本或任何 outbox,也不调用 RocketMQ producer。内存里的 `outbox.Record` 只复用 protobuf 信封结构,不是持久化 outbox。
|
- 不发 `WalletGiftDebited`。
|
||||||
|
- 不写 `user_gift_wall`。
|
||||||
|
- 不写 `host_period_diamond_accounts`。
|
||||||
|
|
||||||
## 幸运礼物隔离
|
## 幸运礼物隔离
|
||||||
|
|
||||||
@ -185,14 +189,25 @@ room-service 只生成展示事件:
|
|||||||
|
|
||||||
## 排行榜与统计隔离
|
## 排行榜与统计隔离
|
||||||
|
|
||||||
机器人展示用 `RoomGiftSent` 保留:
|
`RoomGiftSent` 增加:
|
||||||
|
|
||||||
- `is_robot_gift`
|
- `is_robot_gift`
|
||||||
- `synthetic_lucky_gift`
|
- `synthetic_lucky_gift`
|
||||||
|
|
||||||
该事件不会进入 room outbox/MQ,所以下游活动、CP、任务、成长和 statistics 根本不会收到机器人送礼事实;`is_robot_gift=true` 只供 IM 展示兼容。
|
下游服务看到 `is_robot_gift=true` 时跳过:
|
||||||
|
|
||||||
机器人送礼同时不修改房间热度、礼物榜、麦位 `gift_value`、火箭燃料、周贡献或 Redis 跨房贡献榜。真人房机器人也遵守同一边界;历史 `RealRoomHeat` 参数仅用于区分参与者校验范围,不能授权真实贡献。
|
- 活动播报统计
|
||||||
|
- 周星
|
||||||
|
- 房间流水奖励
|
||||||
|
- 每日任务
|
||||||
|
- 成长值
|
||||||
|
- statistics 礼物消费与幸运礼物统计
|
||||||
|
|
||||||
|
room-service 本地仍允许机器人房内:
|
||||||
|
|
||||||
|
- 房间热度增长
|
||||||
|
- 麦位热度增长
|
||||||
|
- 房内展示事件发送
|
||||||
|
|
||||||
## 本地接口测试
|
## 本地接口测试
|
||||||
|
|
||||||
@ -217,7 +232,7 @@ go run ./tools/robot-room-api-test
|
|||||||
5. 创建机器人房间。
|
5. 创建机器人房间。
|
||||||
6. 查询 active 列表。
|
6. 查询 active 列表。
|
||||||
7. 停止、启动、再次停止机器人房间。
|
7. 停止、启动、再次停止机器人房间。
|
||||||
8. 校验 robot 展示前后 `room_gift_operations`、command log、snapshot、room outbox、房间统计行数不变,且 `DebitRobotGift` 调用次数为 0。
|
8. 校验数据库中机器人房间配置和真实 `gift_debit` 隔离。
|
||||||
|
|
||||||
## 运维注意事项
|
## 运维注意事项
|
||||||
|
|
||||||
@ -226,4 +241,3 @@ go run ./tools/robot-room-api-test
|
|||||||
- 同一机器人账号不能被多个 active 机器人房间占用。
|
- 同一机器人账号不能被多个 active 机器人房间占用。
|
||||||
- 停止机器人房间只停止 runtime,不删除房间。
|
- 停止机器人房间只停止 runtime,不删除房间。
|
||||||
- 如果需要彻底释放机器人账号,应增加删除/归档能力后再开放。
|
- 如果需要彻底释放机器人账号,应增加删除/归档能力后再开放。
|
||||||
- 腾讯云 IM 是唯一实时展示通道,失败不补写数据库或 MQ,也不重试成业务事实;丢失的动画/公屏不恢复,房间快照只保证真实热度、榜单和麦位值不被机器人展示污染。
|
|
||||||
|
|||||||
@ -260,10 +260,6 @@ func UnaryServerInterceptor(service string) grpc.UnaryServerInterceptor {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// gRPC access 日志是跨服务排障入口;reason 只给稳定错误码,message 保留服务端校验分支,便于定位 400/409 的真实拒绝原因。
|
// gRPC access 日志是跨服务排障入口;reason 只给稳定错误码,message 保留服务端校验分支,便于定位 400/409 的真实拒绝原因。
|
||||||
attrs = append(attrs, slog.String("error_message", grpcErrorMessage(err)))
|
attrs = append(attrs, slog.String("error_message", grpcErrorMessage(err)))
|
||||||
if cause := xerr.CauseFromGRPC(err); cause != nil {
|
|
||||||
// Internal/Unknown 对客户端固定脱敏成 "internal error",error_cause 是服务端唯一能看到原始错误链的地方。
|
|
||||||
attrs = append(attrs, slog.String("error_cause", cause.Error()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
attrs = appendServiceAttr(attrs, service)
|
attrs = appendServiceAttr(attrs, service)
|
||||||
attrs = append(attrs, commonAttrsFromPayload(request)...)
|
attrs = append(attrs, commonAttrsFromPayload(request)...)
|
||||||
|
|||||||
@ -5,17 +5,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
userv1 "hyapp.local/api/proto/user/v1"
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
|
||||||
"hyapp/pkg/xerr"
|
|
||||||
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
@ -131,39 +127,6 @@ func TestUnaryServerInterceptorLevelsAndRequestMeta(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestUnaryServerInterceptorLogsOriginalCauseForInternal 锁定 2026-07-12 排障改进:
|
|
||||||
// Internal 类错误客户端只见 "internal error",但 grpc_access ERROR 日志必须带脱敏前的原始错误链。
|
|
||||||
func TestUnaryServerInterceptorLogsOriginalCauseForInternal(t *testing.T) {
|
|
||||||
var output bytes.Buffer
|
|
||||||
if err := Init(Config{Service: "room-service", Env: "test", Format: "json", Output: &output}); err != nil {
|
|
||||||
t.Fatalf("Init failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
interceptor := UnaryServerInterceptor("room-service")
|
|
||||||
request := &userv1.GetUserRequest{Meta: &userv1.RequestMeta{RequestId: "req-grpc-2", AppCode: "lalu"}, UserId: 1001}
|
|
||||||
_, err := interceptor(context.Background(), request, &grpc.UnaryServerInfo{FullMethod: "/hyapp.room.v1.RoomCommandService/MicHeartbeat"}, func(ctx context.Context, req any) (any, error) {
|
|
||||||
return nil, xerr.ToGRPCError(fmt.Errorf("refresh mic presence: %w", errors.New("redis: connection refused")))
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("interceptor should return handler error")
|
|
||||||
}
|
|
||||||
if st, ok := status.FromError(err); !ok || st.Message() != "internal error" {
|
|
||||||
t.Fatalf("client-facing error must stay sanitized: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := decodeOne(t, output.Bytes())
|
|
||||||
if entry["level"] != "ERROR" || entry["msg"] != "grpc_access" || entry["request_id"] != "req-grpc-2" {
|
|
||||||
t.Fatalf("unexpected grpc log entry: %+v", entry)
|
|
||||||
}
|
|
||||||
if entry["error_message"] != "internal error" {
|
|
||||||
t.Fatalf("error_message=%v, want sanitized message", entry["error_message"])
|
|
||||||
}
|
|
||||||
cause, _ := entry["error_cause"].(string)
|
|
||||||
if !strings.Contains(cause, "refresh mic presence") || !strings.Contains(cause, "redis: connection refused") {
|
|
||||||
t.Fatalf("error_cause=%q, want original error chain", cause)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnaryServerInterceptorExtractsTopLevelTraceFieldsWithoutBody(t *testing.T) {
|
func TestUnaryServerInterceptorExtractsTopLevelTraceFieldsWithoutBody(t *testing.T) {
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
if err := Init(Config{Service: "wallet-service", Env: "test", Format: "json", Output: &output}); err != nil {
|
if err := Init(Config{Service: "wallet-service", Env: "test", Format: "json", Output: &output}); err != nil {
|
||||||
|
|||||||
@ -86,26 +86,3 @@ func (u *Uploader) PutObject(ctx context.Context, key string, reader io.Reader,
|
|||||||
|
|
||||||
return publicURL, nil
|
return publicURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OwnsObjectURL 证明 URL 正好属于当前配置的 access_url 且对应同一个 object key。
|
|
||||||
// gateway 在接受客户端回传媒体元数据时必须调用它,不能只相信 https 或 URL 后缀。
|
|
||||||
func (u *Uploader) OwnsObjectURL(objectURL string, objectKey string) bool {
|
|
||||||
if u == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
objectKey = strings.TrimLeft(strings.TrimSpace(objectKey), "/")
|
|
||||||
if objectKey == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
expected, err := url.JoinPath(strings.TrimRight(u.accessURL, "/"), objectKey)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
actualURL, actualErr := url.Parse(strings.TrimSpace(objectURL))
|
|
||||||
expectedURL, expectedErr := url.Parse(expected)
|
|
||||||
if actualErr != nil || expectedErr != nil || actualURL.Scheme != "https" || actualURL.RawQuery != "" || actualURL.Fragment != "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.EqualFold(actualURL.Scheme, expectedURL.Scheme) && strings.EqualFold(actualURL.Host, expectedURL.Host) &&
|
|
||||||
actualURL.EscapedPath() == expectedURL.EscapedPath()
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@ -289,10 +288,7 @@ func (c *RESTClient) PublishGroupCustomMessage(ctx context.Context, message Cust
|
|||||||
|
|
||||||
request := sendGroupMsgRequest{
|
request := sendGroupMsgRequest{
|
||||||
GroupID: message.GroupID,
|
GroupID: message.GroupID,
|
||||||
// 腾讯云群消息会在短窗口内按 Random + 完整消息内容去重。outbox 的 event_id
|
Random: randomUint32(),
|
||||||
// 对同一业务事实稳定,因此从它派生 Random 可收敛“腾讯已成功、本地尚未标记”后的补偿重试;
|
|
||||||
// 客户端仍应按 payload.event_id 去重,以覆盖超出腾讯去重窗口后的极端重放。
|
|
||||||
Random: stableMessageRandom(message.EventID),
|
|
||||||
CloudCustomData: string(payload),
|
CloudCustomData: string(payload),
|
||||||
FromAccount: message.FromAccount,
|
FromAccount: message.FromAccount,
|
||||||
MsgBody: []messageElement{
|
MsgBody: []messageElement{
|
||||||
@ -316,11 +312,6 @@ func (c *RESTClient) PublishGroupCustomMessage(ctx context.Context, message Cust
|
|||||||
return response.err()
|
return response.err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func stableMessageRandom(eventID string) uint32 {
|
|
||||||
sum := sha256.Sum256([]byte(strings.TrimSpace(eventID)))
|
|
||||||
return binary.BigEndian.Uint32(sum[:4])
|
|
||||||
}
|
|
||||||
|
|
||||||
// PublishUserCustomMessage 向单个腾讯云 IM identifier 发送 TIMCustomElem。
|
// PublishUserCustomMessage 向单个腾讯云 IM identifier 发送 TIMCustomElem。
|
||||||
// 该方法只负责投递协议;业务幂等、隐私字段和重试策略必须由调用方在 outbox 中保证。
|
// 该方法只负责投递协议;业务幂等、隐私字段和重试策略必须由调用方在 outbox 中保证。
|
||||||
func (c *RESTClient) PublishUserCustomMessage(ctx context.Context, message CustomUserMessage) error {
|
func (c *RESTClient) PublishUserCustomMessage(ctx context.Context, message CustomUserMessage) error {
|
||||||
|
|||||||
@ -35,7 +35,6 @@ const (
|
|||||||
EventTypeWalletRedPacketClaimed = "WalletRedPacketClaimed"
|
EventTypeWalletRedPacketClaimed = "WalletRedPacketClaimed"
|
||||||
EventTypeWalletRedPacketRefunded = "WalletRedPacketRefunded"
|
EventTypeWalletRedPacketRefunded = "WalletRedPacketRefunded"
|
||||||
EventTypeVIPActivated = "VipActivated"
|
EventTypeVIPActivated = "VipActivated"
|
||||||
EventTypeVIPProgramChanged = "VipProgramChanged"
|
|
||||||
EventTypeVIPDailyCoinRebateAvailable = "VipDailyCoinRebateAvailable"
|
EventTypeVIPDailyCoinRebateAvailable = "VipDailyCoinRebateAvailable"
|
||||||
EventTypeResourceGranted = "ResourceGranted"
|
EventTypeResourceGranted = "ResourceGranted"
|
||||||
EventTypeResourceGroupGranted = "ResourceGroupGranted"
|
EventTypeResourceGroupGranted = "ResourceGroupGranted"
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package xerr
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Code 是服务内部稳定错误原因。
|
// Code 是服务内部稳定错误原因。
|
||||||
@ -175,10 +174,6 @@ const (
|
|||||||
type Error struct {
|
type Error struct {
|
||||||
Code Code
|
Code Code
|
||||||
Message string
|
Message string
|
||||||
// Metadata 只保存业务层显式声明可跨服务、可返回客户端的短字段。
|
|
||||||
// 原始错误、SQL、配置和第三方响应不得写入这里;ToGRPCError 会把它投影到
|
|
||||||
// google.rpc.ErrorInfo.metadata,gateway 再原样写入 envelope.data。
|
|
||||||
Metadata map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error 实现标准 error 接口。
|
// Error 实现标准 error 接口。
|
||||||
@ -200,17 +195,6 @@ func New(code Code, message string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWithMetadata 构造带安全结构化详情的业务错误。
|
|
||||||
// metadata 是显式的 wire contract,不接受空键;键和值都会去除首尾空白并复制,
|
|
||||||
// 防止调用方后续修改 map 导致同一个错误在日志与 wire 上出现不同语义。
|
|
||||||
func NewWithMetadata(code Code, message string, metadata map[string]string) error {
|
|
||||||
return &Error{
|
|
||||||
Code: code,
|
|
||||||
Message: message,
|
|
||||||
Metadata: normalizeMetadata(metadata),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// As 提取当前仓库约定的业务错误。
|
// As 提取当前仓库约定的业务错误。
|
||||||
func As(err error) (*Error, bool) {
|
func As(err error) (*Error, bool) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -250,41 +234,3 @@ func MessageOf(err error) string {
|
|||||||
|
|
||||||
return "internal error"
|
return "internal error"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetadataOf 返回业务错误显式声明的安全 metadata 副本。
|
|
||||||
// 未识别错误没有可安全暴露的结构化详情,必须返回 nil 而不是猜测原始错误内容。
|
|
||||||
func MetadataOf(err error) map[string]string {
|
|
||||||
if typed, ok := As(err); ok {
|
|
||||||
return cloneMetadata(typed.Metadata)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeMetadata(metadata map[string]string) map[string]string {
|
|
||||||
if len(metadata) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
normalized := make(map[string]string, len(metadata))
|
|
||||||
for key, value := range metadata {
|
|
||||||
key = strings.TrimSpace(key)
|
|
||||||
if key == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
normalized[key] = strings.TrimSpace(value)
|
|
||||||
}
|
|
||||||
if len(normalized) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneMetadata(metadata map[string]string) map[string]string {
|
|
||||||
if len(metadata) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cloned := make(map[string]string, len(metadata))
|
|
||||||
for key, value := range metadata {
|
|
||||||
cloned[key] = value
|
|
||||||
}
|
|
||||||
return cloned
|
|
||||||
}
|
|
||||||
|
|||||||
@ -47,46 +47,14 @@ func ToGRPCError(err error) error {
|
|||||||
detail := &errdetails.ErrorInfo{
|
detail := &errdetails.ErrorInfo{
|
||||||
Reason: string(domainErr.Code),
|
Reason: string(domainErr.Code),
|
||||||
Domain: errorDomain,
|
Domain: errorDomain,
|
||||||
Metadata: cloneMetadata(domainErr.Metadata),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if withDetails, detailErr := st.WithDetails(detail); detailErr == nil {
|
withDetails, detailErr := st.WithDetails(detail)
|
||||||
st = withDetails
|
if detailErr != nil {
|
||||||
|
return st.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &grpcError{status: st, cause: err}
|
return withDetails.Err()
|
||||||
}
|
|
||||||
|
|
||||||
// grpcError 对 grpc-go 暴露脱敏后的 status,同时保留脱敏前的原始错误链。
|
|
||||||
// 客户端从 GRPCStatus/Error 只能看到 catalog 文案;原始链只有服务端进程内
|
|
||||||
// 通过 CauseFromGRPC 可达,不会进 wire。
|
|
||||||
type grpcError struct {
|
|
||||||
status *status.Status
|
|
||||||
cause error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *grpcError) Error() string {
|
|
||||||
return e.status.Err().Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GRPCStatus 让 status.FromError 和 grpc-go server 识别脱敏 status,而不是降级成 Unknown。
|
|
||||||
func (e *grpcError) GRPCStatus() *status.Status {
|
|
||||||
return e.status
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap 保留原始错误链,供服务端 errors.Is/As 与 access 日志定位根因。
|
|
||||||
func (e *grpcError) Unwrap() error {
|
|
||||||
return e.cause
|
|
||||||
}
|
|
||||||
|
|
||||||
// CauseFromGRPC 取回 ToGRPCError 脱敏前的原始错误链,仅用于服务端日志;
|
|
||||||
// 对已经是 gRPC status 的透传错误返回 nil。禁止把返回值写回给客户端。
|
|
||||||
func CauseFromGRPC(err error) error {
|
|
||||||
var typed *grpcError
|
|
||||||
if errors.As(err, &typed) {
|
|
||||||
return typed.cause
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReasonFromGRPC 供 gateway 或测试从 gRPC error detail 中恢复稳定 reason。
|
// ReasonFromGRPC 供 gateway 或测试从 gRPC error detail 中恢复稳定 reason。
|
||||||
@ -110,25 +78,6 @@ func ReasonFromGRPC(err error) Code {
|
|||||||
return codeFromGRPC(st.Code())
|
return codeFromGRPC(st.Code())
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetadataFromGRPC 只读取本项目 ErrorInfo 中由 owner service 显式声明的安全字段。
|
|
||||||
// 其它 detail/domain 不属于公开 HTTP 契约,gateway 不得把它们混入 envelope.data。
|
|
||||||
func MetadataFromGRPC(err error) map[string]string {
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
st, ok := status.FromError(err)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, detail := range st.Details() {
|
|
||||||
info, ok := detail.(*errdetails.ErrorInfo)
|
|
||||||
if ok && info.GetDomain() == errorDomain && info.GetReason() != "" {
|
|
||||||
return cloneMetadata(info.GetMetadata())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// codeFromGRPC 是没有 ErrorInfo detail 时的降级映射。
|
// codeFromGRPC 是没有 ErrorInfo detail 时的降级映射。
|
||||||
func codeFromGRPC(code codes.Code) Code {
|
func codeFromGRPC(code codes.Code) Code {
|
||||||
switch code {
|
switch code {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package xerr
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/ast"
|
"go/ast"
|
||||||
"go/parser"
|
"go/parser"
|
||||||
@ -55,37 +54,6 @@ func TestToGRPCErrorPreservesContextTermination(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestToGRPCErrorSanitizesButPreservesCause 锁定 2026-07-12 排障改进:
|
|
||||||
// 非 xerr 错误对客户端仍固定脱敏成 internal error,但服务端能通过 CauseFromGRPC 取回原始错误链。
|
|
||||||
func TestToGRPCErrorSanitizesButPreservesCause(t *testing.T) {
|
|
||||||
rootCause := errors.New("redis: connection refused")
|
|
||||||
err := ToGRPCError(fmt.Errorf("acquire room lease: %w", rootCause))
|
|
||||||
|
|
||||||
st, ok := status.FromError(err)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected grpc status")
|
|
||||||
}
|
|
||||||
if st.Code() != codes.Internal || st.Message() != "internal error" {
|
|
||||||
t.Fatalf("client-facing status must stay sanitized: code=%s message=%q", st.Code(), st.Message())
|
|
||||||
}
|
|
||||||
if err.Error() != st.Err().Error() {
|
|
||||||
t.Fatalf("Error() must not leak cause: %q", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
cause := CauseFromGRPC(err)
|
|
||||||
if cause == nil || !errors.Is(cause, rootCause) {
|
|
||||||
t.Fatalf("cause chain lost: got %v", cause)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 透传已有 status 的错误没有被脱敏,不应报告 cause。
|
|
||||||
if got := CauseFromGRPC(status.Error(codes.NotFound, "room not found")); got != nil {
|
|
||||||
t.Fatalf("passthrough status should have no cause, got %v", got)
|
|
||||||
}
|
|
||||||
if got := ToGRPCError(err); got != err {
|
|
||||||
t.Fatalf("ToGRPCError must be idempotent on wrapped errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCatalogCoversDeclaredCodes(t *testing.T) {
|
func TestCatalogCoversDeclaredCodes(t *testing.T) {
|
||||||
declared := declaredCodesFromSource(t)
|
declared := declaredCodesFromSource(t)
|
||||||
|
|
||||||
|
|||||||
@ -36,10 +36,7 @@ INCREMENTAL_MIGRATION_FILES=(
|
|||||||
"services/user-service/deploy/mysql/migrations/012_login_audit_client_version.sql"
|
"services/user-service/deploy/mysql/migrations/012_login_audit_client_version.sql"
|
||||||
"services/user-service/deploy/mysql/migrations/017_cp_application_gift_coin_value.sql"
|
"services/user-service/deploy/mysql/migrations/017_cp_application_gift_coin_value.sql"
|
||||||
"services/user-service/deploy/mysql/migrations/018_auth_refresh_token_rotation.sql"
|
"services/user-service/deploy/mysql/migrations/018_auth_refresh_token_rotation.sql"
|
||||||
"services/user-service/deploy/mysql/migrations/019_user_profile_anonymous_visits.sql"
|
|
||||||
"services/room-service/deploy/mysql/migrations/003_room_vip_media_and_decorations.sql"
|
|
||||||
"services/wallet-service/deploy/mysql/migrations/002_resource_shop_price_tiers.sql"
|
"services/wallet-service/deploy/mysql/migrations/002_resource_shop_price_tiers.sql"
|
||||||
"services/wallet-service/deploy/mysql/migrations/003_vip_resource_equipment_contract.sql"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Keep MySQL as the only required dependency for this script. Business services
|
# Keep MySQL as the only required dependency for this script. Business services
|
||||||
|
|||||||
@ -1,258 +0,0 @@
|
|||||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
USE hyapp_wallet;
|
|
||||||
|
|
||||||
-- 装备幂等表是 append-only 小行写入;所有重放只按主键 (app_code,command_id) 点查,
|
|
||||||
-- 不修改 user_resource_equipment 主键,也不会为上线迁移扫描用户装备或 entitlement 大表。
|
|
||||||
CREATE TABLE IF NOT EXISTS user_resource_equipment_commands (
|
|
||||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
|
||||||
command_id VARCHAR(128) NOT NULL COMMENT '装备业务幂等 ID',
|
|
||||||
request_hash CHAR(64) NOT NULL COMMENT '用户、资源和指定 entitlement 的请求语义哈希',
|
|
||||||
user_id BIGINT NOT NULL COMMENT '操作用户 ID',
|
|
||||||
resource_id BIGINT NOT NULL COMMENT '目标资源 ID',
|
|
||||||
requested_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '客户端指定的 entitlement;空表示服务端选择',
|
|
||||||
result_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '首个成功命中的 entitlement',
|
|
||||||
resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '首个成功结果的资源类型',
|
|
||||||
response_json JSON NULL COMMENT '首个成功 UserResourceEntitlement 回包快照',
|
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '首次命令时间,UTC epoch ms',
|
|
||||||
updated_at_ms BIGINT NOT NULL COMMENT '结果完成时间,UTC epoch ms',
|
|
||||||
PRIMARY KEY (app_code, command_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资源装备命令幂等事实表';
|
|
||||||
|
|
||||||
-- 配置矩阵很小,历史表按 admin config_version 追加;装备和 appearance 只按资源覆盖索引点查。
|
|
||||||
-- 记录 program_type,避免 legacy_timed App 的普通资源被 tiered VIP 门禁误伤。
|
|
||||||
CREATE TABLE IF NOT EXISTS vip_benefit_resource_binding_history (
|
|
||||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
|
||||||
history_id CHAR(64) NOT NULL COMMENT '配置版本、阶段、等级和权益的稳定哈希',
|
|
||||||
config_version BIGINT NOT NULL COMMENT '写入该快照的 VIP 配置版本',
|
|
||||||
change_phase VARCHAR(16) NOT NULL COMMENT 'baseline/before/after',
|
|
||||||
program_type VARCHAR(32) NOT NULL COMMENT '快照时的 VIP program 类型',
|
|
||||||
level INT NOT NULL COMMENT 'VIP 等级',
|
|
||||||
benefit_code VARCHAR(64) NOT NULL COMMENT '权益编码',
|
|
||||||
benefit_status VARCHAR(32) NOT NULL COMMENT '权益快照状态',
|
|
||||||
trial_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '体验卡是否可用快照',
|
|
||||||
resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '当时绑定资源 ID;0 表示未绑定',
|
|
||||||
resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '当时要求的资源类型',
|
|
||||||
auto_equip BOOLEAN NOT NULL DEFAULT FALSE COMMENT '当时自动装备策略',
|
|
||||||
recorded_at_ms BIGINT NOT NULL COMMENT '记录时间,UTC epoch ms',
|
|
||||||
PRIMARY KEY (app_code, history_id),
|
|
||||||
UNIQUE KEY uk_vip_binding_history_snapshot (app_code, config_version, change_phase, level, benefit_code),
|
|
||||||
KEY idx_vip_binding_history_resource (app_code, resource_id, program_type, benefit_code, level)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 权益资源绑定不可变历史';
|
|
||||||
|
|
||||||
-- 装备门禁按单 App、单 resource_id 查询 active 绑定。该配置表规模远小于业务流水,
|
|
||||||
-- 仍显式补覆盖索引并使用在线 DDL,避免每次装备扫描完整权益矩阵。
|
|
||||||
SET @ddl := IF(
|
|
||||||
(SELECT COUNT(*) FROM information_schema.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_level_benefits'
|
|
||||||
AND INDEX_NAME = 'idx_vip_benefit_resource') = 0,
|
|
||||||
'ALTER TABLE vip_level_benefits ADD INDEX idx_vip_benefit_resource (app_code, resource_id, status, benefit_code, level), ALGORITHM=INPLACE, LOCK=NONE',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @vip_resource_contract_now_ms := CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
|
||||||
|
|
||||||
-- 在任何修正规则执行前保存旧映射;后续 admin 更新还会为每个新版本分别追加 before/after。
|
|
||||||
INSERT IGNORE INTO vip_benefit_resource_binding_history (
|
|
||||||
app_code, history_id, config_version, change_phase, program_type, level,
|
|
||||||
benefit_code, benefit_status, trial_enabled, resource_id, resource_type,
|
|
||||||
auto_equip, recorded_at_ms
|
|
||||||
)
|
|
||||||
SELECT b.app_code,
|
|
||||||
SHA2(CONCAT_WS('|', b.app_code, p.config_version, 'baseline', b.level, b.benefit_code), 256),
|
|
||||||
p.config_version, 'baseline', p.program_type, b.level, b.benefit_code,
|
|
||||||
b.status, b.trial_enabled, b.resource_id, b.resource_type, b.auto_equip,
|
|
||||||
@vip_resource_contract_now_ms
|
|
||||||
FROM vip_level_benefits AS b
|
|
||||||
JOIN vip_program_configs AS p ON p.app_code = b.app_code;
|
|
||||||
|
|
||||||
-- tiered Fami 的房间视觉权益使用稳定映射:mic_skin -> mic_seat_animation、
|
|
||||||
-- room_border -> room_border、colored_room_name -> room_name_style。三项都属于 decoration;
|
|
||||||
-- disabled 可以无素材,但若已有 resource_id 也不能保留错误类型或下架资源。
|
|
||||||
UPDATE vip_level_benefits
|
|
||||||
SET benefit_type = 'decoration',
|
|
||||||
resource_type = CASE benefit_code
|
|
||||||
WHEN 'mic_skin' THEN 'mic_seat_animation'
|
|
||||||
WHEN 'room_border' THEN 'room_border'
|
|
||||||
WHEN 'colored_room_name' THEN 'room_name_style'
|
|
||||||
END,
|
|
||||||
updated_at_ms = @vip_resource_contract_now_ms
|
|
||||||
WHERE app_code = 'fami'
|
|
||||||
AND benefit_code IN ('mic_skin', 'room_border', 'colored_room_name')
|
|
||||||
AND (
|
|
||||||
benefit_type <> 'decoration'
|
|
||||||
OR resource_type <> CASE benefit_code
|
|
||||||
WHEN 'mic_skin' THEN 'mic_seat_animation'
|
|
||||||
WHEN 'room_border' THEN 'room_border'
|
|
||||||
WHEN 'colored_room_name' THEN 'room_name_style'
|
|
||||||
END
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @vip_decoration_normalize_count := ROW_COUNT();
|
|
||||||
|
|
||||||
UPDATE vip_level_benefits AS benefit
|
|
||||||
LEFT JOIN resources AS resource
|
|
||||||
ON resource.app_code = benefit.app_code
|
|
||||||
AND resource.resource_id = benefit.resource_id
|
|
||||||
AND resource.status = 'active'
|
|
||||||
AND resource.resource_type = CASE benefit.benefit_code
|
|
||||||
WHEN 'mic_skin' THEN 'mic_seat_animation'
|
|
||||||
WHEN 'room_border' THEN 'room_border'
|
|
||||||
WHEN 'colored_room_name' THEN 'room_name_style'
|
|
||||||
END
|
|
||||||
SET benefit.resource_id = 0,
|
|
||||||
benefit.updated_at_ms = @vip_resource_contract_now_ms
|
|
||||||
WHERE benefit.app_code = 'fami'
|
|
||||||
AND benefit.benefit_code IN ('mic_skin', 'room_border', 'colored_room_name')
|
|
||||||
AND benefit.resource_id > 0
|
|
||||||
AND resource.resource_id IS NULL;
|
|
||||||
|
|
||||||
SET @vip_invalid_resource_clear_count := ROW_COUNT();
|
|
||||||
|
|
||||||
-- 本地现有 Fami VIP4..VIP8 资源组各自只有一个可核实的 active mic_seat_animation。
|
|
||||||
-- 这里按稳定 group_code + 类型解析 ID,并用 HAVING COUNT(DISTINCT)=1 防止运营新增候选后误绑;
|
|
||||||
-- VIP9、room_border、room_name_style 当前没有真实目录资源,必须保持未配置,不能伪造 ID/URL。
|
|
||||||
UPDATE vip_level_benefits AS benefit
|
|
||||||
JOIN (
|
|
||||||
SELECT resource_group.group_code, MIN(resource.resource_id) AS resource_id
|
|
||||||
FROM resource_groups AS resource_group
|
|
||||||
JOIN resource_group_items AS group_item
|
|
||||||
ON group_item.app_code = resource_group.app_code
|
|
||||||
AND group_item.group_id = resource_group.group_id
|
|
||||||
AND group_item.item_type = 'resource'
|
|
||||||
JOIN resources AS resource
|
|
||||||
ON resource.app_code = group_item.app_code
|
|
||||||
AND resource.resource_id = group_item.resource_id
|
|
||||||
AND resource.resource_type = 'mic_seat_animation'
|
|
||||||
AND resource.status = 'active'
|
|
||||||
WHERE resource_group.app_code = 'fami'
|
|
||||||
AND resource_group.status = 'active'
|
|
||||||
AND resource_group.group_code IN ('VIP4', 'VIP5', 'VIP6', 'VIP7', 'VIP8')
|
|
||||||
GROUP BY resource_group.group_code
|
|
||||||
HAVING COUNT(DISTINCT resource.resource_id) = 1
|
|
||||||
) AS mapping
|
|
||||||
ON mapping.group_code = CONCAT('VIP', benefit.level)
|
|
||||||
SET benefit.resource_id = mapping.resource_id,
|
|
||||||
benefit.resource_type = 'mic_seat_animation',
|
|
||||||
benefit.updated_at_ms = @vip_resource_contract_now_ms
|
|
||||||
WHERE benefit.app_code = 'fami'
|
|
||||||
AND benefit.level BETWEEN 4 AND 8
|
|
||||||
AND benefit.benefit_code = 'mic_skin'
|
|
||||||
AND benefit.status = 'active'
|
|
||||||
AND benefit.resource_id = 0;
|
|
||||||
|
|
||||||
SET @vip_mic_skin_bind_count := ROW_COUNT();
|
|
||||||
|
|
||||||
-- 只给“已经被 active mic_skin 绑定”的真实麦座动画补显式 format:先信任后台已有的
|
|
||||||
-- 合法 animation_format,否则只从实际 animation/asset URL 的明确后缀推导。这个更新
|
|
||||||
-- 通过权益配置小表圈定 resource_id,再按 resources 主键点查,不会扫描或修改未绑定资源。
|
|
||||||
UPDATE vip_level_benefits AS benefit
|
|
||||||
JOIN resources AS resource FORCE INDEX (PRIMARY)
|
|
||||||
ON resource.resource_id = benefit.resource_id
|
|
||||||
AND resource.app_code = benefit.app_code
|
|
||||||
AND resource.resource_type = 'mic_seat_animation'
|
|
||||||
AND resource.status = 'active'
|
|
||||||
SET resource.metadata_json = JSON_SET(
|
|
||||||
COALESCE(resource.metadata_json, JSON_OBJECT()),
|
|
||||||
'$.format',
|
|
||||||
CASE
|
|
||||||
WHEN LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), '')))
|
|
||||||
IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4')
|
|
||||||
THEN LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format'))))
|
|
||||||
WHEN LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1))
|
|
||||||
REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$'
|
|
||||||
THEN CASE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1), '.', -1))
|
|
||||||
WHEN 'jpg' THEN 'jpeg'
|
|
||||||
ELSE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1), '.', -1))
|
|
||||||
END
|
|
||||||
ELSE CASE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1), '.', -1))
|
|
||||||
WHEN 'jpg' THEN 'jpeg'
|
|
||||||
ELSE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1), '.', -1))
|
|
||||||
END
|
|
||||||
END
|
|
||||||
),
|
|
||||||
resource.updated_at_ms = @vip_resource_contract_now_ms
|
|
||||||
WHERE benefit.app_code = 'fami'
|
|
||||||
AND benefit.benefit_code = 'mic_skin'
|
|
||||||
AND benefit.status = 'active'
|
|
||||||
AND benefit.resource_type = 'mic_seat_animation'
|
|
||||||
AND benefit.resource_id > 0
|
|
||||||
AND (TRIM(resource.asset_url) <> '' OR TRIM(resource.animation_url) <> '')
|
|
||||||
AND TRIM(resource.preview_url) <> ''
|
|
||||||
AND LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')), '')))
|
|
||||||
NOT IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4')
|
|
||||||
AND (
|
|
||||||
LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), '')))
|
|
||||||
IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4')
|
|
||||||
OR LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1))
|
|
||||||
REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$'
|
|
||||||
OR LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1))
|
|
||||||
REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @vip_mic_format_backfill_count := ROW_COUNT();
|
|
||||||
|
|
||||||
-- active 必须能联结到同 App、active、精确类型的真实资源。麦座动画和房间边框还必须
|
|
||||||
-- 同时具备素材、preview、合法 format,且可选 animation_format 一旦配置就必须一致。
|
|
||||||
-- 缺少真实配置时只禁用权益,不创建占位资源,也不猜测素材 URL。
|
|
||||||
UPDATE vip_level_benefits AS benefit
|
|
||||||
LEFT JOIN resources AS resource
|
|
||||||
ON resource.app_code = benefit.app_code
|
|
||||||
AND resource.resource_id = benefit.resource_id
|
|
||||||
AND resource.status = 'active'
|
|
||||||
AND resource.resource_type = benefit.resource_type
|
|
||||||
SET benefit.status = 'disabled',
|
|
||||||
benefit.updated_at_ms = @vip_resource_contract_now_ms
|
|
||||||
WHERE benefit.app_code = 'fami'
|
|
||||||
AND benefit.benefit_code IN ('mic_skin', 'room_border', 'colored_room_name')
|
|
||||||
AND benefit.status = 'active'
|
|
||||||
AND (
|
|
||||||
resource.resource_id IS NULL
|
|
||||||
OR (
|
|
||||||
benefit.resource_type IN ('mic_seat_animation', 'room_border')
|
|
||||||
AND (
|
|
||||||
(TRIM(resource.asset_url) = '' AND TRIM(resource.animation_url) = '')
|
|
||||||
OR TRIM(resource.preview_url) = ''
|
|
||||||
OR LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')), '')))
|
|
||||||
NOT IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4')
|
|
||||||
OR (
|
|
||||||
LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) <> ''
|
|
||||||
AND LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format'))))
|
|
||||||
<> LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format'))))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @vip_decoration_disable_count := ROW_COUNT();
|
|
||||||
|
|
||||||
-- config_version 只在首次真实回填时递增;迁移重复执行不会制造无意义版本变化。
|
|
||||||
UPDATE vip_program_configs
|
|
||||||
SET config_version = config_version + 1,
|
|
||||||
updated_at_ms = @vip_resource_contract_now_ms
|
|
||||||
WHERE app_code = 'fami'
|
|
||||||
AND (
|
|
||||||
@vip_decoration_normalize_count
|
|
||||||
+ @vip_invalid_resource_clear_count
|
|
||||||
+ @vip_mic_skin_bind_count
|
|
||||||
+ @vip_mic_format_backfill_count
|
|
||||||
+ @vip_decoration_disable_count
|
|
||||||
) > 0;
|
|
||||||
|
|
||||||
-- 本次迁移若提升了 config_version,立即写入新版本的最终 baseline。这样下一次重放开头的
|
|
||||||
-- baseline INSERT 会命中唯一键,不会仅因为上次版本递增再追加一轮完全相同的历史快照。
|
|
||||||
INSERT IGNORE INTO vip_benefit_resource_binding_history (
|
|
||||||
app_code, history_id, config_version, change_phase, program_type, level,
|
|
||||||
benefit_code, benefit_status, trial_enabled, resource_id, resource_type,
|
|
||||||
auto_equip, recorded_at_ms
|
|
||||||
)
|
|
||||||
SELECT b.app_code,
|
|
||||||
SHA2(CONCAT_WS('|', b.app_code, p.config_version, 'baseline', b.level, b.benefit_code), 256),
|
|
||||||
p.config_version, 'baseline', p.program_type, b.level, b.benefit_code,
|
|
||||||
b.status, b.trial_enabled, b.resource_id, b.resource_type, b.auto_equip,
|
|
||||||
@vip_resource_contract_now_ms
|
|
||||||
FROM vip_level_benefits AS b
|
|
||||||
JOIN vip_program_configs AS p ON p.app_code = b.app_code;
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
-- 与 room-service/deploy/mysql/migrations/003_room_vip_media_and_decorations.sql 保持同一可重入结构。
|
|
||||||
-- 性能边界:INSTANT 列仍会短暂申请 metadata lock;image_url 扩容及唯一索引在线扫描低频素材表,建议低峰执行。
|
|
||||||
|
|
||||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
USE hyapp_room;
|
|
||||||
|
|
||||||
DELIMITER $$
|
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS hyapp_migrate_071_room_vip_media_decorations$$
|
|
||||||
CREATE PROCEDURE hyapp_migrate_071_room_vip_media_decorations()
|
|
||||||
BEGIN
|
|
||||||
DECLARE duplicate_groups BIGINT DEFAULT 0;
|
|
||||||
DECLARE oversized_urls BIGINT DEFAULT 0;
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'room_background_images') THEN
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'image_url'
|
|
||||||
AND (data_type <> 'varchar' OR character_maximum_length <> 1024)
|
|
||||||
) THEN
|
|
||||||
SELECT COUNT(*) INTO oversized_urls FROM room_background_images WHERE CHAR_LENGTH(image_url) > 1024;
|
|
||||||
IF oversized_urls > 0 THEN
|
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'room_background_images.image_url contains values longer than 1024';
|
|
||||||
END IF;
|
|
||||||
ALTER TABLE room_background_images MODIFY COLUMN image_url VARCHAR(1024) NOT NULL, ALGORITHM=INPLACE, LOCK=NONE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'command_id') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN command_id VARCHAR(128) NULL COMMENT '新版素材保存幂等键;历史行保持 NULL', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'object_key') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN object_key VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'COS 对象 key', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'content_type') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN content_type VARCHAR(64) NOT NULL DEFAULT '' COMMENT '服务端嗅探 MIME', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'media_format') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN media_format VARCHAR(16) NOT NULL DEFAULT '' COMMENT 'jpeg/png/gif/webp', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'size_bytes') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN size_bytes BIGINT NOT NULL DEFAULT 0 COMMENT '文件字节数', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'width') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN width INT NOT NULL DEFAULT 0 COMMENT '像素宽度', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'height') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN height INT NOT NULL DEFAULT 0 COMMENT '像素高度', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'animated') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN animated BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否动图', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'frame_count') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN frame_count INT NOT NULL DEFAULT 0 COMMENT '帧数', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'duration_ms') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '动图总时长', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'poster_url') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN poster_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '可选首帧 URL', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'thumbnail_url') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN thumbnail_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '可选缩略图 URL', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'sha256') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN sha256 CHAR(64) NOT NULL DEFAULT '' COMMENT '原文件 SHA-256', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'media_status') THEN
|
|
||||||
ALTER TABLE room_background_images ADD COLUMN media_status VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'active 表示已通过专用上传入口', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.statistics
|
|
||||||
WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND index_name = 'uk_room_background_command'
|
|
||||||
) THEN
|
|
||||||
SELECT COUNT(*) INTO duplicate_groups
|
|
||||||
FROM (
|
|
||||||
SELECT 1 FROM room_background_images
|
|
||||||
WHERE command_id IS NOT NULL
|
|
||||||
GROUP BY app_code, room_id, command_id
|
|
||||||
HAVING COUNT(*) > 1
|
|
||||||
LIMIT 1
|
|
||||||
) AS duplicate_command_groups;
|
|
||||||
IF duplicate_groups > 0 THEN
|
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'duplicate room background command_id blocks unique index';
|
|
||||||
END IF;
|
|
||||||
ALTER TABLE room_background_images ADD UNIQUE KEY uk_room_background_command (app_code, room_id, command_id), ALGORITHM=INPLACE, LOCK=NONE;
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'room_list_entries') THEN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_list_entries' AND column_name = 'room_border_json') THEN
|
|
||||||
ALTER TABLE room_list_entries ADD COLUMN room_border_json JSON NULL COMMENT '当前未过期房间边框资源快照', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_list_entries' AND column_name = 'room_name_style_json') THEN
|
|
||||||
ALTER TABLE room_list_entries ADD COLUMN room_name_style_json JSON NULL COMMENT '当前未过期彩色房名资源快照', ALGORITHM=INSTANT;
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
END$$
|
|
||||||
|
|
||||||
CALL hyapp_migrate_071_room_vip_media_decorations()$$
|
|
||||||
DROP PROCEDURE IF EXISTS hyapp_migrate_071_room_vip_media_decorations$$
|
|
||||||
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS room_media_upload_registrations (
|
|
||||||
app_code VARCHAR(32) NOT NULL,
|
|
||||||
room_id VARCHAR(64) NOT NULL,
|
|
||||||
command_id VARCHAR(128) NOT NULL,
|
|
||||||
actor_user_id BIGINT NOT NULL,
|
|
||||||
purpose VARCHAR(32) NOT NULL,
|
|
||||||
media_payload_sha256 CHAR(64) NOT NULL,
|
|
||||||
expected_object_key VARCHAR(512) NOT NULL,
|
|
||||||
object_url VARCHAR(1024) NOT NULL DEFAULT '',
|
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'authorized',
|
|
||||||
content_type VARCHAR(64) NOT NULL,
|
|
||||||
media_format VARCHAR(16) NOT NULL,
|
|
||||||
size_bytes BIGINT NOT NULL,
|
|
||||||
width INT NOT NULL,
|
|
||||||
height INT NOT NULL,
|
|
||||||
animated BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
frame_count INT NOT NULL,
|
|
||||||
duration_ms BIGINT NOT NULL DEFAULT 0,
|
|
||||||
sha256 CHAR(64) NOT NULL,
|
|
||||||
authorized_room_version BIGINT NOT NULL,
|
|
||||||
created_at_ms BIGINT NOT NULL,
|
|
||||||
updated_at_ms BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (app_code, room_id, command_id),
|
|
||||||
UNIQUE KEY uk_room_media_upload_object (app_code, expected_object_key),
|
|
||||||
KEY idx_room_media_upload_status_created (app_code, status, created_at_ms)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间专用媒体上传授权与完成事实';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS room_decoration_assignments (
|
|
||||||
app_code VARCHAR(32) NOT NULL,
|
|
||||||
room_id VARCHAR(64) NOT NULL,
|
|
||||||
decoration_type VARCHAR(32) NOT NULL,
|
|
||||||
owner_user_id BIGINT NOT NULL,
|
|
||||||
benefit_code VARCHAR(64) NOT NULL,
|
|
||||||
resource_id BIGINT NOT NULL,
|
|
||||||
entitlement_id VARCHAR(128) NOT NULL DEFAULT '',
|
|
||||||
expires_at_ms BIGINT NOT NULL DEFAULT 0,
|
|
||||||
updated_at_ms BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (app_code, room_id, decoration_type),
|
|
||||||
KEY idx_room_decoration_resource (app_code, resource_id, room_id, decoration_type),
|
|
||||||
KEY idx_room_decoration_owner (app_code, owner_user_id, room_id, decoration_type),
|
|
||||||
KEY idx_room_decoration_expiry (app_code, expires_at_ms, room_id, decoration_type)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间当前 VIP 装扮反向索引';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS room_wallet_event_consumption (
|
|
||||||
app_code VARCHAR(32) NOT NULL,
|
|
||||||
event_id VARCHAR(128) NOT NULL,
|
|
||||||
event_type VARCHAR(64) NOT NULL,
|
|
||||||
consumed_at_ms BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (app_code, event_id),
|
|
||||||
KEY idx_room_wallet_event_consumed (app_code, consumed_at_ms)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='room-service 消费 wallet 装扮事实的本地幂等表';
|
|
||||||
@ -26,7 +26,6 @@ import (
|
|||||||
"hyapp-admin-server/internal/integration/statisticsclient"
|
"hyapp-admin-server/internal/integration/statisticsclient"
|
||||||
"hyapp-admin-server/internal/integration/userclient"
|
"hyapp-admin-server/internal/integration/userclient"
|
||||||
"hyapp-admin-server/internal/integration/walletclient"
|
"hyapp-admin-server/internal/integration/walletclient"
|
||||||
"hyapp-admin-server/internal/integration/withdrawalsource"
|
|
||||||
jobrunner "hyapp-admin-server/internal/job"
|
jobrunner "hyapp-admin-server/internal/job"
|
||||||
"hyapp-admin-server/internal/migration"
|
"hyapp-admin-server/internal/migration"
|
||||||
achievementconfigmodule "hyapp-admin-server/internal/modules/achievementconfig"
|
achievementconfigmodule "hyapp-admin-server/internal/modules/achievementconfig"
|
||||||
@ -89,7 +88,6 @@ import (
|
|||||||
vipconfigmodule "hyapp-admin-server/internal/modules/vipconfig"
|
vipconfigmodule "hyapp-admin-server/internal/modules/vipconfig"
|
||||||
weeklystarmodule "hyapp-admin-server/internal/modules/weeklystar"
|
weeklystarmodule "hyapp-admin-server/internal/modules/weeklystar"
|
||||||
wheelmodule "hyapp-admin-server/internal/modules/wheel"
|
wheelmodule "hyapp-admin-server/internal/modules/wheel"
|
||||||
"hyapp-admin-server/internal/platform/dingtalkrobot"
|
|
||||||
"hyapp-admin-server/internal/platform/logging"
|
"hyapp-admin-server/internal/platform/logging"
|
||||||
"hyapp-admin-server/internal/platform/tencentcos"
|
"hyapp-admin-server/internal/platform/tencentcos"
|
||||||
"hyapp-admin-server/internal/repository"
|
"hyapp-admin-server/internal/repository"
|
||||||
@ -358,10 +356,6 @@ func main() {
|
|||||||
)
|
)
|
||||||
luckyGiftHandler := luckygiftmodule.New(luckygiftadmin.NewGRPC(luckyGiftConn), cfg.LuckyGiftService.RequestTimeout, auditHandler).
|
luckyGiftHandler := luckygiftmodule.New(luckygiftadmin.NewGRPC(luckyGiftConn), cfg.LuckyGiftService.RequestTimeout, auditHandler).
|
||||||
BindUserClient(userclient.NewGRPC(userConn))
|
BindUserClient(userclient.NewGRPC(userConn))
|
||||||
withdrawalReviewNotifier, err := newWithdrawalReviewNotifier(cfg.FinanceNotifications.DingTalk)
|
|
||||||
if err != nil {
|
|
||||||
fatalRuntime("create_finance_dingtalk_notifier_failed", err)
|
|
||||||
}
|
|
||||||
handlers := router.Handlers{
|
handlers := router.Handlers{
|
||||||
Audit: auditHandler,
|
Audit: auditHandler,
|
||||||
Auth: authmodule.New(store, auth, auditHandler, cfg),
|
Auth: authmodule.New(store, auth, auditHandler, cfg),
|
||||||
@ -390,8 +384,7 @@ func main() {
|
|||||||
financeordermodule.WithLegacyCoinSellerRegionResolver(func(ctx context.Context, appCode string, countryCode string) (int64, bool, error) {
|
financeordermodule.WithLegacyCoinSellerRegionResolver(func(ctx context.Context, appCode string, countryCode string) (int64, bool, error) {
|
||||||
return paymentmodule.RegionIDForCountryCode(ctx, moneyRegionSources, appCode, countryCode)
|
return paymentmodule.RegionIDForCountryCode(ctx, moneyRegionSources, appCode, countryCode)
|
||||||
})),
|
})),
|
||||||
FinanceWithdrawal: financewithdrawalmodule.NewWithSources(store, walletclient.NewGRPC(walletConn), activityclient.NewGRPC(activityConn), auditHandler, withdrawalsource.New(cfg.WithdrawalSources)).
|
FinanceWithdrawal: financewithdrawalmodule.New(store, walletclient.NewGRPC(walletConn), activityclient.NewGRPC(activityConn), auditHandler),
|
||||||
BindReviewPendingNotifier(withdrawalReviewNotifier),
|
|
||||||
ExternalAdmin: externaladminmodule.New(db, userDB, externaladminmodule.Config{
|
ExternalAdmin: externaladminmodule.New(db, userDB, externaladminmodule.Config{
|
||||||
SessionTTL: cfg.ExternalAdmin.SessionTTL,
|
SessionTTL: cfg.ExternalAdmin.SessionTTL,
|
||||||
MaxLoginFailures: cfg.ExternalAdmin.MaxLoginFailures,
|
MaxLoginFailures: cfg.ExternalAdmin.MaxLoginFailures,
|
||||||
@ -480,23 +473,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWithdrawalReviewNotifier(cfg config.DingTalkRobotConfig) (financewithdrawalmodule.ReviewPendingNotifier, error) {
|
|
||||||
if !cfg.Enabled || strings.TrimSpace(cfg.WebhookURL) == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
client, err := dingtalkrobot.New(dingtalkrobot.Config{
|
|
||||||
WebhookURL: cfg.WebhookURL,
|
|
||||||
Secret: cfg.Secret,
|
|
||||||
AtMobiles: cfg.AtMobiles,
|
|
||||||
AtAll: cfg.AtAll,
|
|
||||||
RequestTimeout: cfg.RequestTimeout,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return financewithdrawalmodule.NewDingTalkReviewPendingNotifier(client), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fatalRuntime(msg string, err error) {
|
func fatalRuntime(msg string, err error) {
|
||||||
slog.Error(msg, "error", err.Error())
|
slog.Error(msg, "error", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@ -69,15 +69,6 @@ finance_google_paid_sync:
|
|||||||
poll_interval: "15s"
|
poll_interval: "15s"
|
||||||
batch_size: 100
|
batch_size: 100
|
||||||
request_timeout: "2m"
|
request_timeout: "2m"
|
||||||
# 生产通过 HYAPP_ADMIN_ASLAN_WITHDRAWAL_* 注入密钥、回调地址并开启。
|
|
||||||
withdrawal_sources:
|
|
||||||
- enabled: false
|
|
||||||
app_code: "aslan"
|
|
||||||
source_system: "likei"
|
|
||||||
submit_token: ""
|
|
||||||
callback_url: ""
|
|
||||||
callback_token: ""
|
|
||||||
request_timeout: "5s"
|
|
||||||
mysql_auto_migrate: false
|
mysql_auto_migrate: false
|
||||||
migrations:
|
migrations:
|
||||||
enabled: true
|
enabled: true
|
||||||
@ -137,15 +128,6 @@ statistics_service:
|
|||||||
base_url: "http://10.2.1.16:13010"
|
base_url: "http://10.2.1.16:13010"
|
||||||
# 漏斗冷查询允许短时索引页预热;正常查询应远低于该上限,超时只负责隔离异常请求。
|
# 漏斗冷查询允许短时索引页预热;正常查询应远低于该上限,超时只负责隔离异常请求。
|
||||||
request_timeout: "10s"
|
request_timeout: "10s"
|
||||||
finance_notifications:
|
|
||||||
dingtalk:
|
|
||||||
enabled: true
|
|
||||||
# 生产凭证通过 HYAPP_ADMIN_FINANCE_DINGTALK_* 注入,示例配置不保存机器人 access_token。
|
|
||||||
webhook_url: ""
|
|
||||||
secret: ""
|
|
||||||
at_mobiles: []
|
|
||||||
at_all: false
|
|
||||||
request_timeout: "3s"
|
|
||||||
dashboard_external_sources:
|
dashboard_external_sources:
|
||||||
- enabled: true
|
- enabled: true
|
||||||
type: "mysql"
|
type: "mysql"
|
||||||
|
|||||||
@ -67,16 +67,6 @@ finance_google_paid_sync:
|
|||||||
poll_interval: "15s"
|
poll_interval: "15s"
|
||||||
batch_size: 100
|
batch_size: 100
|
||||||
request_timeout: "2m"
|
request_timeout: "2m"
|
||||||
# 外部钱包统一提现双审来源。生产密钥和回调地址只从
|
|
||||||
# HYAPP_ADMIN_ASLAN_WITHDRAWAL_* 环境变量注入,仓库不保存资金接口凭证。
|
|
||||||
withdrawal_sources:
|
|
||||||
- enabled: false
|
|
||||||
app_code: "aslan"
|
|
||||||
source_system: "likei"
|
|
||||||
submit_token: ""
|
|
||||||
callback_url: ""
|
|
||||||
callback_token: ""
|
|
||||||
request_timeout: "5s"
|
|
||||||
mysql_auto_migrate: true
|
mysql_auto_migrate: true
|
||||||
migrations:
|
migrations:
|
||||||
enabled: true
|
enabled: true
|
||||||
@ -135,15 +125,6 @@ statistics_service:
|
|||||||
base_url: "http://127.0.0.1:13010"
|
base_url: "http://127.0.0.1:13010"
|
||||||
# 漏斗冷查询允许短时索引页预热;正常查询应远低于该上限,超时只负责隔离异常请求。
|
# 漏斗冷查询允许短时索引页预热;正常查询应远低于该上限,超时只负责隔离异常请求。
|
||||||
request_timeout: "10s"
|
request_timeout: "10s"
|
||||||
finance_notifications:
|
|
||||||
dingtalk:
|
|
||||||
enabled: true
|
|
||||||
# 真实 webhook/access_token 和可选签名密钥只通过 HYAPP_ADMIN_FINANCE_DINGTALK_* 注入。
|
|
||||||
webhook_url: ""
|
|
||||||
secret: ""
|
|
||||||
at_mobiles: []
|
|
||||||
at_all: false
|
|
||||||
request_timeout: "3s"
|
|
||||||
dashboard_external_sources:
|
dashboard_external_sources:
|
||||||
- enabled: false
|
- enabled: false
|
||||||
type: "mysql"
|
type: "mysql"
|
||||||
|
|||||||
@ -50,10 +50,8 @@ type Config struct {
|
|||||||
LuckyGiftService LuckyGiftServiceConfig `yaml:"lucky_gift_service"`
|
LuckyGiftService LuckyGiftServiceConfig `yaml:"lucky_gift_service"`
|
||||||
GameService GameServiceConfig `yaml:"game_service"`
|
GameService GameServiceConfig `yaml:"game_service"`
|
||||||
StatisticsService StatisticsServiceConfig `yaml:"statistics_service"`
|
StatisticsService StatisticsServiceConfig `yaml:"statistics_service"`
|
||||||
FinanceNotifications FinanceNotificationsConfig `yaml:"finance_notifications"`
|
|
||||||
DashboardExternalSources []DashboardExternalSourceConfig `yaml:"dashboard_external_sources"`
|
DashboardExternalSources []DashboardExternalSourceConfig `yaml:"dashboard_external_sources"`
|
||||||
LegacyCoinSellerRechargeSources []LegacyCoinSellerRechargeSourceConfig `yaml:"legacy_coin_seller_recharge_sources"`
|
LegacyCoinSellerRechargeSources []LegacyCoinSellerRechargeSourceConfig `yaml:"legacy_coin_seller_recharge_sources"`
|
||||||
WithdrawalSources []WithdrawalSourceConfig `yaml:"withdrawal_sources"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MigrationConfig struct {
|
type MigrationConfig struct {
|
||||||
@ -117,33 +115,6 @@ type StatisticsServiceConfig struct {
|
|||||||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FinanceNotificationsConfig 只保存审核协作通知配置;提现状态和资金终态仍由 Admin/Wallet 的持久事实决定。
|
|
||||||
type FinanceNotificationsConfig struct {
|
|
||||||
DingTalk DingTalkRobotConfig `yaml:"dingtalk"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DingTalkRobotConfig 的 webhook 和签名密钥属于运行时凭证,可提交配置只能保留空值。
|
|
||||||
type DingTalkRobotConfig struct {
|
|
||||||
Enabled bool `yaml:"enabled"`
|
|
||||||
WebhookURL string `yaml:"webhook_url"`
|
|
||||||
Secret string `yaml:"secret"`
|
|
||||||
AtMobiles []string `yaml:"at_mobiles"`
|
|
||||||
AtAll bool `yaml:"at_all"`
|
|
||||||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithdrawalSourceConfig 描述接入统一双审工作台的外部钱包系统。
|
|
||||||
// submit_token 只允许来源 App 创建自己的申请;callback_token 用于 admin 终审时回调来源钱包,二者分离避免任一方向泄漏后扩大权限。
|
|
||||||
type WithdrawalSourceConfig struct {
|
|
||||||
Enabled bool `yaml:"enabled"`
|
|
||||||
AppCode string `yaml:"app_code"`
|
|
||||||
SourceSystem string `yaml:"source_system"`
|
|
||||||
SubmitToken string `yaml:"submit_token"`
|
|
||||||
CallbackURL string `yaml:"callback_url"`
|
|
||||||
CallbackToken string `yaml:"callback_token"`
|
|
||||||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DashboardExternalSourceConfig struct {
|
type DashboardExternalSourceConfig struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
SourceType string `yaml:"type"`
|
SourceType string `yaml:"type"`
|
||||||
@ -377,12 +348,6 @@ func Default() Config {
|
|||||||
BaseURL: "http://127.0.0.1:13010",
|
BaseURL: "http://127.0.0.1:13010",
|
||||||
RequestTimeout: 3 * time.Second,
|
RequestTimeout: 3 * time.Second,
|
||||||
},
|
},
|
||||||
FinanceNotifications: FinanceNotificationsConfig{
|
|
||||||
DingTalk: DingTalkRobotConfig{
|
|
||||||
Enabled: true,
|
|
||||||
RequestTimeout: 3 * time.Second,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
DashboardExternalSources: []DashboardExternalSourceConfig{
|
DashboardExternalSources: []DashboardExternalSourceConfig{
|
||||||
{
|
{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
@ -420,14 +385,6 @@ func Default() Config {
|
|||||||
RequestTimeout: 5 * time.Second,
|
RequestTimeout: 5 * time.Second,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
WithdrawalSources: []WithdrawalSourceConfig{
|
|
||||||
{
|
|
||||||
Enabled: false,
|
|
||||||
AppCode: "aslan",
|
|
||||||
SourceSystem: "likei",
|
|
||||||
RequestTimeout: 5 * time.Second,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
FinanceGooglePaidSync: FinanceGooglePaidSyncConfig{
|
FinanceGooglePaidSync: FinanceGooglePaidSyncConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
PollInterval: 15 * time.Second,
|
PollInterval: 15 * time.Second,
|
||||||
@ -608,13 +565,6 @@ func (cfg *Config) Normalize() {
|
|||||||
if cfg.StatisticsService.RequestTimeout <= 0 {
|
if cfg.StatisticsService.RequestTimeout <= 0 {
|
||||||
cfg.StatisticsService.RequestTimeout = 3 * time.Second
|
cfg.StatisticsService.RequestTimeout = 3 * time.Second
|
||||||
}
|
}
|
||||||
cfg.FinanceNotifications.DingTalk.WebhookURL = strings.TrimSpace(cfg.FinanceNotifications.DingTalk.WebhookURL)
|
|
||||||
cfg.FinanceNotifications.DingTalk.Secret = strings.TrimSpace(cfg.FinanceNotifications.DingTalk.Secret)
|
|
||||||
cfg.FinanceNotifications.DingTalk.AtMobiles = compactStrings(cfg.FinanceNotifications.DingTalk.AtMobiles)
|
|
||||||
if cfg.FinanceNotifications.DingTalk.RequestTimeout <= 0 {
|
|
||||||
cfg.FinanceNotifications.DingTalk.RequestTimeout = 3 * time.Second
|
|
||||||
}
|
|
||||||
cfg.applyFinanceDingTalkEnvOverrides()
|
|
||||||
for index := range cfg.DashboardExternalSources {
|
for index := range cfg.DashboardExternalSources {
|
||||||
source := &cfg.DashboardExternalSources[index]
|
source := &cfg.DashboardExternalSources[index]
|
||||||
// 外接 App 大屏的数据源类型在配置层归一化;查询层只根据类型读取对应的外部聚合事实,不回退到 hyapp statistics-service。
|
// 外接 App 大屏的数据源类型在配置层归一化;查询层只根据类型读取对应的外部聚合事实,不回退到 hyapp statistics-service。
|
||||||
@ -673,18 +623,6 @@ func (cfg *Config) Normalize() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cfg.applyLegacyCoinSellerRechargeSourceEnvOverrides()
|
cfg.applyLegacyCoinSellerRechargeSourceEnvOverrides()
|
||||||
for index := range cfg.WithdrawalSources {
|
|
||||||
source := &cfg.WithdrawalSources[index]
|
|
||||||
source.AppCode = strings.ToLower(strings.TrimSpace(source.AppCode))
|
|
||||||
source.SourceSystem = strings.ToLower(strings.TrimSpace(source.SourceSystem))
|
|
||||||
source.SubmitToken = strings.TrimSpace(source.SubmitToken)
|
|
||||||
source.CallbackURL = strings.TrimRight(strings.TrimSpace(source.CallbackURL), "/")
|
|
||||||
source.CallbackToken = strings.TrimSpace(source.CallbackToken)
|
|
||||||
if source.RequestTimeout <= 0 {
|
|
||||||
source.RequestTimeout = 5 * time.Second
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cfg.applyWithdrawalSourceEnvOverrides()
|
|
||||||
cfg.RobotProfileSource.MySQLDSN = strings.TrimSpace(cfg.RobotProfileSource.MySQLDSN)
|
cfg.RobotProfileSource.MySQLDSN = strings.TrimSpace(cfg.RobotProfileSource.MySQLDSN)
|
||||||
if cfg.RobotProfileSource.RequestTimeout <= 0 {
|
if cfg.RobotProfileSource.RequestTimeout <= 0 {
|
||||||
cfg.RobotProfileSource.RequestTimeout = 5 * time.Second
|
cfg.RobotProfileSource.RequestTimeout = 5 * time.Second
|
||||||
@ -966,26 +904,6 @@ func (cfg Config) Validate() error {
|
|||||||
return fmt.Errorf("dashboard_external_sources[%d].request_timeout must be greater than 0", index)
|
return fmt.Errorf("dashboard_external_sources[%d].request_timeout must be greater than 0", index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for index, source := range cfg.WithdrawalSources {
|
|
||||||
if !source.Enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(source.AppCode) == "" || strings.TrimSpace(source.SourceSystem) == "" {
|
|
||||||
return fmt.Errorf("withdrawal_sources[%d] app_code and source_system are required when enabled", index)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(source.SubmitToken) == "" || containsConfigPlaceholder(source.SubmitToken) {
|
|
||||||
return fmt.Errorf("withdrawal_sources[%d].submit_token must be injected when enabled", index)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(source.CallbackToken) == "" || containsConfigPlaceholder(source.CallbackToken) {
|
|
||||||
return fmt.Errorf("withdrawal_sources[%d].callback_token must be injected when enabled", index)
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(source.CallbackURL, "http://") && !strings.HasPrefix(source.CallbackURL, "https://") {
|
|
||||||
return fmt.Errorf("withdrawal_sources[%d].callback_url must be an absolute url", index)
|
|
||||||
}
|
|
||||||
if source.RequestTimeout <= 0 {
|
|
||||||
return fmt.Errorf("withdrawal_sources[%d].request_timeout must be greater than 0", index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isLockedEnvironment(cfg.Environment) && strings.TrimSpace(cfg.RobotProfileSource.MySQLDSN) == "" {
|
if isLockedEnvironment(cfg.Environment) && strings.TrimSpace(cfg.RobotProfileSource.MySQLDSN) == "" {
|
||||||
return errors.New("robot_profile_source.mysql_dsn is required outside local/dev")
|
return errors.New("robot_profile_source.mysql_dsn is required outside local/dev")
|
||||||
}
|
}
|
||||||
@ -1196,54 +1114,6 @@ func (cfg *Config) applyLegacyCoinSellerRechargeSourceEnvOverrides() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) applyWithdrawalSourceEnvOverrides() {
|
|
||||||
for index := range cfg.WithdrawalSources {
|
|
||||||
source := &cfg.WithdrawalSources[index]
|
|
||||||
appKey := envAppKey(source.AppCode)
|
|
||||||
if appKey == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
prefix := "HYAPP_ADMIN_" + appKey + "_WITHDRAWAL_"
|
|
||||||
if value := strings.TrimSpace(os.Getenv(prefix + "ENABLED")); value != "" {
|
|
||||||
source.Enabled = parseBoolEnv(value)
|
|
||||||
}
|
|
||||||
if value := strings.TrimSpace(os.Getenv(prefix + "SOURCE_SYSTEM")); value != "" {
|
|
||||||
source.SourceSystem = value
|
|
||||||
}
|
|
||||||
if value := strings.TrimSpace(os.Getenv(prefix + "SUBMIT_TOKEN")); value != "" {
|
|
||||||
// 双向令牌都属于资金系统密钥,只允许通过运行环境注入,不能写进仓库配置。
|
|
||||||
source.SubmitToken = value
|
|
||||||
}
|
|
||||||
if value := strings.TrimSpace(os.Getenv(prefix + "CALLBACK_URL")); value != "" {
|
|
||||||
source.CallbackURL = value
|
|
||||||
}
|
|
||||||
if value := strings.TrimSpace(os.Getenv(prefix + "CALLBACK_TOKEN")); value != "" {
|
|
||||||
source.CallbackToken = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) applyFinanceDingTalkEnvOverrides() {
|
|
||||||
dingTalk := &cfg.FinanceNotifications.DingTalk
|
|
||||||
if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_FINANCE_DINGTALK_ENABLED")); value != "" {
|
|
||||||
dingTalk.Enabled = parseBoolEnv(value)
|
|
||||||
}
|
|
||||||
if value := strings.TrimSpace(firstEnv("HYAPP_ADMIN_FINANCE_DINGTALK_WEBHOOK_URL", "FINANCE_DINGTALK_WEBHOOK_URL")); value != "" {
|
|
||||||
// webhook query 中包含 access_token,只允许运行环境覆盖,不能复制到 Admin YAML 或日志。
|
|
||||||
dingTalk.WebhookURL = value
|
|
||||||
dingTalk.Enabled = true
|
|
||||||
}
|
|
||||||
if value := strings.TrimSpace(firstEnv("HYAPP_ADMIN_FINANCE_DINGTALK_SECRET", "FINANCE_DINGTALK_SECRET")); value != "" {
|
|
||||||
dingTalk.Secret = value
|
|
||||||
}
|
|
||||||
if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_FINANCE_DINGTALK_AT_MOBILES")); value != "" {
|
|
||||||
dingTalk.AtMobiles = compactStrings(strings.Split(value, ","))
|
|
||||||
}
|
|
||||||
if value := strings.TrimSpace(os.Getenv("HYAPP_ADMIN_FINANCE_DINGTALK_AT_ALL")); value != "" {
|
|
||||||
dingTalk.AtAll = parseBoolEnv(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func envAppKey(appCode string) string {
|
func envAppKey(appCode string) string {
|
||||||
appCode = strings.ToUpper(strings.TrimSpace(appCode))
|
appCode = strings.ToUpper(strings.TrimSpace(appCode))
|
||||||
if appCode == "" {
|
if appCode == "" {
|
||||||
|
|||||||
@ -14,8 +14,6 @@ type Client interface {
|
|||||||
GetLuckyGiftConfig(ctx context.Context, req *luckygiftv1.GetLuckyGiftConfigRequest) (*luckygiftv1.GetLuckyGiftConfigResponse, error)
|
GetLuckyGiftConfig(ctx context.Context, req *luckygiftv1.GetLuckyGiftConfigRequest) (*luckygiftv1.GetLuckyGiftConfigResponse, error)
|
||||||
UpsertLuckyGiftConfig(ctx context.Context, req *luckygiftv1.UpsertLuckyGiftConfigRequest) (*luckygiftv1.UpsertLuckyGiftConfigResponse, error)
|
UpsertLuckyGiftConfig(ctx context.Context, req *luckygiftv1.UpsertLuckyGiftConfigRequest) (*luckygiftv1.UpsertLuckyGiftConfigResponse, error)
|
||||||
ListLuckyGiftConfigs(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigsRequest) (*luckygiftv1.ListLuckyGiftConfigsResponse, error)
|
ListLuckyGiftConfigs(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigsRequest) (*luckygiftv1.ListLuckyGiftConfigsResponse, error)
|
||||||
ListLuckyGiftConfigVersions(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigVersionsRequest) (*luckygiftv1.ListLuckyGiftConfigVersionsResponse, error)
|
|
||||||
RollbackLuckyGiftConfig(ctx context.Context, req *luckygiftv1.RollbackLuckyGiftConfigRequest) (*luckygiftv1.RollbackLuckyGiftConfigResponse, error)
|
|
||||||
ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error)
|
ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error)
|
||||||
GetLuckyGiftDrawSummary(ctx context.Context, req *luckygiftv1.GetLuckyGiftDrawSummaryRequest) (*luckygiftv1.GetLuckyGiftDrawSummaryResponse, error)
|
GetLuckyGiftDrawSummary(ctx context.Context, req *luckygiftv1.GetLuckyGiftDrawSummaryRequest) (*luckygiftv1.GetLuckyGiftDrawSummaryResponse, error)
|
||||||
ListLuckyGiftUserProfiles(ctx context.Context, req *luckygiftv1.ListLuckyGiftUserProfilesRequest) (*luckygiftv1.ListLuckyGiftUserProfilesResponse, error)
|
ListLuckyGiftUserProfiles(ctx context.Context, req *luckygiftv1.ListLuckyGiftUserProfilesRequest) (*luckygiftv1.ListLuckyGiftUserProfilesResponse, error)
|
||||||
@ -50,14 +48,6 @@ func (c *GRPCClient) ListLuckyGiftConfigs(ctx context.Context, req *luckygiftv1.
|
|||||||
return c.client.ListLuckyGiftConfigs(ctx, req)
|
return c.client.ListLuckyGiftConfigs(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GRPCClient) ListLuckyGiftConfigVersions(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigVersionsRequest) (*luckygiftv1.ListLuckyGiftConfigVersionsResponse, error) {
|
|
||||||
return c.client.ListLuckyGiftConfigVersions(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GRPCClient) RollbackLuckyGiftConfig(ctx context.Context, req *luckygiftv1.RollbackLuckyGiftConfigRequest) (*luckygiftv1.RollbackLuckyGiftConfigResponse, error) {
|
|
||||||
return c.client.RollbackLuckyGiftConfig(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GRPCClient) ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error) {
|
func (c *GRPCClient) ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error) {
|
||||||
return c.client.ListLuckyGiftDraws(ctx, req)
|
return c.client.ListLuckyGiftDraws(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,114 +0,0 @@
|
|||||||
package withdrawalsource
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"hyapp-admin-server/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxErrorBodyBytes = 2048
|
|
||||||
|
|
||||||
type DecisionRequest struct {
|
|
||||||
SourceApplicationID string `json:"sourceApplicationId"`
|
|
||||||
Decision string `json:"decision"`
|
|
||||||
CredentialURLs []string `json:"credentialUrls"`
|
|
||||||
Remark string `json:"remark"`
|
|
||||||
ReviewerUserID uint `json:"reviewerUserId"`
|
|
||||||
ReviewerName string `json:"reviewerName"`
|
|
||||||
CommandID string `json:"commandId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Registry struct {
|
|
||||||
sources map[string]source
|
|
||||||
}
|
|
||||||
|
|
||||||
type source struct {
|
|
||||||
config config.WithdrawalSourceConfig
|
|
||||||
client *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(configs []config.WithdrawalSourceConfig) *Registry {
|
|
||||||
registry := &Registry{sources: make(map[string]source)}
|
|
||||||
for _, item := range configs {
|
|
||||||
if !item.Enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := sourceKey(item.AppCode, item.SourceSystem)
|
|
||||||
registry.sources[key] = source{
|
|
||||||
config: item,
|
|
||||||
client: &http.Client{Timeout: item.RequestTimeout},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return registry
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthenticateSubmit 对 app_code + source_system 组合做独立令牌校验。
|
|
||||||
// 常量时间比较避免从鉴权耗时侧信道逐字符猜测资金接口令牌。
|
|
||||||
func (r *Registry) AuthenticateSubmit(appCode string, sourceSystem string, authorization string) bool {
|
|
||||||
item, ok := r.lookup(appCode, sourceSystem)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
authorization = strings.TrimSpace(authorization)
|
|
||||||
if !strings.HasPrefix(authorization, "Bearer ") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
provided := strings.TrimSpace(strings.TrimPrefix(authorization, "Bearer "))
|
|
||||||
expected := strings.TrimSpace(item.config.SubmitToken)
|
|
||||||
return provided != "" && len(provided) == len(expected) && subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) HasSource(appCode string, sourceSystem string) bool {
|
|
||||||
_, ok := r.lookup(appCode, sourceSystem)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApplyDecision 只负责把终态回调来源钱包;来源端必须按 commandId 幂等。
|
|
||||||
// admin 本地状态只有在 2xx 后才会提交,因此任何网络错误都会保留可重试的审核状态。
|
|
||||||
func (r *Registry) ApplyDecision(ctx context.Context, appCode string, sourceSystem string, request DecisionRequest) (string, error) {
|
|
||||||
item, ok := r.lookup(appCode, sourceSystem)
|
|
||||||
if !ok {
|
|
||||||
return "", errors.New("withdrawal source is not configured")
|
|
||||||
}
|
|
||||||
body, err := json.Marshal(request)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("encode withdrawal source decision: %w", err)
|
|
||||||
}
|
|
||||||
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, item.config.CallbackURL, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("build withdrawal source decision: %w", err)
|
|
||||||
}
|
|
||||||
httpRequest.Header.Set("Authorization", "Bearer "+item.config.CallbackToken)
|
|
||||||
httpRequest.Header.Set("Content-Type", "application/json")
|
|
||||||
response, err := item.client.Do(httpRequest)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("call withdrawal source decision: %w", err)
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
|
|
||||||
message, _ := io.ReadAll(io.LimitReader(response.Body, maxErrorBodyBytes))
|
|
||||||
return "", fmt.Errorf("withdrawal source decision returned %d: %s", response.StatusCode, strings.TrimSpace(string(message)))
|
|
||||||
}
|
|
||||||
// 来源申请号已单独持久化;阶段 commandId 作为短事务号即可关联 admin 审核与来源幂等执行,且不会突破字段长度上限。
|
|
||||||
return "external:" + strings.TrimSpace(request.CommandID), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) lookup(appCode string, sourceSystem string) (source, bool) {
|
|
||||||
if r == nil {
|
|
||||||
return source{}, false
|
|
||||||
}
|
|
||||||
item, ok := r.sources[sourceKey(appCode, sourceSystem)]
|
|
||||||
return item, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func sourceKey(appCode string, sourceSystem string) string {
|
|
||||||
return strings.ToLower(strings.TrimSpace(appCode)) + "\x00" + strings.ToLower(strings.TrimSpace(sourceSystem))
|
|
||||||
}
|
|
||||||
@ -422,13 +422,10 @@ func (PolicyPublishItem) TableName() string {
|
|||||||
|
|
||||||
type HostAgencySalaryPolicy struct {
|
type HostAgencySalaryPolicy struct {
|
||||||
ID uint `gorm:"primaryKey" json:"id"`
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
AppCode string `gorm:"size:32;uniqueIndex:uk_admin_host_agency_salary_policy_name;uniqueIndex:uk_admin_host_agency_salary_policy_version;index:idx_admin_host_agency_salary_policy_region;index:idx_admin_host_agency_salary_policy_cycle,not null;default:lalu" json:"appCode"`
|
AppCode string `gorm:"size:32;uniqueIndex:uk_admin_host_agency_salary_policy_name;index:idx_admin_host_agency_salary_policy_region,not null;default:lalu" json:"appCode"`
|
||||||
Name string `gorm:"size:120;uniqueIndex:uk_admin_host_agency_salary_policy_name;not null" json:"name"`
|
Name string `gorm:"size:120;uniqueIndex:uk_admin_host_agency_salary_policy_name;not null" json:"name"`
|
||||||
// RegionID 是第一阶段的政策适用边界;结算时按用户所属区域命中唯一 active 政策。
|
// RegionID 是第一阶段的政策适用边界;结算时按用户所属区域命中唯一 active 政策。
|
||||||
RegionID int64 `gorm:"uniqueIndex:uk_admin_host_agency_salary_policy_name;uniqueIndex:uk_admin_host_agency_salary_policy_version;index:idx_admin_host_agency_salary_policy_region;index:idx_admin_host_agency_salary_policy_cycle,not null" json:"regionId"`
|
RegionID int64 `gorm:"uniqueIndex:uk_admin_host_agency_salary_policy_name;index:idx_admin_host_agency_salary_policy_region,not null" json:"regionId"`
|
||||||
// CycleKey 是工资归属月;PolicyVersion 使用政策主键形成稳定版本号,发布后两者均不可修改。
|
|
||||||
CycleKey string `gorm:"column:cycle_key;size:7;uniqueIndex:uk_admin_host_agency_salary_policy_version;index:idx_admin_host_agency_salary_policy_cycle,not null" json:"cycleKey"`
|
|
||||||
PolicyVersion uint64 `gorm:"column:policy_version;uniqueIndex:uk_admin_host_agency_salary_policy_version;not null;default:0" json:"policyVersion"`
|
|
||||||
Status string `gorm:"size:24;index:idx_admin_host_agency_salary_policy_region,not null;default:disabled" json:"status"`
|
Status string `gorm:"size:24;index:idx_admin_host_agency_salary_policy_region,not null;default:disabled" json:"status"`
|
||||||
// SettlementMode 只配置结算节奏,等级金额仍按累计值保存,实际发放由后续结算任务计算差额。
|
// SettlementMode 只配置结算节奏,等级金额仍按累计值保存,实际发放由后续结算任务计算差额。
|
||||||
SettlementMode string `gorm:"size:24;not null;default:daily" json:"settlementMode"`
|
SettlementMode string `gorm:"size:24;not null;default:daily" json:"settlementMode"`
|
||||||
@ -437,7 +434,7 @@ type HostAgencySalaryPolicy struct {
|
|||||||
// 比例字段使用 decimal 字符串承载,避免金币、钻石、美元换算出现 float 精度误差。
|
// 比例字段使用 decimal 字符串承载,避免金币、钻石、美元换算出现 float 精度误差。
|
||||||
GiftCoinToDiamondRatio string `gorm:"column:gift_coin_to_diamond_ratio;type:decimal(18,6);not null;default:1.000000" json:"giftCoinToDiamondRatio"`
|
GiftCoinToDiamondRatio string `gorm:"column:gift_coin_to_diamond_ratio;type:decimal(18,6);not null;default:1.000000" json:"giftCoinToDiamondRatio"`
|
||||||
ResidualDiamondToUSDRate string `gorm:"column:residual_diamond_to_usd_rate;type:decimal(24,12);not null;default:0.000000000000" json:"residualDiamondToUsdRate"`
|
ResidualDiamondToUSDRate string `gorm:"column:residual_diamond_to_usd_rate;type:decimal(24,12);not null;default:0.000000000000" json:"residualDiamondToUsdRate"`
|
||||||
// 时间范围保留为运营展示元数据;工资结算只按 CycleKey 绑定版本,不能再用执行时间选政策。
|
// 时间范围采用 epoch ms 半开区间,EffectiveToMS=0 表示长期有效。
|
||||||
EffectiveFromMS int64 `gorm:"column:effective_from_ms;index:idx_admin_host_agency_salary_policy_region,not null;default:0" json:"effectiveFromMs"`
|
EffectiveFromMS int64 `gorm:"column:effective_from_ms;index:idx_admin_host_agency_salary_policy_region,not null;default:0" json:"effectiveFromMs"`
|
||||||
EffectiveToMS int64 `gorm:"column:effective_to_ms;not null;default:0" json:"effectiveToMs"`
|
EffectiveToMS int64 `gorm:"column:effective_to_ms;not null;default:0" json:"effectiveToMs"`
|
||||||
Description string `gorm:"size:255;not null;default:''" json:"description"`
|
Description string `gorm:"size:255;not null;default:''" json:"description"`
|
||||||
@ -731,8 +728,6 @@ func (order CoinSellerRechargeOrder) BusinessTimeMS() int64 {
|
|||||||
type UserWithdrawalApplication struct {
|
type UserWithdrawalApplication struct {
|
||||||
ID uint `gorm:"primaryKey;index:idx_admin_withdrawal_app_operations_status_time,priority:4" json:"id"`
|
ID uint `gorm:"primaryKey;index:idx_admin_withdrawal_app_operations_status_time,priority:4" json:"id"`
|
||||||
AppCode string `gorm:"size:32;index:idx_admin_withdrawal_app_status_time;index:idx_admin_withdrawal_app_operations_status_time,priority:1;not null" json:"appCode"`
|
AppCode string `gorm:"size:32;index:idx_admin_withdrawal_app_status_time;index:idx_admin_withdrawal_app_operations_status_time,priority:1;not null" json:"appCode"`
|
||||||
SourceSystem string `gorm:"column:source_system;size:32;not null;default:hyapp_wallet" json:"sourceSystem"`
|
|
||||||
SourceApplicationID string `gorm:"column:source_application_id;size:128;not null;default:''" json:"sourceApplicationId"`
|
|
||||||
UserID string `gorm:"size:64;index:idx_admin_withdrawal_user;not null" json:"userId"`
|
UserID string `gorm:"size:64;index:idx_admin_withdrawal_user;not null" json:"userId"`
|
||||||
SalaryAssetType string `gorm:"size:64;not null;default:''" json:"salaryAssetType"`
|
SalaryAssetType string `gorm:"size:64;not null;default:''" json:"salaryAssetType"`
|
||||||
WithdrawAmount string `gorm:"type:decimal(18,2);not null;default:0.00" json:"withdrawAmount"`
|
WithdrawAmount string `gorm:"type:decimal(18,2);not null;default:0.00" json:"withdrawAmount"`
|
||||||
|
|||||||
@ -18,8 +18,6 @@ var (
|
|||||||
errLegacyCoinSellerOrderDuplicate = errors.New("legacy coin seller recharge order already exists")
|
errLegacyCoinSellerOrderDuplicate = errors.New("legacy coin seller recharge order already exists")
|
||||||
)
|
)
|
||||||
|
|
||||||
const legacyCoinSellerRechargeType = "USDT-进货"
|
|
||||||
|
|
||||||
type coinSellerTarget struct {
|
type coinSellerTarget struct {
|
||||||
UserID int64
|
UserID int64
|
||||||
DisplayUserID string
|
DisplayUserID string
|
||||||
@ -216,9 +214,7 @@ WHERE id = ?`, input.CoinAmount, input.UserID, input.GrantedAt, balanceID); err
|
|||||||
return legacyRechargeGrantResult{}, err
|
return legacyRechargeGrantResult{}, err
|
||||||
}
|
}
|
||||||
waterID := legacySnowflakeLikeID(input.GrantedAt)
|
waterID := legacySnowflakeLikeID(input.GrantedAt)
|
||||||
// likei-services 经理中心按固定 recharge_type=USDT-进货 汇总币商入金;
|
rechargeType := legacyRechargeType(input.ProviderCode, input.Chain)
|
||||||
// V5Pay、Binance 站内转账和链上网络属于支付凭证维度,已完整保存在 admin 订单中,不能写进旧钱包的统计分类字段。
|
|
||||||
rechargeType := legacyCoinSellerRechargeType
|
|
||||||
if _, err := tx.ExecContext(queryCtx, `
|
if _, err := tx.ExecContext(queryCtx, `
|
||||||
INSERT INTO user_freight_balance_running_water
|
INSERT INTO user_freight_balance_running_water
|
||||||
(id, sys_origin, user_id, accept_user_id, seller_id, type, quantity, usd_quantity, balance, amount, recharge_type, origin, remark, create_user, update_user, create_time, update_time)
|
(id, sys_origin, user_id, accept_user_id, seller_id, type, quantity, usd_quantity, balance, amount, recharge_type, origin, remark, create_user, update_user, create_time, update_time)
|
||||||
@ -262,6 +258,17 @@ func decimalBalanceAfter(earnPointsText string, consumptionText string, delta in
|
|||||||
return int64(earn) + delta - int64(consumption), nil
|
return int64(earn) + delta - int64(consumption), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func legacyRechargeType(providerCode string, chain string) string {
|
||||||
|
providerCode = strings.ToUpper(strings.TrimSpace(providerCode))
|
||||||
|
if providerCode == "USDT" {
|
||||||
|
if chain = strings.ToUpper(strings.TrimSpace(chain)); chain != "" {
|
||||||
|
return "USDT-" + chain + "-进货"
|
||||||
|
}
|
||||||
|
return "USDT-进货"
|
||||||
|
}
|
||||||
|
return providerCode + "-进货"
|
||||||
|
}
|
||||||
|
|
||||||
func legacySnowflakeLikeID(now time.Time) int64 {
|
func legacySnowflakeLikeID(now time.Time) int64 {
|
||||||
// 旧服务用 MyBatis ASSIGN_ID 写 bigint 主键;后台没有接入旧雪花 worker,只需保证单进程内足够唯一且单调。
|
// 旧服务用 MyBatis ASSIGN_ID 写 bigint 主键;后台没有接入旧雪花 worker,只需保证单进程内足够唯一且单调。
|
||||||
return now.UTC().UnixMilli()*1000 + int64(legacyWaterIDCounter.Add(1)%1000)
|
return now.UTC().UnixMilli()*1000 + int64(legacyWaterIDCounter.Add(1)%1000)
|
||||||
|
|||||||
@ -32,7 +32,7 @@ func TestLegacyCoinSellerRechargeWriterGrantUpdatesBalanceAndRunningWater(t *tes
|
|||||||
WithArgs(int64(1200), int64(10001), grantedAt, int64(77)).
|
WithArgs(int64(1200), int64(10001), grantedAt, int64(77)).
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
walletMock.ExpectExec(`(?s)INSERT INTO user_freight_balance_running_water`).
|
walletMock.ExpectExec(`(?s)INSERT INTO user_freight_balance_running_water`).
|
||||||
WithArgs(sqlmock.AnyArg(), "ATYOU", int64(10001), int64(10001), nil, int64(1200), int64(5200), "12.34", legacyCoinSellerRechargeType, "MIFA-001", int64(10001), int64(10001), grantedAt, grantedAt).
|
WithArgs(sqlmock.AnyArg(), "ATYOU", int64(10001), int64(10001), nil, int64(1200), int64(5200), "12.34", "MIFAPAY-进货", "MIFA-001", int64(10001), int64(10001), grantedAt, grantedAt).
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
walletMock.ExpectCommit()
|
walletMock.ExpectCommit()
|
||||||
|
|
||||||
|
|||||||
@ -1,100 +0,0 @@
|
|||||||
package financewithdrawal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"hyapp-admin-server/internal/model"
|
|
||||||
"hyapp-admin-server/internal/repository"
|
|
||||||
)
|
|
||||||
|
|
||||||
type markdownSender interface {
|
|
||||||
SendMarkdown(ctx context.Context, title string, text string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReviewPendingNotifier 只通知“当前待谁处理”,不参与提现创建、冻结或审核事务。
|
|
||||||
type ReviewPendingNotifier interface {
|
|
||||||
NotifyReviewPending(ctx context.Context, application model.UserWithdrawalApplication, stage repository.WithdrawalApplicationReviewStage) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type dingTalkReviewPendingNotifier struct {
|
|
||||||
sender markdownSender
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDingTalkReviewPendingNotifier(sender markdownSender) ReviewPendingNotifier {
|
|
||||||
if sender == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &dingTalkReviewPendingNotifier{sender: sender}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *dingTalkReviewPendingNotifier) NotifyReviewPending(ctx context.Context, application model.UserWithdrawalApplication, stage repository.WithdrawalApplicationReviewStage) error {
|
|
||||||
if n == nil || n.sender == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
title, text := withdrawalReviewPendingMarkdown(application, stage)
|
|
||||||
return n.sender.SendMarkdown(ctx, title, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
func withdrawalReviewPendingMarkdown(application model.UserWithdrawalApplication, stage repository.WithdrawalApplicationReviewStage) (string, string) {
|
|
||||||
stageName := "运营审核阶段"
|
|
||||||
pageURL := "https://admin-acc.global-interaction.com/finance/?view=withdrawalOperations"
|
|
||||||
if stage == repository.WithdrawalApplicationReviewStageFinance {
|
|
||||||
stageName = "财务审核阶段"
|
|
||||||
pageURL = "https://admin-acc.global-interaction.com/finance/?view=withdrawalFinance"
|
|
||||||
}
|
|
||||||
title := fmt.Sprintf("用户提现待处理【%s】", stageName)
|
|
||||||
lines := []string{
|
|
||||||
fmt.Sprintf("### 用户提现待处理【%s】", stageName),
|
|
||||||
"",
|
|
||||||
fmt.Sprintf("- 当前阶段:**%s**", stageName),
|
|
||||||
fmt.Sprintf("- 申请ID:%d", application.ID),
|
|
||||||
fmt.Sprintf("- APP:%s", withdrawalMarkdownValue(application.AppCode)),
|
|
||||||
fmt.Sprintf("- 来源系统:%s", withdrawalMarkdownValue(application.SourceSystem)),
|
|
||||||
fmt.Sprintf("- 来源单号:%s", withdrawalMarkdownValue(application.SourceApplicationID)),
|
|
||||||
fmt.Sprintf("- 用户ID:%s", withdrawalMarkdownValue(application.UserID)),
|
|
||||||
fmt.Sprintf("- 提现金额:$ %s", withdrawalMarkdownValue(application.WithdrawAmount)),
|
|
||||||
fmt.Sprintf("- 提现方式:%s", withdrawalMarkdownValue(application.WithdrawMethod)),
|
|
||||||
fmt.Sprintf("- 提现地址:%s", withdrawalMarkdownValue(application.WithdrawAddress)),
|
|
||||||
fmt.Sprintf("- 申请时间:%s", formatWithdrawalNotifyMillis(application.CreatedAtMS)),
|
|
||||||
}
|
|
||||||
if stage == repository.WithdrawalApplicationReviewStageFinance {
|
|
||||||
// 财务提醒保留运营审核快照,接收人无需先打开后台就能确认这是一张已完成初审的申请。
|
|
||||||
lines = append(lines,
|
|
||||||
fmt.Sprintf("- 运营审核人:%s", withdrawalMarkdownValue(application.OperationsReviewerName)),
|
|
||||||
fmt.Sprintf("- 运营审核备注:%s", withdrawalMarkdownValue(application.OperationsAuditRemark)),
|
|
||||||
fmt.Sprintf("- 运营通过时间:%s", formatWithdrawalNotifyMillis(int64Value(application.OperationsReviewedAtMS))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
lines = append(lines, "- 后台地址:"+pageURL)
|
|
||||||
return title, strings.Join(lines, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatWithdrawalNotifyMillis(value int64) string {
|
|
||||||
if value <= 0 {
|
|
||||||
return "-"
|
|
||||||
}
|
|
||||||
// 协作消息按运营团队使用的北京时间展示;接口和数据库仍以 Unix 毫秒 UTC 事实为准。
|
|
||||||
return time.UnixMilli(value).In(time.FixedZone("UTC+8", 8*60*60)).Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
|
|
||||||
func int64Value(value *int64) int64 {
|
|
||||||
if value == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return *value
|
|
||||||
}
|
|
||||||
|
|
||||||
func withdrawalMarkdownValue(value string) string {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if value == "" {
|
|
||||||
return "-"
|
|
||||||
}
|
|
||||||
runes := []rune(value)
|
|
||||||
if len(runes) > 240 {
|
|
||||||
value = string(runes[:240]) + "..."
|
|
||||||
}
|
|
||||||
return strings.NewReplacer("\r", " ", "\n", " ", "\t", " ").Replace(value)
|
|
||||||
}
|
|
||||||
@ -8,7 +8,6 @@ import (
|
|||||||
"hyapp-admin-server/internal/appctx"
|
"hyapp-admin-server/internal/appctx"
|
||||||
"hyapp-admin-server/internal/integration/activityclient"
|
"hyapp-admin-server/internal/integration/activityclient"
|
||||||
"hyapp-admin-server/internal/integration/walletclient"
|
"hyapp-admin-server/internal/integration/walletclient"
|
||||||
"hyapp-admin-server/internal/integration/withdrawalsource"
|
|
||||||
"hyapp-admin-server/internal/middleware"
|
"hyapp-admin-server/internal/middleware"
|
||||||
"hyapp-admin-server/internal/modules/shared"
|
"hyapp-admin-server/internal/modules/shared"
|
||||||
"hyapp-admin-server/internal/repository"
|
"hyapp-admin-server/internal/repository"
|
||||||
@ -21,64 +20,10 @@ import (
|
|||||||
type Handler struct {
|
type Handler struct {
|
||||||
service *Service
|
service *Service
|
||||||
audit shared.OperationLogger
|
audit shared.OperationLogger
|
||||||
sources *withdrawalsource.Registry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(store *repository.Store, wallet walletclient.Client, activity activityclient.Client, audit shared.OperationLogger) *Handler {
|
func New(store *repository.Store, wallet walletclient.Client, activity activityclient.Client, audit shared.OperationLogger) *Handler {
|
||||||
return NewWithSources(store, wallet, activity, audit, nil)
|
return &Handler{service: NewService(store, wallet, activity), audit: audit}
|
||||||
}
|
|
||||||
|
|
||||||
func NewWithSources(store *repository.Store, wallet walletclient.Client, activity activityclient.Client, audit shared.OperationLogger, sources *withdrawalsource.Registry) *Handler {
|
|
||||||
return &Handler{service: NewServiceWithSources(store, wallet, activity, sources), audit: audit, sources: sources}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) BindReviewPendingNotifier(notifier ReviewPendingNotifier) *Handler {
|
|
||||||
if h != nil && h.service != nil {
|
|
||||||
h.service.BindReviewPendingNotifier(notifier)
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateExternalApplication 是来源钱包的服务间入口,不使用后台 JWT。
|
|
||||||
// 每个 App 使用独立 submit token,且业务唯一键保证来源端持久重试不会生成重复审核单。
|
|
||||||
func (h *Handler) CreateExternalApplication(c *gin.Context) {
|
|
||||||
var req struct {
|
|
||||||
AppCode string `json:"appCode" binding:"required"`
|
|
||||||
SourceSystem string `json:"sourceSystem" binding:"required"`
|
|
||||||
SourceApplicationID string `json:"sourceApplicationId" binding:"required"`
|
|
||||||
UserID string `json:"userId" binding:"required"`
|
|
||||||
SalaryAssetType string `json:"salaryAssetType" binding:"required"`
|
|
||||||
WithdrawAmount string `json:"withdrawAmount" binding:"required"`
|
|
||||||
WithdrawAmountMinor int64 `json:"withdrawAmountMinor" binding:"required"`
|
|
||||||
WithdrawMethod string `json:"withdrawMethod" binding:"required"`
|
|
||||||
WithdrawAddress string `json:"withdrawAddress" binding:"required"`
|
|
||||||
CreatedAtMS int64 `json:"createdAtMs"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
response.BadRequest(c, "提现申请参数不正确")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if h.sources == nil || !h.sources.AuthenticateSubmit(req.AppCode, req.SourceSystem, c.GetHeader("Authorization")) {
|
|
||||||
response.Unauthorized(c, "提现来源鉴权失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
item, err := h.service.CreateExternalApplication(c.Request.Context(), externalWithdrawalApplicationInput{
|
|
||||||
AppCode: req.AppCode,
|
|
||||||
SourceSystem: req.SourceSystem,
|
|
||||||
SourceApplicationID: req.SourceApplicationID,
|
|
||||||
UserID: req.UserID,
|
|
||||||
SalaryAssetType: req.SalaryAssetType,
|
|
||||||
WithdrawAmount: req.WithdrawAmount,
|
|
||||||
WithdrawAmountMinor: req.WithdrawAmountMinor,
|
|
||||||
WithdrawMethod: req.WithdrawMethod,
|
|
||||||
WithdrawAddress: req.WithdrawAddress,
|
|
||||||
CreatedAtMS: req.CreatedAtMS,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
response.BadRequest(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response.Created(c, item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) ListApplications(c *gin.Context) {
|
func (h *Handler) ListApplications(c *gin.Context) {
|
||||||
|
|||||||
@ -6,8 +6,6 @@ type withdrawalApplicationDTO struct {
|
|||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
AppCode string `json:"appCode"`
|
AppCode string `json:"appCode"`
|
||||||
AppName string `json:"appName"`
|
AppName string `json:"appName"`
|
||||||
SourceSystem string `json:"sourceSystem"`
|
|
||||||
SourceApplicationID string `json:"sourceApplicationId"`
|
|
||||||
UserID string `json:"userId"`
|
UserID string `json:"userId"`
|
||||||
SalaryAssetType string `json:"salaryAssetType"`
|
SalaryAssetType string `json:"salaryAssetType"`
|
||||||
WithdrawAmount string `json:"withdrawAmount"`
|
WithdrawAmount string `json:"withdrawAmount"`
|
||||||
@ -43,8 +41,6 @@ func withdrawalApplicationDTOFromModel(item model.UserWithdrawalApplication) wit
|
|||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
AppCode: item.AppCode,
|
AppCode: item.AppCode,
|
||||||
AppName: item.AppCode,
|
AppName: item.AppCode,
|
||||||
SourceSystem: item.SourceSystem,
|
|
||||||
SourceApplicationID: item.SourceApplicationID,
|
|
||||||
UserID: item.UserID,
|
UserID: item.UserID,
|
||||||
SalaryAssetType: item.SalaryAssetType,
|
SalaryAssetType: item.SalaryAssetType,
|
||||||
WithdrawAmount: item.WithdrawAmount,
|
WithdrawAmount: item.WithdrawAmount,
|
||||||
|
|||||||
@ -17,10 +17,3 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
|||||||
protected.POST("/admin/operations/withdrawal-applications/:application_id/approve", middleware.RequirePermission(permissionAuditOperationsWithdrawals), h.ApproveOperationsApplication)
|
protected.POST("/admin/operations/withdrawal-applications/:application_id/approve", middleware.RequirePermission(permissionAuditOperationsWithdrawals), h.ApproveOperationsApplication)
|
||||||
protected.POST("/admin/operations/withdrawal-applications/:application_id/reject", middleware.RequirePermission(permissionAuditOperationsWithdrawals), h.RejectOperationsApplication)
|
protected.POST("/admin/operations/withdrawal-applications/:application_id/reject", middleware.RequirePermission(permissionAuditOperationsWithdrawals), h.RejectOperationsApplication)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterIntegrationRoutes(api *gin.RouterGroup, h *Handler) {
|
|
||||||
if h == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
api.POST("/integrations/withdrawal-applications", h.CreateExternalApplication)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"math/big"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -14,7 +12,6 @@ import (
|
|||||||
"hyapp-admin-server/internal/appctx"
|
"hyapp-admin-server/internal/appctx"
|
||||||
"hyapp-admin-server/internal/integration/activityclient"
|
"hyapp-admin-server/internal/integration/activityclient"
|
||||||
"hyapp-admin-server/internal/integration/walletclient"
|
"hyapp-admin-server/internal/integration/walletclient"
|
||||||
"hyapp-admin-server/internal/integration/withdrawalsource"
|
|
||||||
"hyapp-admin-server/internal/model"
|
"hyapp-admin-server/internal/model"
|
||||||
"hyapp-admin-server/internal/modules/shared"
|
"hyapp-admin-server/internal/modules/shared"
|
||||||
"hyapp-admin-server/internal/repository"
|
"hyapp-admin-server/internal/repository"
|
||||||
@ -38,24 +35,9 @@ type Service struct {
|
|||||||
store *repository.Store
|
store *repository.Store
|
||||||
wallet walletclient.Client
|
wallet walletclient.Client
|
||||||
activity activityclient.Client
|
activity activityclient.Client
|
||||||
sources *withdrawalsource.Registry
|
|
||||||
notifier ReviewPendingNotifier
|
|
||||||
now func() time.Time
|
now func() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type externalWithdrawalApplicationInput struct {
|
|
||||||
AppCode string
|
|
||||||
SourceSystem string
|
|
||||||
SourceApplicationID string
|
|
||||||
UserID string
|
|
||||||
SalaryAssetType string
|
|
||||||
WithdrawAmount string
|
|
||||||
WithdrawAmountMinor int64
|
|
||||||
WithdrawMethod string
|
|
||||||
WithdrawAddress string
|
|
||||||
CreatedAtMS int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type pointWithdrawalWallet interface {
|
type pointWithdrawalWallet interface {
|
||||||
SettlePointWithdrawal(ctx context.Context, req *walletv1.SettlePointWithdrawalRequest) (*walletv1.SettlePointWithdrawalResponse, error)
|
SettlePointWithdrawal(ctx context.Context, req *walletv1.SettlePointWithdrawalRequest) (*walletv1.SettlePointWithdrawalResponse, error)
|
||||||
ReleasePointWithdrawal(ctx context.Context, req *walletv1.ReleasePointWithdrawalRequest) (*walletv1.ReleasePointWithdrawalResponse, error)
|
ReleasePointWithdrawal(ctx context.Context, req *walletv1.ReleasePointWithdrawalRequest) (*walletv1.ReleasePointWithdrawalResponse, error)
|
||||||
@ -65,86 +47,6 @@ func NewService(store *repository.Store, wallet walletclient.Client, activity ac
|
|||||||
return &Service{store: store, wallet: wallet, activity: activity, now: func() time.Time { return time.Now().UTC() }}
|
return &Service{store: store, wallet: wallet, activity: activity, now: func() time.Time { return time.Now().UTC() }}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServiceWithSources(store *repository.Store, wallet walletclient.Client, activity activityclient.Client, sources *withdrawalsource.Registry) *Service {
|
|
||||||
service := NewService(store, wallet, activity)
|
|
||||||
service.sources = sources
|
|
||||||
return service
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) BindReviewPendingNotifier(notifier ReviewPendingNotifier) *Service {
|
|
||||||
if s != nil {
|
|
||||||
s.notifier = notifier
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CreateExternalApplication(ctx context.Context, input externalWithdrawalApplicationInput) (*withdrawalApplicationDTO, error) {
|
|
||||||
if s == nil || s.store == nil || s.sources == nil {
|
|
||||||
return nil, errors.New("external withdrawal intake is not configured")
|
|
||||||
}
|
|
||||||
input.AppCode = appctx.Normalize(input.AppCode)
|
|
||||||
input.SourceSystem = strings.ToLower(strings.TrimSpace(input.SourceSystem))
|
|
||||||
input.SourceApplicationID = strings.TrimSpace(input.SourceApplicationID)
|
|
||||||
input.UserID = strings.TrimSpace(input.UserID)
|
|
||||||
input.SalaryAssetType = strings.TrimSpace(input.SalaryAssetType)
|
|
||||||
input.WithdrawAmount = strings.TrimSpace(input.WithdrawAmount)
|
|
||||||
input.WithdrawMethod = strings.TrimSpace(input.WithdrawMethod)
|
|
||||||
input.WithdrawAddress = strings.TrimSpace(input.WithdrawAddress)
|
|
||||||
if !s.sources.HasSource(input.AppCode, input.SourceSystem) {
|
|
||||||
return nil, errors.New("withdrawal source is not configured")
|
|
||||||
}
|
|
||||||
if input.SourceApplicationID == "" || len(input.SourceApplicationID) > 128 || input.UserID == "" || len(input.UserID) > 64 {
|
|
||||||
return nil, errors.New("来源申请号或用户 ID 不正确")
|
|
||||||
}
|
|
||||||
if input.SalaryAssetType == "" || len(input.SalaryAssetType) > 64 || input.WithdrawMethod == "" || len(input.WithdrawMethod) > 64 || input.WithdrawAddress == "" || len(input.WithdrawAddress) > 255 {
|
|
||||||
return nil, errors.New("提现资产或收款信息不完整")
|
|
||||||
}
|
|
||||||
if err := validateExternalWithdrawalAmount(input.WithdrawAmount, input.WithdrawAmountMinor); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
createdAtMS := input.CreatedAtMS
|
|
||||||
if createdAtMS <= 0 {
|
|
||||||
createdAtMS = s.now().UnixMilli()
|
|
||||||
}
|
|
||||||
application, err := s.store.CreateExternalWithdrawalApplication(model.UserWithdrawalApplication{
|
|
||||||
AppCode: input.AppCode,
|
|
||||||
SourceSystem: input.SourceSystem,
|
|
||||||
SourceApplicationID: input.SourceApplicationID,
|
|
||||||
UserID: input.UserID,
|
|
||||||
SalaryAssetType: input.SalaryAssetType,
|
|
||||||
WithdrawAmount: input.WithdrawAmount,
|
|
||||||
WithdrawAmountMinor: input.WithdrawAmountMinor,
|
|
||||||
WithdrawMethod: input.WithdrawMethod,
|
|
||||||
WithdrawAddress: input.WithdrawAddress,
|
|
||||||
// 外部来源在提交前已经完成扣款;这两个字段保留来源事实,真正终态由 callback adapter 执行,绝不误调用 HY wallet。
|
|
||||||
FreezeCommandID: "external:" + input.SourceSystem + ":" + input.SourceApplicationID,
|
|
||||||
FreezeTransactionID: "external:" + input.SourceApplicationID,
|
|
||||||
Status: model.WithdrawalApplicationStatusPending,
|
|
||||||
OperationsStatus: model.WithdrawalOperationsStatusPending,
|
|
||||||
CreatedAtMS: createdAtMS,
|
|
||||||
UpdatedAtMS: createdAtMS,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 外部 Likei/Aslan 申请直接进入 Admin,不经过 Gateway 的创建通知;落库成功后在这里补齐运营阶段提醒。
|
|
||||||
s.notifyReviewPending(ctx, *application, repository.WithdrawalApplicationReviewStageOperations)
|
|
||||||
dto := withdrawalApplicationDTOFromModel(*application)
|
|
||||||
return &dto, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateExternalWithdrawalAmount(amount string, amountMinor int64) error {
|
|
||||||
parsed, ok := new(big.Rat).SetString(strings.TrimSpace(amount))
|
|
||||||
if !ok || parsed.Sign() <= 0 || amountMinor <= 0 {
|
|
||||||
return errors.New("提现金额不正确")
|
|
||||||
}
|
|
||||||
minor := new(big.Rat).Mul(parsed, big.NewRat(100, 1))
|
|
||||||
if !minor.IsInt() || !minor.Num().IsInt64() || minor.Num().Int64() != amountMinor {
|
|
||||||
return errors.New("提现金额与最小单位金额不一致")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListApplications(actor shared.Actor, options repository.WithdrawalApplicationListOptions) ([]withdrawalApplicationDTO, int64, error) {
|
func (s *Service) ListApplications(actor shared.Actor, options repository.WithdrawalApplicationListOptions) ([]withdrawalApplicationDTO, int64, error) {
|
||||||
if s == nil || s.store == nil {
|
if s == nil || s.store == nil {
|
||||||
return nil, 0, errors.New("admin store is not configured")
|
return nil, 0, errors.New("admin store is not configured")
|
||||||
@ -254,16 +156,19 @@ func (s *Service) auditApplication(ctx context.Context, actor shared.Actor, id u
|
|||||||
AuditImageURL: auditImageURL,
|
AuditImageURL: auditImageURL,
|
||||||
ApprovedAtMS: nowMS,
|
ApprovedAtMS: nowMS,
|
||||||
}, func(item model.UserWithdrawalApplication) (repository.WithdrawalApplicationAuditEffect, error) {
|
}, func(item model.UserWithdrawalApplication) (repository.WithdrawalApplicationAuditEffect, error) {
|
||||||
|
userID, parseErr := withdrawalWalletUserID(item)
|
||||||
|
if parseErr != nil {
|
||||||
|
return repository.WithdrawalApplicationAuditEffect{}, parseErr
|
||||||
|
}
|
||||||
if stage == repository.WithdrawalApplicationReviewStageOperations && decision == model.WithdrawalApplicationStatusApproved {
|
if stage == repository.WithdrawalApplicationReviewStageOperations && decision == model.WithdrawalApplicationStatusApproved {
|
||||||
// 运营通过只确认业务资料并转交财务;申请金额继续保留在 frozen,且不能提前向用户发送最终通过通知。
|
// 运营通过只确认业务资料并转交财务;申请金额继续保留在 frozen,且不能提前向用户发送最终通过通知。
|
||||||
return repository.WithdrawalApplicationAuditEffect{}, nil
|
return repository.WithdrawalApplicationAuditEffect{}, nil
|
||||||
}
|
}
|
||||||
transactionID, userID, walletErr := s.applyWithdrawalDecision(ctx, item, decision, commandID, appCode, actor, remark, auditImageURL, id)
|
transactionID, walletErr := s.applyWalletDecision(ctx, decision, commandID, appCode, userID, item.SalaryAssetType, item.WithdrawAmountMinor, item.PointFeeAmount, item.PointNetAmount, item.PointsPerUSD, item.PointFeeBPS, actor, strings.TrimSpace(remark), id)
|
||||||
if walletErr != nil {
|
if walletErr != nil {
|
||||||
return repository.WithdrawalApplicationAuditEffect{}, walletErr
|
return repository.WithdrawalApplicationAuditEffect{}, walletErr
|
||||||
}
|
}
|
||||||
if !isExternalWithdrawal(item) {
|
if noticeErr := s.sendWithdrawalAuditNotice(ctx, auditNoticeInput{
|
||||||
noticeErr := s.sendWithdrawalAuditNotice(ctx, auditNoticeInput{
|
|
||||||
ApplicationID: id,
|
ApplicationID: id,
|
||||||
AppCode: appCode,
|
AppCode: appCode,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
@ -276,39 +181,19 @@ func (s *Service) auditApplication(ctx context.Context, actor shared.Actor, id u
|
|||||||
WithdrawMethod: item.WithdrawMethod,
|
WithdrawMethod: item.WithdrawMethod,
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
SentAtMS: nowMS,
|
SentAtMS: nowMS,
|
||||||
})
|
}); noticeErr != nil {
|
||||||
if noticeErr != nil {
|
|
||||||
// 钱包扣冻/释放使用阶段固定 command id,通知使用阶段+结果事件 id;回调失败会回滚 admin 状态,重试不会重复资金动作或消息。
|
// 钱包扣冻/释放使用阶段固定 command id,通知使用阶段+结果事件 id;回调失败会回滚 admin 状态,重试不会重复资金动作或消息。
|
||||||
return repository.WithdrawalApplicationAuditEffect{}, noticeErr
|
return repository.WithdrawalApplicationAuditEffect{}, noticeErr
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return repository.WithdrawalApplicationAuditEffect{CommandID: commandID, TransactionID: transactionID}, nil
|
return repository.WithdrawalApplicationAuditEffect{CommandID: commandID, TransactionID: transactionID}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if stage == repository.WithdrawalApplicationReviewStageOperations && decision == model.WithdrawalApplicationStatusApproved {
|
|
||||||
// 运营通过已经提交到数据库,随后独立提醒财务;机器人异常不能把已转交的申请回滚到运营队列。
|
|
||||||
s.notifyReviewPending(ctx, *updated, repository.WithdrawalApplicationReviewStageFinance)
|
|
||||||
}
|
|
||||||
dto := withdrawalApplicationDTOFromModel(*updated)
|
dto := withdrawalApplicationDTOFromModel(*updated)
|
||||||
return &dto, nil
|
return &dto, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) notifyReviewPending(ctx context.Context, application model.UserWithdrawalApplication, stage repository.WithdrawalApplicationReviewStage) {
|
|
||||||
if s == nil || s.notifier == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := s.notifier.NotifyReviewPending(ctx, application, stage); err != nil {
|
|
||||||
slog.Warn("withdrawal_review_dingtalk_notify_failed",
|
|
||||||
"application_id", application.ID,
|
|
||||||
"app_code", application.AppCode,
|
|
||||||
"review_stage", stage,
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// executeOperationsRejection 把拒绝拆成 claim -> 外部幂等动作 -> finalize 三段。
|
// executeOperationsRejection 把拒绝拆成 claim -> 外部幂等动作 -> finalize 三段。
|
||||||
// claim 是独立短事务,因此 wallet 已释放后即使 notice 或最终 admin 提交失败,申请仍停在 rejecting,不能被改点通过或进入财务。
|
// claim 是独立短事务,因此 wallet 已释放后即使 notice 或最终 admin 提交失败,申请仍停在 rejecting,不能被改点通过或进入财务。
|
||||||
func (s *Service) executeOperationsRejection(ctx context.Context, actor shared.Actor, id uint, appCode string, remark string, requestID string, commandID string, nowMS int64) (*withdrawalApplicationDTO, error) {
|
func (s *Service) executeOperationsRejection(ctx context.Context, actor shared.Actor, id uint, appCode string, remark string, requestID string, commandID string, nowMS int64) (*withdrawalApplicationDTO, error) {
|
||||||
@ -321,7 +206,8 @@ func (s *Service) executeOperationsRejection(ctx context.Context, actor shared.A
|
|||||||
AuditCommandID: commandID,
|
AuditCommandID: commandID,
|
||||||
ApprovedAtMS: nowMS,
|
ApprovedAtMS: nowMS,
|
||||||
}, func(item model.UserWithdrawalApplication) error {
|
}, func(item model.UserWithdrawalApplication) error {
|
||||||
return validateWithdrawalDecisionTarget(item)
|
_, err := withdrawalWalletUserID(item)
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -332,13 +218,17 @@ func (s *Service) executeOperationsRejection(ctx context.Context, actor shared.A
|
|||||||
return &dto, nil
|
return &dto, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userID, err := withdrawalWalletUserID(*claimed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if claimed.OperationsReviewerUserID == nil || *claimed.OperationsReviewerUserID == 0 || strings.TrimSpace(claimed.OperationsAuditRemark) == "" {
|
if claimed.OperationsReviewerUserID == nil || *claimed.OperationsReviewerUserID == 0 || strings.TrimSpace(claimed.OperationsAuditRemark) == "" {
|
||||||
return nil, errors.New("提现单运营拒绝 claim 不完整")
|
return nil, errors.New("提现单运营拒绝 claim 不完整")
|
||||||
}
|
}
|
||||||
// 重试沿用第一次 claim 的审核人和拒绝原因,不能让资金 metadata、用户通知和后台审计快照随重试人改变。
|
// 重试沿用第一次 claim 的审核人和拒绝原因,不能让资金 metadata、用户通知和后台审计快照随重试人改变。
|
||||||
claimedActor := shared.Actor{UserID: *claimed.OperationsReviewerUserID, Username: claimed.OperationsReviewerName}
|
claimedActor := shared.Actor{UserID: *claimed.OperationsReviewerUserID, Username: claimed.OperationsReviewerName}
|
||||||
claimedRemark := strings.TrimSpace(claimed.OperationsAuditRemark)
|
claimedRemark := strings.TrimSpace(claimed.OperationsAuditRemark)
|
||||||
transactionID, userID, err := s.applyWithdrawalDecision(ctx, *claimed, model.WithdrawalApplicationStatusRejected, commandID, appCode, claimedActor, claimedRemark, "", id)
|
transactionID, err := s.applyWalletDecision(ctx, model.WithdrawalApplicationStatusRejected, commandID, appCode, userID, claimed.SalaryAssetType, claimed.WithdrawAmountMinor, claimed.PointFeeAmount, claimed.PointNetAmount, claimed.PointsPerUSD, claimed.PointFeeBPS, claimedActor, claimedRemark, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -346,8 +236,7 @@ func (s *Service) executeOperationsRejection(ctx context.Context, actor shared.A
|
|||||||
if claimed.OperationsReviewedAtMS != nil && *claimed.OperationsReviewedAtMS > 0 {
|
if claimed.OperationsReviewedAtMS != nil && *claimed.OperationsReviewedAtMS > 0 {
|
||||||
sentAtMS = *claimed.OperationsReviewedAtMS
|
sentAtMS = *claimed.OperationsReviewedAtMS
|
||||||
}
|
}
|
||||||
if !isExternalWithdrawal(*claimed) {
|
if err := s.sendWithdrawalAuditNotice(ctx, auditNoticeInput{
|
||||||
err = s.sendWithdrawalAuditNotice(ctx, auditNoticeInput{
|
|
||||||
ApplicationID: id,
|
ApplicationID: id,
|
||||||
AppCode: appCode,
|
AppCode: appCode,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
@ -359,12 +248,10 @@ func (s *Service) executeOperationsRejection(ctx context.Context, actor shared.A
|
|||||||
WithdrawMethod: claimed.WithdrawMethod,
|
WithdrawMethod: claimed.WithdrawMethod,
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
SentAtMS: sentAtMS,
|
SentAtMS: sentAtMS,
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
|
||||||
// rejecting 已在前一事务提交;返回错误让运营页展示“重试拒绝”,绝不能回退成 pending。
|
// rejecting 已在前一事务提交;返回错误让运营页展示“重试拒绝”,绝不能回退成 pending。
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
updated, err := s.store.FinalizeWithdrawalOperationsRejectionForApp(appCode, id, commandID, transactionID, s.now().UnixMilli())
|
updated, err := s.store.FinalizeWithdrawalOperationsRejectionForApp(appCode, id, commandID, transactionID, s.now().UnixMilli())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -381,52 +268,6 @@ func withdrawalWalletUserID(item model.UserWithdrawalApplication) (int64, error)
|
|||||||
return userID, nil
|
return userID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateWithdrawalDecisionTarget(item model.UserWithdrawalApplication) error {
|
|
||||||
if isExternalWithdrawal(item) {
|
|
||||||
if strings.TrimSpace(item.SourceApplicationID) == "" || strings.TrimSpace(item.UserID) == "" || item.WithdrawAmountMinor <= 0 {
|
|
||||||
return errors.New("外部提现单来源信息不完整")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
_, err := withdrawalWalletUserID(item)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func isExternalWithdrawal(item model.UserWithdrawalApplication) bool {
|
|
||||||
sourceSystem := strings.ToLower(strings.TrimSpace(item.SourceSystem))
|
|
||||||
return sourceSystem != "" && sourceSystem != "hyapp_wallet"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) applyWithdrawalDecision(ctx context.Context, item model.UserWithdrawalApplication, decision string, commandID string, appCode string, actor shared.Actor, remark string, auditImageURL string, applicationID uint) (string, int64, error) {
|
|
||||||
if isExternalWithdrawal(item) {
|
|
||||||
if s.sources == nil {
|
|
||||||
return "", 0, errors.New("withdrawal source callback is not configured")
|
|
||||||
}
|
|
||||||
sourceDecision := "NOT_PASS"
|
|
||||||
credentials := []string(nil)
|
|
||||||
if decision == model.WithdrawalApplicationStatusApproved {
|
|
||||||
sourceDecision = "PASS"
|
|
||||||
credentials = []string{strings.TrimSpace(auditImageURL)}
|
|
||||||
}
|
|
||||||
transactionID, err := s.sources.ApplyDecision(ctx, appCode, item.SourceSystem, withdrawalsource.DecisionRequest{
|
|
||||||
SourceApplicationID: item.SourceApplicationID,
|
|
||||||
Decision: sourceDecision,
|
|
||||||
CredentialURLs: credentials,
|
|
||||||
Remark: strings.TrimSpace(remark),
|
|
||||||
ReviewerUserID: actor.UserID,
|
|
||||||
ReviewerName: actor.Username,
|
|
||||||
CommandID: commandID,
|
|
||||||
})
|
|
||||||
return transactionID, 0, err
|
|
||||||
}
|
|
||||||
userID, err := withdrawalWalletUserID(item)
|
|
||||||
if err != nil {
|
|
||||||
return "", 0, err
|
|
||||||
}
|
|
||||||
transactionID, err := s.applyWalletDecision(ctx, decision, commandID, appCode, userID, item.SalaryAssetType, item.WithdrawAmountMinor, item.PointFeeAmount, item.PointNetAmount, item.PointsPerUSD, item.PointFeeBPS, actor, strings.TrimSpace(remark), applicationID)
|
|
||||||
return transactionID, userID, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) applyWalletDecision(ctx context.Context, decision string, commandID string, appCode string, userID int64, assetType string, amountMinor int64, storedFeePoints int64, storedNetPoints int64, storedPointsPerUSD int64, storedFeeBPS int32, actor shared.Actor, remark string, applicationID uint) (string, error) {
|
func (s *Service) applyWalletDecision(ctx context.Context, decision string, commandID string, appCode string, userID int64, assetType string, amountMinor int64, storedFeePoints int64, storedNetPoints int64, storedPointsPerUSD int64, storedFeeBPS int32, actor shared.Actor, remark string, applicationID uint) (string, error) {
|
||||||
reason := strings.TrimSpace(remark)
|
reason := strings.TrimSpace(remark)
|
||||||
if reason == "" {
|
if reason == "" {
|
||||||
|
|||||||
@ -29,7 +29,6 @@ func (h *Handler) ListPolicies(c *gin.Context) {
|
|||||||
items, total, err := h.service.List(appctx.FromContext(c.Request.Context()), repository.HostAgencySalaryPolicyListOptions{
|
items, total, err := h.service.List(appctx.FromContext(c.Request.Context()), repository.HostAgencySalaryPolicyListOptions{
|
||||||
Keyword: options.Keyword,
|
Keyword: options.Keyword,
|
||||||
RegionID: queryInt64(c, "region_id", "regionId"),
|
RegionID: queryInt64(c, "region_id", "regionId"),
|
||||||
CycleKey: firstQuery(c, "cycle_key", "cycleKey"),
|
|
||||||
Status: options.Status,
|
Status: options.Status,
|
||||||
SettlementMode: firstQuery(c, "settlement_mode", "settlementMode"),
|
SettlementMode: firstQuery(c, "settlement_mode", "settlementMode"),
|
||||||
// trigger mode 可直接筛出“只允许手动结算”的政策,后续工资结算页会复用同一查询口径。
|
// trigger mode 可直接筛出“只允许手动结算”的政策,后续工资结算页会复用同一查询口径。
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package hostagencypolicy
|
|||||||
type policyRequest struct {
|
type policyRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
RegionID int64 `json:"region_id"`
|
RegionID int64 `json:"region_id"`
|
||||||
CycleKey string `json:"cycle_key"`
|
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
SettlementMode string `json:"settlement_mode"`
|
SettlementMode string `json:"settlement_mode"`
|
||||||
SettlementTriggerMode string `json:"settlement_trigger_mode"`
|
SettlementTriggerMode string `json:"settlement_trigger_mode"`
|
||||||
|
|||||||
@ -11,8 +11,6 @@ type policyDTO struct {
|
|||||||
AppCode string `json:"app_code"`
|
AppCode string `json:"app_code"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
RegionID int64 `json:"region_id"`
|
RegionID int64 `json:"region_id"`
|
||||||
CycleKey string `json:"cycle_key"`
|
|
||||||
PolicyVersion uint64 `json:"policy_version"`
|
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
SettlementMode string `json:"settlement_mode"`
|
SettlementMode string `json:"settlement_mode"`
|
||||||
SettlementTriggerMode string `json:"settlement_trigger_mode"`
|
SettlementTriggerMode string `json:"settlement_trigger_mode"`
|
||||||
@ -58,8 +56,6 @@ func policyFromModel(item model.HostAgencySalaryPolicy) policyDTO {
|
|||||||
AppCode: item.AppCode,
|
AppCode: item.AppCode,
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
RegionID: item.RegionID,
|
RegionID: item.RegionID,
|
||||||
CycleKey: item.CycleKey,
|
|
||||||
PolicyVersion: item.PolicyVersion,
|
|
||||||
Status: item.Status,
|
Status: item.Status,
|
||||||
SettlementMode: item.SettlementMode,
|
SettlementMode: item.SettlementMode,
|
||||||
SettlementTriggerMode: firstNonBlank(item.SettlementTriggerMode, settlementTriggerAutomatic),
|
SettlementTriggerMode: firstNonBlank(item.SettlementTriggerMode, settlementTriggerAutomatic),
|
||||||
|
|||||||
@ -69,7 +69,7 @@ func (s *Service) Create(appCode string, actorID uint, req policyRequest) (polic
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return policyDTO{}, err
|
return policyDTO{}, err
|
||||||
}
|
}
|
||||||
// 启用政策必须先做周期冲突校验,否则同一区域同一月份会出现两套工资规则。
|
// 启用政策必须先做时间段冲突校验,否则同一区域结算时会匹配到两套工资规则。
|
||||||
if err := s.ensureNoActiveOverlap(item, 0); err != nil {
|
if err := s.ensureNoActiveOverlap(item, 0); err != nil {
|
||||||
return policyDTO{}, err
|
return policyDTO{}, err
|
||||||
}
|
}
|
||||||
@ -85,10 +85,6 @@ func (s *Service) Update(appCode string, actorID uint, id uint, req policyReques
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return policyDTO{}, err
|
return policyDTO{}, err
|
||||||
}
|
}
|
||||||
if item.PublishStatus == publishStatusPublished || item.PublishedAtMS > 0 {
|
|
||||||
// 运行侧已经按周期绑定该版本;编辑会让历史结算口径漂移,只能为新周期新建政策。
|
|
||||||
return policyDTO{}, errors.New("已发布的周期政策版本不可修改,请为新周期创建政策")
|
|
||||||
}
|
|
||||||
// 请求体重新走创建同一套校验,编辑和新增保持完全一致的字段约束。
|
// 请求体重新走创建同一套校验,编辑和新增保持完全一致的字段约束。
|
||||||
updated, err := policyModelFromRequest(item.AppCode, actorID, req)
|
updated, err := policyModelFromRequest(item.AppCode, actorID, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -96,7 +92,6 @@ func (s *Service) Update(appCode string, actorID uint, id uint, req policyReques
|
|||||||
}
|
}
|
||||||
item.Name = updated.Name
|
item.Name = updated.Name
|
||||||
item.RegionID = updated.RegionID
|
item.RegionID = updated.RegionID
|
||||||
item.CycleKey = updated.CycleKey
|
|
||||||
item.Status = updated.Status
|
item.Status = updated.Status
|
||||||
item.SettlementMode = updated.SettlementMode
|
item.SettlementMode = updated.SettlementMode
|
||||||
item.SettlementTriggerMode = updated.SettlementTriggerMode
|
item.SettlementTriggerMode = updated.SettlementTriggerMode
|
||||||
@ -120,15 +115,14 @@ func (s *Service) Update(appCode string, actorID uint, id uint, req policyReques
|
|||||||
return policyFromModel(item), nil
|
return policyFromModel(item), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Delete(_ context.Context, appCode string, id uint) error {
|
func (s *Service) Delete(ctx context.Context, appCode string, id uint) error {
|
||||||
// 先按 app_code 查一次,确保删除动作只能落在当前应用上下文内,随后仓储层事务删除主表和等级表。
|
// 先按 app_code 查一次,确保删除动作只能落在当前应用上下文内,随后仓储层事务删除主表和等级表。
|
||||||
item, err := s.store.GetHostAgencySalaryPolicy(appctx.Normalize(appCode), id)
|
if _, err := s.store.GetHostAgencySalaryPolicy(appctx.Normalize(appCode), id); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if item.PublishStatus == publishStatusPublished || item.PublishedAtMS > 0 {
|
// 删除后台配置前先清掉 wallet 运行快照,避免 admin 记录消失后结算仍命中过期政策。
|
||||||
// 已发布版本及其周期绑定是历史结算依据;停用或删除都不能破坏已存在月份的可追溯性。
|
if err := s.deleteRuntimePolicy(ctx, appctx.Normalize(appCode), id); err != nil {
|
||||||
return errors.New("已发布的周期政策版本不可删除")
|
return err
|
||||||
}
|
}
|
||||||
return s.store.DeleteHostAgencySalaryPolicy(appctx.Normalize(appCode), id)
|
return s.store.DeleteHostAgencySalaryPolicy(appctx.Normalize(appCode), id)
|
||||||
}
|
}
|
||||||
@ -139,13 +133,6 @@ func (s *Service) Publish(ctx context.Context, appCode string, actorID uint, id
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return policyDTO{}, err
|
return policyDTO{}, err
|
||||||
}
|
}
|
||||||
if item.PublishStatus == publishStatusPublished || item.PublishedAtMS > 0 {
|
|
||||||
// 重复点击发布按幂等成功返回;不再覆盖运行表,也不生成新的周期绑定。
|
|
||||||
return policyFromModel(item), nil
|
|
||||||
}
|
|
||||||
if item.Status != policyStatusActive {
|
|
||||||
return policyDTO{}, errors.New("工资政策启用后才能发布周期版本")
|
|
||||||
}
|
|
||||||
if err := s.ensureNoActiveOverlap(item, id); err != nil {
|
if err := s.ensureNoActiveOverlap(item, id); err != nil {
|
||||||
return policyDTO{}, err
|
return policyDTO{}, err
|
||||||
}
|
}
|
||||||
@ -170,12 +157,13 @@ func (s *Service) ensureNoActiveOverlap(item model.HostAgencySalaryPolicy, exclu
|
|||||||
if item.Status != policyStatusActive {
|
if item.Status != policyStatusActive {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
overlap, err := s.store.HasActiveHostAgencySalaryPolicyForCycle(item.AppCode, item.RegionID, item.CycleKey, excludeID)
|
// 时间段判断采用半开区间:[effective_from_ms, effective_to_ms),effective_to_ms=0 表示长期有效。
|
||||||
|
overlap, err := s.store.HasOverlappingActiveHostAgencySalaryPolicy(item.AppCode, item.RegionID, item.EffectiveFromMS, item.EffectiveToMS, excludeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if overlap {
|
if overlap {
|
||||||
return errors.New("同一区域同一工资周期只能存在一个启用政策")
|
return errors.New("同一区域同一时间只能存在一个启用政策")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -193,22 +181,26 @@ func (s *Service) publishRuntimePolicy(ctx context.Context, item model.HostAgenc
|
|||||||
}
|
}
|
||||||
defer func() { _ = tx.Rollback() }()
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
policyVersion := item.PolicyVersion
|
// wallet-service 运行表以 admin policy_id 为主键;每次发布整包覆盖主表和等级,保证结算读取到一套完整梯度。
|
||||||
if policyVersion == 0 {
|
|
||||||
// 仅兼容迁移前创建但尚未发布的草稿;新记录在 admin 仓储创建事务中固定为 policy ID。
|
|
||||||
policyVersion = uint64(item.ID)
|
|
||||||
}
|
|
||||||
// policy 主表和等级表只允许首次插入;任何重复主键或同周期绑定冲突都会回滚整包发布,禁止覆盖历史版本。
|
|
||||||
if _, err := tx.ExecContext(ctx, `
|
if _, err := tx.ExecContext(ctx, `
|
||||||
INSERT INTO host_agency_salary_policies (
|
INSERT INTO host_agency_salary_policies (
|
||||||
app_code, policy_id, cycle_key, policy_version, name, region_id, status, settlement_mode, settlement_trigger_mode,
|
app_code, policy_id, name, region_id, status, settlement_mode, settlement_trigger_mode,
|
||||||
gift_coin_to_diamond_ratio, residual_diamond_to_usd_rate,
|
gift_coin_to_diamond_ratio, residual_diamond_to_usd_rate,
|
||||||
effective_from_ms, effective_to_ms, created_at_ms, updated_at_ms
|
effective_from_ms, effective_to_ms, created_at_ms, updated_at_ms
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
name = VALUES(name),
|
||||||
|
region_id = VALUES(region_id),
|
||||||
|
status = VALUES(status),
|
||||||
|
settlement_mode = VALUES(settlement_mode),
|
||||||
|
settlement_trigger_mode = VALUES(settlement_trigger_mode),
|
||||||
|
gift_coin_to_diamond_ratio = VALUES(gift_coin_to_diamond_ratio),
|
||||||
|
residual_diamond_to_usd_rate = VALUES(residual_diamond_to_usd_rate),
|
||||||
|
effective_from_ms = VALUES(effective_from_ms),
|
||||||
|
effective_to_ms = VALUES(effective_to_ms),
|
||||||
|
updated_at_ms = VALUES(updated_at_ms)`,
|
||||||
item.AppCode,
|
item.AppCode,
|
||||||
item.ID,
|
item.ID,
|
||||||
item.CycleKey,
|
|
||||||
policyVersion,
|
|
||||||
item.Name,
|
item.Name,
|
||||||
item.RegionID,
|
item.RegionID,
|
||||||
item.Status,
|
item.Status,
|
||||||
@ -223,6 +215,9 @@ func (s *Service) publishRuntimePolicy(ctx context.Context, item model.HostAgenc
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx, `DELETE FROM host_agency_salary_policy_levels WHERE app_code = ? AND policy_id = ?`, item.AppCode, item.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for _, level := range item.Levels {
|
for _, level := range item.Levels {
|
||||||
_, hostSalaryMinor, err := parseFixedDecimal(level.HostSalaryUSD, 2, true, "主播美元工资")
|
_, hostSalaryMinor, err := parseFixedDecimal(level.HostSalaryUSD, 2, true, "主播美元工资")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -253,13 +248,22 @@ func (s *Service) publishRuntimePolicy(ctx context.Context, item model.HostAgenc
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 周期绑定的复合主键是最终防线:同一区域同一周期只能首次绑定一次,后续月份发布不会影响它。
|
return tx.Commit()
|
||||||
if _, err := tx.ExecContext(ctx, `
|
}
|
||||||
INSERT INTO host_salary_policy_cycle_bindings (
|
|
||||||
app_code, region_id, cycle_key, policy_id, policy_version, created_at_ms
|
func (s *Service) deleteRuntimePolicy(ctx context.Context, appCode string, policyID uint) error {
|
||||||
) VALUES (?, ?, ?, ?, ?, ?)`,
|
if s == nil || s.walletDB == nil {
|
||||||
item.AppCode, item.RegionID, item.CycleKey, item.ID, policyVersion, publishedAtMS,
|
return errors.New("wallet mysql is not configured")
|
||||||
); err != nil {
|
}
|
||||||
|
tx, err := s.walletDB.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
if _, err := tx.ExecContext(ctx, `DELETE FROM host_agency_salary_policy_levels WHERE app_code = ? AND policy_id = ?`, appCode, policyID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx, `DELETE FROM host_agency_salary_policies WHERE app_code = ? AND policy_id = ?`, appCode, policyID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
@ -270,14 +274,10 @@ func policyModelFromRequest(appCode string, actorID uint, req policyRequest) (mo
|
|||||||
if name == "" || len([]rune(name)) > 120 {
|
if name == "" || len([]rune(name)) > 120 {
|
||||||
return model.HostAgencySalaryPolicy{}, errors.New("政策名称不正确")
|
return model.HostAgencySalaryPolicy{}, errors.New("政策名称不正确")
|
||||||
}
|
}
|
||||||
// 当前阶段保持单区域政策:一个区域同一工资周期只能命中一条 active 规则。
|
// 当前阶段保持单区域政策:一个区域同一时段只能命中一条 active 规则,后续结算查询也以 region_id 做索引。
|
||||||
if req.RegionID <= 0 {
|
if req.RegionID <= 0 {
|
||||||
return model.HostAgencySalaryPolicy{}, errors.New("请选择适用区域")
|
return model.HostAgencySalaryPolicy{}, errors.New("请选择适用区域")
|
||||||
}
|
}
|
||||||
cycleKey, err := normalizePolicyCycleKey(req.CycleKey, req.EffectiveFromMS)
|
|
||||||
if err != nil {
|
|
||||||
return model.HostAgencySalaryPolicy{}, err
|
|
||||||
}
|
|
||||||
status := normalizeStatus(req.Status)
|
status := normalizeStatus(req.Status)
|
||||||
if status == "" {
|
if status == "" {
|
||||||
// 新建默认停用,避免运营保存草稿时立即影响结算口径。
|
// 新建默认停用,避免运营保存草稿时立即影响结算口径。
|
||||||
@ -329,7 +329,6 @@ func policyModelFromRequest(appCode string, actorID uint, req policyRequest) (mo
|
|||||||
AppCode: appCode,
|
AppCode: appCode,
|
||||||
Name: name,
|
Name: name,
|
||||||
RegionID: req.RegionID,
|
RegionID: req.RegionID,
|
||||||
CycleKey: cycleKey,
|
|
||||||
Status: status,
|
Status: status,
|
||||||
SettlementMode: settlementMode,
|
SettlementMode: settlementMode,
|
||||||
SettlementTriggerMode: settlementTriggerMode,
|
SettlementTriggerMode: settlementTriggerMode,
|
||||||
@ -526,22 +525,6 @@ func nonZeroMS(value int64, fallback int64) int64 {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizePolicyCycleKey(raw string, effectiveFromMS int64) (string, error) {
|
|
||||||
cycleKey := strings.TrimSpace(raw)
|
|
||||||
if cycleKey == "" && effectiveFromMS > 0 {
|
|
||||||
// 兼容尚未升级的后台页面:旧请求仍可从生效开始时间推导 UTC 归属月,新页面应显式提交 cycle_key。
|
|
||||||
cycleKey = time.UnixMilli(effectiveFromMS).UTC().Format("2006-01")
|
|
||||||
}
|
|
||||||
if len(cycleKey) != len("2006-01") {
|
|
||||||
return "", errors.New("请选择工资周期")
|
|
||||||
}
|
|
||||||
parsed, err := time.Parse("2006-01", cycleKey)
|
|
||||||
if err != nil || parsed.UTC().Format("2006-01") != cycleKey {
|
|
||||||
return "", errors.New("工资周期格式必须为 YYYY-MM")
|
|
||||||
}
|
|
||||||
return cycleKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func truncateRunes(value string, limit int) string {
|
func truncateRunes(value string, limit int) string {
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@ -6,7 +6,6 @@ func TestPolicyModelFromRequestNormalizesAndSortsLevels(t *testing.T) {
|
|||||||
item, err := policyModelFromRequest("lalu", 7, policyRequest{
|
item, err := policyModelFromRequest("lalu", 7, policyRequest{
|
||||||
Name: "Lalu 默认政策",
|
Name: "Lalu 默认政策",
|
||||||
RegionID: 101,
|
RegionID: 101,
|
||||||
CycleKey: "2026-06",
|
|
||||||
Status: "active",
|
Status: "active",
|
||||||
SettlementMode: "half-month",
|
SettlementMode: "half-month",
|
||||||
SettlementTriggerMode: "manual",
|
SettlementTriggerMode: "manual",
|
||||||
@ -20,7 +19,7 @@ func TestPolicyModelFromRequestNormalizesAndSortsLevels(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("policy should be valid: %v", err)
|
t.Fatalf("policy should be valid: %v", err)
|
||||||
}
|
}
|
||||||
if item.AppCode != "lalu" || item.CycleKey != "2026-06" || item.CreatedByAdminID != 7 || item.SettlementMode != settlementModeHalfMonth || item.SettlementTriggerMode != settlementTriggerManual {
|
if item.AppCode != "lalu" || item.CreatedByAdminID != 7 || item.SettlementMode != settlementModeHalfMonth || item.SettlementTriggerMode != settlementTriggerManual {
|
||||||
t.Fatalf("policy fields mismatch: %+v", item)
|
t.Fatalf("policy fields mismatch: %+v", item)
|
||||||
}
|
}
|
||||||
// 工资政策不能单独改送礼入主播周期钻石的比例;该比例只由礼物钻石配置页控制。
|
// 工资政策不能单独改送礼入主播周期钻石的比例;该比例只由礼物钻石配置页控制。
|
||||||
@ -39,7 +38,6 @@ func TestPolicyModelFromRequestRejectsNonIncreasingLevels(t *testing.T) {
|
|||||||
_, err := policyModelFromRequest("lalu", 1, policyRequest{
|
_, err := policyModelFromRequest("lalu", 1, policyRequest{
|
||||||
Name: "Invalid",
|
Name: "Invalid",
|
||||||
RegionID: 101,
|
RegionID: 101,
|
||||||
CycleKey: "2026-06",
|
|
||||||
SettlementMode: settlementModeDaily,
|
SettlementMode: settlementModeDaily,
|
||||||
Levels: []levelRequest{
|
Levels: []levelRequest{
|
||||||
{Level: 1, RequiredDiamonds: 700000, HostSalaryUSD: "3", HostCoinReward: 198000, AgencySalaryUSD: "0.8"},
|
{Level: 1, RequiredDiamonds: 700000, HostSalaryUSD: "3", HostCoinReward: 198000, AgencySalaryUSD: "0.8"},
|
||||||
@ -55,7 +53,6 @@ func TestPolicyModelFromRequestDefaultsSettlementTriggerMode(t *testing.T) {
|
|||||||
item, err := policyModelFromRequest("lalu", 1, policyRequest{
|
item, err := policyModelFromRequest("lalu", 1, policyRequest{
|
||||||
Name: "Default trigger",
|
Name: "Default trigger",
|
||||||
RegionID: 101,
|
RegionID: 101,
|
||||||
CycleKey: "2026-06",
|
|
||||||
SettlementMode: settlementModeDaily,
|
SettlementMode: settlementModeDaily,
|
||||||
Levels: []levelRequest{
|
Levels: []levelRequest{
|
||||||
{Level: 1, RequiredDiamonds: 350000, HostSalaryUSD: "1.5", HostCoinReward: 90000, AgencySalaryUSD: "0.5"},
|
{Level: 1, RequiredDiamonds: 350000, HostSalaryUSD: "1.5", HostCoinReward: 90000, AgencySalaryUSD: "0.5"},
|
||||||
@ -73,7 +70,6 @@ func TestPolicyModelFromRequestRejectsInvalidSettlementMode(t *testing.T) {
|
|||||||
_, err := policyModelFromRequest("lalu", 1, policyRequest{
|
_, err := policyModelFromRequest("lalu", 1, policyRequest{
|
||||||
Name: "Invalid",
|
Name: "Invalid",
|
||||||
RegionID: 101,
|
RegionID: 101,
|
||||||
CycleKey: "2026-06",
|
|
||||||
SettlementMode: "monthly",
|
SettlementMode: "monthly",
|
||||||
Levels: []levelRequest{
|
Levels: []levelRequest{
|
||||||
{Level: 1, RequiredDiamonds: 350000, HostSalaryUSD: "1.5", HostCoinReward: 90000, AgencySalaryUSD: "0.5"},
|
{Level: 1, RequiredDiamonds: 350000, HostSalaryUSD: "1.5", HostCoinReward: 90000, AgencySalaryUSD: "0.5"},
|
||||||
@ -88,7 +84,6 @@ func TestPolicyModelFromRequestRejectsInvalidSettlementTriggerMode(t *testing.T)
|
|||||||
_, err := policyModelFromRequest("lalu", 1, policyRequest{
|
_, err := policyModelFromRequest("lalu", 1, policyRequest{
|
||||||
Name: "Invalid",
|
Name: "Invalid",
|
||||||
RegionID: 101,
|
RegionID: 101,
|
||||||
CycleKey: "2026-06",
|
|
||||||
SettlementMode: settlementModeDaily,
|
SettlementMode: settlementModeDaily,
|
||||||
SettlementTriggerMode: "cron",
|
SettlementTriggerMode: "cron",
|
||||||
Levels: []levelRequest{
|
Levels: []levelRequest{
|
||||||
@ -99,34 +94,3 @@ func TestPolicyModelFromRequestRejectsInvalidSettlementTriggerMode(t *testing.T)
|
|||||||
t.Fatal("expected invalid settlement trigger mode to fail")
|
t.Fatal("expected invalid settlement trigger mode to fail")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPolicyModelFromRequestDerivesUTCCycleForLegacyRequest(t *testing.T) {
|
|
||||||
item, err := policyModelFromRequest("lalu", 1, policyRequest{
|
|
||||||
Name: "Legacy cycle",
|
|
||||||
RegionID: 101,
|
|
||||||
EffectiveFromMS: 1782864000000, // 2026-07-01T00:00:00Z
|
|
||||||
Levels: []levelRequest{
|
|
||||||
{Level: 1, RequiredDiamonds: 350000, HostSalaryUSD: "1.5", HostCoinReward: 90000, AgencySalaryUSD: "0.5"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("legacy request should derive cycle: %v", err)
|
|
||||||
}
|
|
||||||
if item.CycleKey != "2026-07" {
|
|
||||||
t.Fatalf("derived cycle mismatch: %+v", item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPolicyModelFromRequestRejectsInvalidCycle(t *testing.T) {
|
|
||||||
_, err := policyModelFromRequest("lalu", 1, policyRequest{
|
|
||||||
Name: "Invalid cycle",
|
|
||||||
RegionID: 101,
|
|
||||||
CycleKey: "2026-13",
|
|
||||||
Levels: []levelRequest{
|
|
||||||
{Level: 1, RequiredDiamonds: 350000, HostSalaryUSD: "1.5", HostCoinReward: 90000, AgencySalaryUSD: "0.5"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected invalid cycle to fail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -61,7 +61,6 @@ type pendingDTO struct {
|
|||||||
RegionID int64 `json:"region_id"`
|
RegionID int64 `json:"region_id"`
|
||||||
RegionName string `json:"region_name"`
|
RegionName string `json:"region_name"`
|
||||||
PolicyID uint64 `json:"policy_id"`
|
PolicyID uint64 `json:"policy_id"`
|
||||||
PolicyVersion uint64 `json:"policy_version"`
|
|
||||||
PolicyName string `json:"policy_name"`
|
PolicyName string `json:"policy_name"`
|
||||||
SettlementType string `json:"settlement_type"`
|
SettlementType string `json:"settlement_type"`
|
||||||
TriggerMode string `json:"trigger_mode"`
|
TriggerMode string `json:"trigger_mode"`
|
||||||
@ -108,7 +107,6 @@ type settlementDTO struct {
|
|||||||
AgencyOwner userDTO `json:"agency_owner"`
|
AgencyOwner userDTO `json:"agency_owner"`
|
||||||
CycleKey string `json:"cycle_key"`
|
CycleKey string `json:"cycle_key"`
|
||||||
PolicyID uint64 `json:"policy_id"`
|
PolicyID uint64 `json:"policy_id"`
|
||||||
PolicyVersion uint64 `json:"policy_version"`
|
|
||||||
LevelNo int32 `json:"level_no"`
|
LevelNo int32 `json:"level_no"`
|
||||||
TotalDiamonds int64 `json:"total_diamonds"`
|
TotalDiamonds int64 `json:"total_diamonds"`
|
||||||
HostSalaryUSDMinorDelta int64 `json:"host_salary_usd_minor_delta"`
|
HostSalaryUSDMinorDelta int64 `json:"host_salary_usd_minor_delta"`
|
||||||
|
|||||||
@ -54,8 +54,6 @@ type walletCronClient interface {
|
|||||||
|
|
||||||
type hostPolicy struct {
|
type hostPolicy struct {
|
||||||
PolicyID uint64
|
PolicyID uint64
|
||||||
CycleKey string
|
|
||||||
PolicyVersion uint64
|
|
||||||
Name string
|
Name string
|
||||||
RegionID int64
|
RegionID int64
|
||||||
SettlementMode string
|
SettlementMode string
|
||||||
@ -110,7 +108,7 @@ func (s *Service) ListPending(ctx context.Context, appCode string, req pendingQu
|
|||||||
}
|
}
|
||||||
req = normalizePendingQuery(req)
|
req = normalizePendingQuery(req)
|
||||||
// 待结算列表只做只读重算,不写 progress、不写钱包;真正入账必须走 Settle 调 wallet-service。
|
// 待结算列表只做只读重算,不写 progress、不写钱包;真正入账必须走 Settle 调 wallet-service。
|
||||||
candidates, err := s.collectPendingCandidates(ctx, strings.TrimSpace(appCode), req)
|
candidates, err := s.collectPendingCandidates(ctx, strings.TrimSpace(appCode), req, time.Now().UTC().UnixMilli())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@ -160,7 +158,7 @@ func (s *Service) Settle(ctx context.Context, appCode string, operatorAdminID in
|
|||||||
RegionID: req.RegionID,
|
RegionID: req.RegionID,
|
||||||
CountryID: req.CountryID,
|
CountryID: req.CountryID,
|
||||||
CountryCode: req.CountryCode,
|
CountryCode: req.CountryCode,
|
||||||
})
|
}, time.Now().UTC().UnixMilli())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return settleResultDTO{}, err
|
return settleResultDTO{}, err
|
||||||
}
|
}
|
||||||
@ -231,7 +229,7 @@ func (s *Service) callWalletSettlement(ctx context.Context, appCode string, req
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) collectPendingCandidates(ctx context.Context, appCode string, req pendingQuery) ([]hostCandidate, error) {
|
func (s *Service) collectPendingCandidates(ctx context.Context, appCode string, req pendingQuery, nowMS int64) ([]hostCandidate, error) {
|
||||||
req = normalizePendingQuery(req)
|
req = normalizePendingQuery(req)
|
||||||
regions, err := s.regionFilter(ctx, appCode, req.RegionID, req.CountryID, req.CountryCode)
|
regions, err := s.regionFilter(ctx, appCode, req.RegionID, req.CountryID, req.CountryCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -257,6 +255,7 @@ func (s *Service) collectPendingCandidates(ctx context.Context, appCode string,
|
|||||||
userClause = "AND a.user_id = ?"
|
userClause = "AND a.user_id = ?"
|
||||||
args = append(args, req.UserID)
|
args = append(args, req.UserID)
|
||||||
}
|
}
|
||||||
|
args = append(args, nowMS, nowMS)
|
||||||
triggerClause := ""
|
triggerClause := ""
|
||||||
if req.TriggerMode != "" {
|
if req.TriggerMode != "" {
|
||||||
triggerClause = "AND p.settlement_trigger_mode = ?"
|
triggerClause = "AND p.settlement_trigger_mode = ?"
|
||||||
@ -285,17 +284,12 @@ func (s *Service) collectPendingCandidates(ctx context.Context, appCode string,
|
|||||||
%s
|
%s
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM host_salary_policy_cycle_bindings binding
|
FROM host_agency_salary_policies p
|
||||||
JOIN host_agency_salary_policies p
|
WHERE p.app_code = a.app_code
|
||||||
ON p.app_code = binding.app_code
|
AND p.region_id = a.region_id
|
||||||
AND p.policy_id = binding.policy_id
|
|
||||||
AND p.region_id = binding.region_id
|
|
||||||
AND p.cycle_key = binding.cycle_key
|
|
||||||
AND p.policy_version = binding.policy_version
|
|
||||||
WHERE binding.app_code = a.app_code
|
|
||||||
AND binding.region_id = a.region_id
|
|
||||||
AND binding.cycle_key = a.cycle_key
|
|
||||||
AND p.status = 'active'
|
AND p.status = 'active'
|
||||||
|
AND p.effective_from_ms <= ?
|
||||||
|
AND (p.effective_to_ms = 0 OR p.effective_to_ms > ?)
|
||||||
%s
|
%s
|
||||||
%s
|
%s
|
||||||
)
|
)
|
||||||
@ -325,10 +319,10 @@ func (s *Service) collectPendingCandidates(ctx context.Context, appCode string,
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cacheKey := fmt.Sprintf("%d:%s:%s:%s", item.RegionID, item.CycleKey, req.SettlementType, req.TriggerMode)
|
cacheKey := fmt.Sprintf("%d:%s:%s", item.RegionID, req.SettlementType, req.TriggerMode)
|
||||||
policy, ok := policyCache[cacheKey]
|
policy, ok := policyCache[cacheKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
policy, ok, err = s.resolvePolicy(ctx, appCode, item.RegionID, item.CycleKey, req.SettlementType, req.TriggerMode)
|
policy, ok, err = s.resolvePolicy(ctx, appCode, item.RegionID, req.SettlementType, req.TriggerMode, nowMS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -386,7 +380,7 @@ func (s *Service) List(ctx context.Context, appCode string, req query) ([]settle
|
|||||||
}
|
}
|
||||||
rows, err := s.walletDB.QueryContext(ctx, `
|
rows, err := s.walletDB.QueryContext(ctx, `
|
||||||
SELECT settlement_id, command_id, transaction_id, settlement_type, trigger_mode, settlement_role, user_id, agency_owner_user_id,
|
SELECT settlement_id, command_id, transaction_id, settlement_type, trigger_mode, settlement_role, user_id, agency_owner_user_id,
|
||||||
cycle_key, policy_id, policy_version, level_no, total_diamonds, host_salary_usd_minor_delta,
|
cycle_key, policy_id, level_no, total_diamonds, host_salary_usd_minor_delta,
|
||||||
host_coin_reward_delta, agency_salary_usd_minor_delta, residual_usd_minor_delta,
|
host_coin_reward_delta, agency_salary_usd_minor_delta, residual_usd_minor_delta,
|
||||||
status, reason, created_at_ms
|
status, reason, created_at_ms
|
||||||
FROM host_salary_settlement_records
|
FROM host_salary_settlement_records
|
||||||
@ -417,7 +411,6 @@ func (s *Service) List(ctx context.Context, appCode string, req query) ([]settle
|
|||||||
&agencyOwnerUserID,
|
&agencyOwnerUserID,
|
||||||
&item.CycleKey,
|
&item.CycleKey,
|
||||||
&item.PolicyID,
|
&item.PolicyID,
|
||||||
&item.PolicyVersion,
|
|
||||||
&item.LevelNo,
|
&item.LevelNo,
|
||||||
&item.TotalDiamonds,
|
&item.TotalDiamonds,
|
||||||
&item.HostSalaryUSDMinorDelta,
|
&item.HostSalaryUSDMinorDelta,
|
||||||
@ -522,31 +515,25 @@ func settlementWhere(appCode string, req query) (string, []any) {
|
|||||||
return "WHERE " + strings.Join(conditions, " AND "), args
|
return "WHERE " + strings.Join(conditions, " AND "), args
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) resolvePolicy(ctx context.Context, appCode string, regionID int64, cycleKey string, settlementType string, triggerMode string) (hostPolicy, bool, error) {
|
func (s *Service) resolvePolicy(ctx context.Context, appCode string, regionID int64, settlementType string, triggerMode string, nowMS int64) (hostPolicy, bool, error) {
|
||||||
conditions := []string{"binding.app_code = ?", "binding.region_id = ?", "binding.cycle_key = ?", "p.status = 'active'"}
|
conditions := []string{"app_code = ?", "region_id = ?", "status = 'active'", "effective_from_ms <= ?", "(effective_to_ms = 0 OR effective_to_ms > ?)"}
|
||||||
args := []any{appCode, regionID, cycleKey}
|
args := []any{appCode, regionID, nowMS, nowMS}
|
||||||
if triggerMode != "" {
|
if triggerMode != "" {
|
||||||
conditions = append(conditions, "p.settlement_trigger_mode = ?")
|
conditions = append(conditions, "settlement_trigger_mode = ?")
|
||||||
args = append(args, triggerMode)
|
args = append(args, triggerMode)
|
||||||
}
|
}
|
||||||
if settlementType != settlementTypeMonthEnd {
|
if settlementType != settlementTypeMonthEnd {
|
||||||
conditions = append(conditions, "p.settlement_mode = ?")
|
conditions = append(conditions, "settlement_mode = ?")
|
||||||
args = append(args, settlementType)
|
args = append(args, settlementType)
|
||||||
}
|
}
|
||||||
row := s.walletDB.QueryRowContext(ctx, `
|
row := s.walletDB.QueryRowContext(ctx, `
|
||||||
SELECT p.policy_id, p.cycle_key, p.policy_version, p.name, p.region_id,
|
SELECT policy_id, name, region_id, settlement_mode, settlement_trigger_mode, CAST(residual_diamond_to_usd_rate AS CHAR)
|
||||||
p.settlement_mode, p.settlement_trigger_mode, CAST(p.residual_diamond_to_usd_rate AS CHAR)
|
FROM host_agency_salary_policies
|
||||||
FROM host_salary_policy_cycle_bindings binding
|
|
||||||
JOIN host_agency_salary_policies p
|
|
||||||
ON p.app_code = binding.app_code
|
|
||||||
AND p.policy_id = binding.policy_id
|
|
||||||
AND p.region_id = binding.region_id
|
|
||||||
AND p.cycle_key = binding.cycle_key
|
|
||||||
AND p.policy_version = binding.policy_version
|
|
||||||
WHERE `+strings.Join(conditions, " AND ")+`
|
WHERE `+strings.Join(conditions, " AND ")+`
|
||||||
|
ORDER BY effective_from_ms DESC, policy_id DESC
|
||||||
LIMIT 1`, args...)
|
LIMIT 1`, args...)
|
||||||
var policy hostPolicy
|
var policy hostPolicy
|
||||||
if err := row.Scan(&policy.PolicyID, &policy.CycleKey, &policy.PolicyVersion, &policy.Name, &policy.RegionID, &policy.SettlementMode, &policy.TriggerMode, &policy.ResidualDiamondToUSDRate); err != nil {
|
if err := row.Scan(&policy.PolicyID, &policy.Name, &policy.RegionID, &policy.SettlementMode, &policy.TriggerMode, &policy.ResidualDiamondToUSDRate); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return hostPolicy{}, false, nil
|
return hostPolicy{}, false, nil
|
||||||
}
|
}
|
||||||
@ -880,7 +867,6 @@ func pendingDTOFromCandidate(item hostCandidate, host userProfile, agency userPr
|
|||||||
RegionID: item.RegionID,
|
RegionID: item.RegionID,
|
||||||
RegionName: item.RegionName,
|
RegionName: item.RegionName,
|
||||||
PolicyID: item.Policy.PolicyID,
|
PolicyID: item.Policy.PolicyID,
|
||||||
PolicyVersion: item.Policy.PolicyVersion,
|
|
||||||
PolicyName: item.Policy.Name,
|
PolicyName: item.Policy.Name,
|
||||||
SettlementType: item.SettlementType,
|
SettlementType: item.SettlementType,
|
||||||
TriggerMode: item.Policy.TriggerMode,
|
TriggerMode: item.Policy.TriggerMode,
|
||||||
|
|||||||
@ -95,10 +95,6 @@ type configRequest struct {
|
|||||||
Stages []stageDTO `json:"stages"`
|
Stages []stageDTO `json:"stages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type configRollbackRequest struct {
|
|
||||||
TargetRuleVersion int64 `json:"target_rule_version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// configUpsertRequest 只服务 HTTP 写入口。滚动发布期间旧前端仍可能发送 day/72h 字段,
|
// configUpsertRequest 只服务 HTTP 写入口。滚动发布期间旧前端仍可能发送 day/72h 字段,
|
||||||
// 因此输入 DTO 明确接收两代名字;configRequest 继续只暴露 canonical JSON,保证 GET 和写入响应不会双写字段。
|
// 因此输入 DTO 明确接收两代名字;configRequest 继续只暴露 canonical JSON,保证 GET 和写入响应不会双写字段。
|
||||||
type configUpsertRequest struct {
|
type configUpsertRequest struct {
|
||||||
@ -301,67 +297,6 @@ func (h *Handler) ListLuckyGiftConfigs(c *gin.Context) {
|
|||||||
response.OK(c, items)
|
response.OK(c, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) ListLuckyGiftConfigVersions(c *gin.Context) {
|
|
||||||
appCode := strings.TrimSpace(c.Query("app_code"))
|
|
||||||
poolID := strings.TrimSpace(c.Query("pool_id"))
|
|
||||||
if appCode == "" || poolID == "" {
|
|
||||||
response.BadRequest(c, "app_code and pool_id are required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx, cancel := h.luckyGiftContext(c)
|
|
||||||
defer cancel()
|
|
||||||
resp, err := h.luckyGift.ListLuckyGiftConfigVersions(ctx, &luckygiftv1.ListLuckyGiftConfigVersionsRequest{
|
|
||||||
Meta: h.metaForApp(c, appCode),
|
|
||||||
PoolId: poolID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
h.writeConfigRollbackError(c, err, "获取幸运礼物历史版本失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
items := make([]configDTO, 0, len(resp.GetConfigs()))
|
|
||||||
for _, config := range resp.GetConfigs() {
|
|
||||||
items = append(items, configFromProto(config))
|
|
||||||
}
|
|
||||||
response.OK(c, items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) RollbackLuckyGiftConfig(c *gin.Context) {
|
|
||||||
appCode := strings.TrimSpace(c.Query("app_code"))
|
|
||||||
poolID := strings.TrimSpace(c.Query("pool_id"))
|
|
||||||
if appCode == "" || poolID == "" {
|
|
||||||
response.BadRequest(c, "app_code and pool_id are required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var input configRollbackRequest
|
|
||||||
decoder := json.NewDecoder(c.Request.Body)
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
if err := decoder.Decode(&input); err != nil || input.TargetRuleVersion <= 0 {
|
|
||||||
response.BadRequest(c, "回滚目标版本不正确")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx, cancel := h.luckyGiftContext(c)
|
|
||||||
defer cancel()
|
|
||||||
resp, err := h.luckyGift.RollbackLuckyGiftConfig(ctx, &luckygiftv1.RollbackLuckyGiftConfigRequest{
|
|
||||||
Meta: h.metaForApp(c, appCode),
|
|
||||||
PoolId: poolID,
|
|
||||||
TargetRuleVersion: input.TargetRuleVersion,
|
|
||||||
OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
h.writeConfigRollbackError(c, err, "回滚幸运礼物版本失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
item := configFromProto(resp.GetConfig())
|
|
||||||
shared.OperationLogWithResourceID(
|
|
||||||
c, h.audit, "rollback-lucky-gift-config", "lucky_gift_rule_versions", fmt.Sprintf("%s/%s/v%d", appCode, poolID, item.RuleVersion), "success",
|
|
||||||
fmt.Sprintf("source_rule_version=v%d new_rule_version=v%d", resp.GetSourceRuleVersion(), item.RuleVersion),
|
|
||||||
)
|
|
||||||
response.OK(c, gin.H{
|
|
||||||
"source_rule_version": resp.GetSourceRuleVersion(),
|
|
||||||
"config": item,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) ListLuckyGiftDraws(c *gin.Context) {
|
func (h *Handler) ListLuckyGiftDraws(c *gin.Context) {
|
||||||
if strings.TrimSpace(c.Query("app_code")) == "" {
|
if strings.TrimSpace(c.Query("app_code")) == "" {
|
||||||
response.BadRequest(c, "app_code is required")
|
response.BadRequest(c, "app_code is required")
|
||||||
@ -509,20 +444,6 @@ func (h *Handler) writePoolAdjustmentError(c *gin.Context, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) writeConfigRollbackError(c *gin.Context, err error, fallback string) {
|
|
||||||
status := grpcstatus.Convert(err)
|
|
||||||
switch status.Code() {
|
|
||||||
case codes.InvalidArgument:
|
|
||||||
response.BadRequest(c, status.Message())
|
|
||||||
case codes.NotFound:
|
|
||||||
response.NotFound(c, status.Message())
|
|
||||||
case codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
|
|
||||||
response.Conflict(c, status.Message())
|
|
||||||
default:
|
|
||||||
response.ServerError(c, fallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) meta(c *gin.Context) *luckygiftv1.RequestMeta {
|
func (h *Handler) meta(c *gin.Context) *luckygiftv1.RequestMeta {
|
||||||
return h.metaForApp(c, "")
|
return h.metaForApp(c, "")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,6 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
|||||||
group.GET("/lucky-gifts/config", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.GetLuckyGiftConfig)
|
group.GET("/lucky-gifts/config", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.GetLuckyGiftConfig)
|
||||||
group.PUT("/lucky-gifts/config", middleware.RequirePermission("lucky-gift:update"), h.luckyGift.UpsertLuckyGiftConfig)
|
group.PUT("/lucky-gifts/config", middleware.RequirePermission("lucky-gift:update"), h.luckyGift.UpsertLuckyGiftConfig)
|
||||||
group.GET("/lucky-gifts/configs", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftConfigs)
|
group.GET("/lucky-gifts/configs", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftConfigs)
|
||||||
group.GET("/lucky-gifts/config/versions", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftConfigVersions)
|
|
||||||
group.POST("/lucky-gifts/config/rollback", middleware.RequirePermission("lucky-gift:update"), h.luckyGift.RollbackLuckyGiftConfig)
|
|
||||||
group.GET("/lucky-gifts/draws", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftDraws)
|
group.GET("/lucky-gifts/draws", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftDraws)
|
||||||
group.GET("/lucky-gifts/summary", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.GetLuckyGiftDrawSummary)
|
group.GET("/lucky-gifts/summary", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.GetLuckyGiftDrawSummary)
|
||||||
group.GET("/lucky-gifts/user-profiles", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftUserProfiles)
|
group.GET("/lucky-gifts/user-profiles", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftUserProfiles)
|
||||||
|
|||||||
@ -141,10 +141,6 @@ func (h *Handler) CreateResource(c *gin.Context) {
|
|||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := validateRoomDecorationResourceMaterial(req); err != nil {
|
|
||||||
response.BadRequest(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := validateResourceMetadata(req); err != nil {
|
if err := validateResourceMetadata(req); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
@ -261,10 +257,6 @@ func (h *Handler) UpdateResource(c *gin.Context) {
|
|||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := validateRoomDecorationResourceMaterial(req); err != nil {
|
|
||||||
response.BadRequest(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := validateResourceMetadata(req); err != nil {
|
if err := validateResourceMetadata(req); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
@ -3,8 +3,6 @@ package resource
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -24,19 +22,10 @@ const (
|
|||||||
resourceTypeAvatarFrame = "avatar_frame"
|
resourceTypeAvatarFrame = "avatar_frame"
|
||||||
resourceTypeBadge = "badge"
|
resourceTypeBadge = "badge"
|
||||||
resourceTypeEmojiPack = "emoji_pack"
|
resourceTypeEmojiPack = "emoji_pack"
|
||||||
resourceTypeMicAnimation = "mic_seat_animation"
|
|
||||||
resourceTypeProfileCard = "profile_card"
|
resourceTypeProfileCard = "profile_card"
|
||||||
resourceTypeRoomBorder = "room_border"
|
|
||||||
resourceTypeRoomName = "room_name_style"
|
|
||||||
resourceTypeVIPTrialCard = "vip_trial_card"
|
resourceTypeVIPTrialCard = "vip_trial_card"
|
||||||
)
|
)
|
||||||
|
|
||||||
var roomNameStyleColorPattern = regexp.MustCompile(`^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$`)
|
|
||||||
|
|
||||||
var vipVisualResourceFormats = map[string]struct{}{
|
|
||||||
"gif": {}, "jpeg": {}, "mp4": {}, "pag": {}, "png": {}, "svga": {}, "webp": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
resourceAnimationFormatMP4 = "mp4"
|
resourceAnimationFormatMP4 = "mp4"
|
||||||
)
|
)
|
||||||
@ -137,13 +126,9 @@ type badgeMetadataPayload struct {
|
|||||||
type resourceMetadataPayload struct {
|
type resourceMetadataPayload struct {
|
||||||
AnimationFormat string `json:"animation_format,omitempty"`
|
AnimationFormat string `json:"animation_format,omitempty"`
|
||||||
AvatarFrameKind string `json:"avatar_frame_kind,omitempty"`
|
AvatarFrameKind string `json:"avatar_frame_kind,omitempty"`
|
||||||
Format string `json:"format,omitempty"`
|
|
||||||
LevelTrack string `json:"level_track,omitempty"`
|
LevelTrack string `json:"level_track,omitempty"`
|
||||||
MP4AlphaLayout *mp4AlphaLayoutMetadataPayload `json:"mp4_alpha_layout,omitempty"`
|
MP4AlphaLayout *mp4AlphaLayoutMetadataPayload `json:"mp4_alpha_layout,omitempty"`
|
||||||
ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"`
|
ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"`
|
||||||
TextColors []string `json:"text_colors,omitempty"`
|
|
||||||
Stops []float64 `json:"stops,omitempty"`
|
|
||||||
AngleDegrees *float64 `json:"angle_degrees,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type mp4AlphaLayoutMetadataPayload struct {
|
type mp4AlphaLayoutMetadataPayload struct {
|
||||||
@ -659,25 +644,8 @@ func validateResourceMetadata(req resourceRequest) error {
|
|||||||
return fmt.Errorf("资源元数据格式不正确")
|
return fmt.Errorf("资源元数据格式不正确")
|
||||||
}
|
}
|
||||||
if payload == nil {
|
if payload == nil {
|
||||||
if resourceType == resourceTypeRoomName {
|
|
||||||
return fmt.Errorf("请配置房间名称渐变样式")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if resourceType == resourceTypeRoomName {
|
|
||||||
if err := validateRoomNameStyleMetadata(payload); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if resourceRequiresExplicitVisualFormat(resourceType) {
|
|
||||||
format := strings.ToLower(strings.TrimSpace(payload.Format))
|
|
||||||
if _, allowed := vipVisualResourceFormats[format]; !allowed {
|
|
||||||
return fmt.Errorf("视觉资源素材格式不支持")
|
|
||||||
}
|
|
||||||
if animationFormat := strings.ToLower(strings.TrimSpace(payload.AnimationFormat)); animationFormat != "" && animationFormat != format {
|
|
||||||
return fmt.Errorf("视觉资源素材格式与动效格式不一致")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rawFormat, ok := fields["animation_format"]; ok && len(rawFormat) > 0 {
|
if rawFormat, ok := fields["animation_format"]; ok && len(rawFormat) > 0 {
|
||||||
animationFormat := strings.ToLower(strings.TrimSpace(payload.AnimationFormat))
|
animationFormat := strings.ToLower(strings.TrimSpace(payload.AnimationFormat))
|
||||||
if animationFormat != "" && animationFormat != resourceAnimationFormatMP4 {
|
if animationFormat != "" && animationFormat != resourceAnimationFormatMP4 {
|
||||||
@ -715,39 +683,6 @@ func resourceGrantStrategy(resourceType string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRoomDecorationResourceMaterial(req resourceRequest) error {
|
|
||||||
resourceType := normalizeResourceType(req.ResourceType)
|
|
||||||
if !resourceRequiresExplicitVisualFormat(resourceType) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// 麦位动效和房间边框都是实际视觉资源,不能创建只有编码的占位行;
|
|
||||||
// 样式型 room_name_style 则由 metadata 独立渲染。
|
|
||||||
if strings.TrimSpace(req.AssetURL) == "" && strings.TrimSpace(req.AnimationURL) == "" {
|
|
||||||
return fmt.Errorf("请上传视觉资源素材")
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(req.PreviewURL) == "" {
|
|
||||||
return fmt.Errorf("请上传视觉资源预览图")
|
|
||||||
}
|
|
||||||
payload, _, err := parseResourceMetadataPayload(req.MetadataJSON)
|
|
||||||
if err != nil || payload == nil {
|
|
||||||
return fmt.Errorf("请配置视觉资源素材格式")
|
|
||||||
}
|
|
||||||
format := strings.ToLower(strings.TrimSpace(payload.Format))
|
|
||||||
if _, allowed := vipVisualResourceFormats[format]; !allowed {
|
|
||||||
return fmt.Errorf("视觉资源素材格式不支持")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resourceRequiresExplicitVisualFormat(resourceType string) bool {
|
|
||||||
switch normalizeResourceType(resourceType) {
|
|
||||||
case resourceTypeMicAnimation, resourceTypeRoomBorder:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func resourceMetadataJSON(resourceType string, badgeForm string, metadataJSON string, badgeKind string, levelTrack string, avatarFrameKind string) string {
|
func resourceMetadataJSON(resourceType string, badgeForm string, metadataJSON string, badgeKind string, levelTrack string, avatarFrameKind string) string {
|
||||||
if resourceType == resourceTypeBadge {
|
if resourceType == resourceTypeBadge {
|
||||||
return badgeMetadataJSON(badgeForm, badgeKind, levelTrack)
|
return badgeMetadataJSON(badgeForm, badgeKind, levelTrack)
|
||||||
@ -772,14 +707,6 @@ func resourceMetadataJSON(resourceType string, badgeForm string, metadataJSON st
|
|||||||
payload.LevelTrack = normalizeLevelTrack(levelTrack)
|
payload.LevelTrack = normalizeLevelTrack(levelTrack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if resourceType == resourceTypeRoomName || resourceRequiresExplicitVisualFormat(resourceType) {
|
|
||||||
payload.Format = strings.ToLower(strings.TrimSpace(rawPayload.Format))
|
|
||||||
}
|
|
||||||
if resourceType == resourceTypeRoomName {
|
|
||||||
payload.TextColors = append([]string(nil), rawPayload.TextColors...)
|
|
||||||
payload.Stops = append([]float64(nil), rawPayload.Stops...)
|
|
||||||
payload.AngleDegrees = rawPayload.AngleDegrees
|
|
||||||
}
|
|
||||||
if resourceAllowsAnimationMetadata(resourceType) {
|
if resourceAllowsAnimationMetadata(resourceType) {
|
||||||
if layout, err := sanitizeMP4AlphaLayoutMetadata(rawPayload.MP4AlphaLayout); err == nil && layout != nil && layout.Confirmed {
|
if layout, err := sanitizeMP4AlphaLayoutMetadata(rawPayload.MP4AlphaLayout); err == nil && layout != nil && layout.Confirmed {
|
||||||
payload.AnimationFormat = resourceAnimationFormatMP4
|
payload.AnimationFormat = resourceAnimationFormatMP4
|
||||||
@ -850,7 +777,7 @@ func parseResourceMetadataPayload(metadataJSON string) (*resourceMetadataPayload
|
|||||||
}
|
}
|
||||||
|
|
||||||
func marshalResourceMetadataPayload(payload resourceMetadataPayload) string {
|
func marshalResourceMetadataPayload(payload resourceMetadataPayload) string {
|
||||||
if payload.AnimationFormat == "" && payload.AvatarFrameKind == "" && payload.Format == "" && payload.LevelTrack == "" && payload.MP4AlphaLayout == nil && payload.ProfileCardLayout == nil && len(payload.TextColors) == 0 && len(payload.Stops) == 0 && payload.AngleDegrees == nil {
|
if payload.AnimationFormat == "" && payload.AvatarFrameKind == "" && payload.LevelTrack == "" && payload.MP4AlphaLayout == nil && payload.ProfileCardLayout == nil {
|
||||||
return "{}"
|
return "{}"
|
||||||
}
|
}
|
||||||
body, err := json.Marshal(payload)
|
body, err := json.Marshal(payload)
|
||||||
@ -948,40 +875,13 @@ func normalizeMP4AlphaLayout(value string) string {
|
|||||||
|
|
||||||
func resourceAllowsAnimationMetadata(resourceType string) bool {
|
func resourceAllowsAnimationMetadata(resourceType string) bool {
|
||||||
switch resourceType {
|
switch resourceType {
|
||||||
case "gift", "vehicle", "avatar_frame", "chat_bubble", "floating_screen", resourceTypeProfileCard, resourceTypeRoomBorder:
|
case "gift", "vehicle", "avatar_frame", "chat_bubble", "floating_screen", resourceTypeProfileCard:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return strings.HasPrefix(resourceType, "mic_seat_")
|
return strings.HasPrefix(resourceType, "mic_seat_")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRoomNameStyleMetadata(payload *resourceMetadataPayload) error {
|
|
||||||
if payload == nil || len(payload.TextColors) == 0 || len(payload.TextColors) > 8 {
|
|
||||||
return fmt.Errorf("房名渐变颜色必须配置 1 至 8 个")
|
|
||||||
}
|
|
||||||
for index, color := range payload.TextColors {
|
|
||||||
color = strings.TrimSpace(color)
|
|
||||||
if !roomNameStyleColorPattern.MatchString(color) {
|
|
||||||
return fmt.Errorf("房名渐变颜色必须使用 #RRGGBB 或 #RRGGBBAA")
|
|
||||||
}
|
|
||||||
payload.TextColors[index] = color
|
|
||||||
}
|
|
||||||
if len(payload.Stops) != 0 && len(payload.Stops) != len(payload.TextColors) {
|
|
||||||
return fmt.Errorf("房名渐变位置数量必须与颜色一致")
|
|
||||||
}
|
|
||||||
previous := -1.0
|
|
||||||
for _, stop := range payload.Stops {
|
|
||||||
if math.IsNaN(stop) || math.IsInf(stop, 0) || stop < 0 || stop > 1 || stop < previous {
|
|
||||||
return fmt.Errorf("房名渐变位置必须按 0 至 1 递增")
|
|
||||||
}
|
|
||||||
previous = stop
|
|
||||||
}
|
|
||||||
if payload.AngleDegrees != nil && (math.IsNaN(*payload.AngleDegrees) || math.IsInf(*payload.AngleDegrees, 0)) {
|
|
||||||
return fmt.Errorf("房名渐变角度不正确")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeProfileCardLayoutMetadata(layout *profileCardLayoutMetadataPayload) *profileCardLayoutMetadataPayload {
|
func sanitizeProfileCardLayoutMetadata(layout *profileCardLayoutMetadataPayload) *profileCardLayoutMetadataPayload {
|
||||||
if layout == nil {
|
if layout == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -1,132 +0,0 @@
|
|||||||
package dingtalkrobot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
WebhookURL string
|
|
||||||
Secret string
|
|
||||||
AtMobiles []string
|
|
||||||
AtAll bool
|
|
||||||
RequestTimeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client 位于 admin 自己的 platform 边界,避免独立 module 反向依赖根模块 hyapp/pkg。
|
|
||||||
type Client struct {
|
|
||||||
webhookURL string
|
|
||||||
secret string
|
|
||||||
atMobiles []string
|
|
||||||
atAll bool
|
|
||||||
httpClient *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(config Config) (*Client, error) {
|
|
||||||
webhookURL := strings.TrimSpace(config.WebhookURL)
|
|
||||||
if webhookURL == "" {
|
|
||||||
return nil, errors.New("dingtalk webhook url is required")
|
|
||||||
}
|
|
||||||
parsed, err := url.ParseRequestURI(webhookURL)
|
|
||||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
|
|
||||||
return nil, errors.New("dingtalk webhook url must be an absolute http url")
|
|
||||||
}
|
|
||||||
timeout := config.RequestTimeout
|
|
||||||
if timeout <= 0 {
|
|
||||||
timeout = 3 * time.Second
|
|
||||||
}
|
|
||||||
return &Client{
|
|
||||||
webhookURL: webhookURL,
|
|
||||||
secret: strings.TrimSpace(config.Secret),
|
|
||||||
atMobiles: compactStrings(config.AtMobiles),
|
|
||||||
atAll: config.AtAll,
|
|
||||||
httpClient: &http.Client{Timeout: timeout},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMarkdown 将钉钉 HTTP 状态和业务 errcode 都视为失败;业务层决定通知失败不能回滚审核事实。
|
|
||||||
func (c *Client) SendMarkdown(ctx context.Context, title string, text string) error {
|
|
||||||
if c == nil || c.httpClient == nil {
|
|
||||||
return errors.New("dingtalk client is not configured")
|
|
||||||
}
|
|
||||||
title, text = strings.TrimSpace(title), strings.TrimSpace(text)
|
|
||||||
if title == "" || text == "" {
|
|
||||||
return errors.New("dingtalk markdown title and text are required")
|
|
||||||
}
|
|
||||||
body, err := json.Marshal(map[string]any{
|
|
||||||
"msgtype": "markdown",
|
|
||||||
"markdown": map[string]string{"title": title, "text": text},
|
|
||||||
"at": map[string]any{"atMobiles": c.atMobiles, "isAtAll": c.atAll},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.signedWebhookURL(), bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
resp, err := c.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
payload, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
||||||
return fmt.Errorf("dingtalk returned http %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
var result struct {
|
|
||||||
ErrCode int `json:"errcode"`
|
|
||||||
ErrMsg string `json:"errmsg"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(payload, &result); err != nil {
|
|
||||||
return fmt.Errorf("decode dingtalk response: %w", err)
|
|
||||||
}
|
|
||||||
if result.ErrCode != 0 {
|
|
||||||
return fmt.Errorf("dingtalk returned errcode=%d errmsg=%s", result.ErrCode, result.ErrMsg)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) signedWebhookURL() string {
|
|
||||||
if c.secret == "" {
|
|
||||||
return c.webhookURL
|
|
||||||
}
|
|
||||||
parsed, err := url.Parse(c.webhookURL)
|
|
||||||
if err != nil {
|
|
||||||
return c.webhookURL
|
|
||||||
}
|
|
||||||
timestamp := time.Now().UTC().UnixMilli()
|
|
||||||
payload := fmt.Sprintf("%d\n%s", timestamp, c.secret)
|
|
||||||
mac := hmac.New(sha256.New, []byte(c.secret))
|
|
||||||
_, _ = mac.Write([]byte(payload))
|
|
||||||
query := parsed.Query()
|
|
||||||
query.Set("timestamp", fmt.Sprintf("%d", timestamp))
|
|
||||||
query.Set("sign", base64.StdEncoding.EncodeToString(mac.Sum(nil)))
|
|
||||||
parsed.RawQuery = query.Encode()
|
|
||||||
return parsed.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func compactStrings(values []string) []string {
|
|
||||||
out := make([]string, 0, len(values))
|
|
||||||
for _, value := range values {
|
|
||||||
if value = strings.TrimSpace(value); value != "" {
|
|
||||||
out = append(out, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
@ -12,7 +12,6 @@ type HostAgencySalaryPolicyListOptions struct {
|
|||||||
AppCode string
|
AppCode string
|
||||||
Keyword string
|
Keyword string
|
||||||
RegionID int64
|
RegionID int64
|
||||||
CycleKey string
|
|
||||||
Status string
|
Status string
|
||||||
SettlementMode string
|
SettlementMode string
|
||||||
SettlementTriggerMode string
|
SettlementTriggerMode string
|
||||||
@ -27,9 +26,6 @@ func (s *Store) ListHostAgencySalaryPolicies(options HostAgencySalaryPolicyListO
|
|||||||
// 第一阶段仍按单区域保存,区域筛选直接命中主表索引。
|
// 第一阶段仍按单区域保存,区域筛选直接命中主表索引。
|
||||||
db = db.Where("region_id = ?", options.RegionID)
|
db = db.Where("region_id = ?", options.RegionID)
|
||||||
}
|
}
|
||||||
if cycleKey := strings.TrimSpace(options.CycleKey); cycleKey != "" {
|
|
||||||
db = db.Where("cycle_key = ?", cycleKey)
|
|
||||||
}
|
|
||||||
if status := strings.TrimSpace(options.Status); status != "" {
|
if status := strings.TrimSpace(options.Status); status != "" {
|
||||||
db = db.Where("status = ?", status)
|
db = db.Where("status = ?", status)
|
||||||
}
|
}
|
||||||
@ -54,7 +50,7 @@ func (s *Store) ListHostAgencySalaryPolicies(options HostAgencySalaryPolicyListO
|
|||||||
// 等级必须随政策一起预加载并按 level_no 排序,前端展示和后续编辑都依赖稳定顺序。
|
// 等级必须随政策一起预加载并按 level_no 排序,前端展示和后续编辑都依赖稳定顺序。
|
||||||
err := db.Preload("Levels", func(tx *gorm.DB) *gorm.DB {
|
err := db.Preload("Levels", func(tx *gorm.DB) *gorm.DB {
|
||||||
return tx.Order("level_no ASC")
|
return tx.Order("level_no ASC")
|
||||||
}).Order("region_id ASC, cycle_key DESC, policy_version DESC, id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&items).Error
|
}).Order("region_id ASC, effective_from_ms DESC, id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&items).Error
|
||||||
return items, total, err
|
return items, total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,11 +71,6 @@ func (s *Store) CreateHostAgencySalaryPolicy(item *model.HostAgencySalaryPolicy)
|
|||||||
if err := tx.Create(item).Error; err != nil {
|
if err := tx.Create(item).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 主键在同一事务内作为不可变版本号;它全局唯一,也让旧数据可以无歧义回填 policy_version=id。
|
|
||||||
item.PolicyVersion = uint64(item.ID)
|
|
||||||
if err := tx.Model(item).UpdateColumn("policy_version", item.PolicyVersion).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for index := range levels {
|
for index := range levels {
|
||||||
levels[index].PolicyID = item.ID
|
levels[index].PolicyID = item.ID
|
||||||
}
|
}
|
||||||
@ -143,10 +134,15 @@ func (s *Store) DeleteHostAgencySalaryPolicy(appCode string, id uint) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) HasActiveHostAgencySalaryPolicyForCycle(appCode string, regionID int64, cycleKey string, excludeID uint) (bool, error) {
|
func (s *Store) HasOverlappingActiveHostAgencySalaryPolicy(appCode string, regionID int64, effectiveFromMS int64, effectiveToMS int64, excludeID uint) (bool, error) {
|
||||||
// 工资政策以周期为唯一选择边界;不同月份允许并存,同一区域同一月份只能有一条 active 配置。
|
// 与当前政策开始时间相交:已有长期有效或已有结束时间晚于当前开始。
|
||||||
db := s.db.Model(&model.HostAgencySalaryPolicy{}).
|
db := s.db.Model(&model.HostAgencySalaryPolicy{}).
|
||||||
Where("app_code = ? AND region_id = ? AND cycle_key = ? AND status = ?", strings.TrimSpace(appCode), regionID, strings.TrimSpace(cycleKey), "active")
|
Where("app_code = ? AND region_id = ? AND status = ?", strings.TrimSpace(appCode), regionID, "active").
|
||||||
|
Where("(effective_to_ms = 0 OR effective_to_ms > ?)", effectiveFromMS)
|
||||||
|
if effectiveToMS > 0 {
|
||||||
|
// 当前政策不是长期有效时,再要求已有政策开始时间早于当前结束,形成标准半开区间相交判断。
|
||||||
|
db = db.Where("effective_from_ms < ?", effectiveToMS)
|
||||||
|
}
|
||||||
if excludeID > 0 {
|
if excludeID > 0 {
|
||||||
db = db.Where("id <> ?", excludeID)
|
db = db.Where("id <> ?", excludeID)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -381,8 +381,7 @@ func (s *Store) seedMenus() error {
|
|||||||
{ParentID: &paymentID, Title: "三方临时支付链接", Code: "payment-temporary-links", Path: "/payment/temporary-links", Icon: "receipt", PermissionCode: "payment-temporary-link:view", Sort: 70, Visible: true},
|
{ParentID: &paymentID, Title: "三方临时支付链接", Code: "payment-temporary-links", Path: "/payment/temporary-links", Icon: "receipt", PermissionCode: "payment-temporary-link:view", Sort: 70, Visible: true},
|
||||||
{ParentID: &paymentID, Title: "支付内购商品", Code: "payment-recharge-products", Path: "/payment/recharge-products", Icon: "wallet", PermissionCode: "payment-product:view", Sort: 71, Visible: true},
|
{ParentID: &paymentID, Title: "支付内购商品", Code: "payment-recharge-products", Path: "/payment/recharge-products", Icon: "wallet", PermissionCode: "payment-product:view", Sort: 71, Visible: true},
|
||||||
{ParentID: &paymentID, Title: "币商充值订单", Code: "finance-coin-seller-recharge-orders", Path: "/finance/orders/coin-seller-recharges", Icon: "receipt", PermissionCode: "finance-order:coin-seller-recharge:view", Sort: 72, Visible: true},
|
{ParentID: &paymentID, Title: "币商充值订单", Code: "finance-coin-seller-recharge-orders", Path: "/finance/orders/coin-seller-recharges", Icon: "receipt", PermissionCode: "finance-order:coin-seller-recharge:view", Sort: 72, Visible: true},
|
||||||
// 活动模版权限继续用于 ops-center 的按钮和接口鉴权;主后台旧二级菜单只保留不可见兼容记录。
|
{ParentID: &activityID, Title: "活动模版", Code: "activity-template", Path: "/activities/templates", Icon: "campaign", PermissionCode: "activity-template:view", Sort: 68, Visible: true},
|
||||||
{ParentID: &activityID, Title: "活动模版", Code: "activity-template", Path: "/activities/templates", Icon: "campaign", PermissionCode: "activity-template:view", Sort: 68, Visible: false},
|
|
||||||
{ParentID: &activityID, Title: "每日任务", Code: "daily-task-list", Path: "/activities/daily-tasks", Icon: "task", PermissionCode: "daily-task:view", Sort: 69, Visible: true},
|
{ParentID: &activityID, Title: "每日任务", Code: "daily-task-list", Path: "/activities/daily-tasks", Icon: "task", PermissionCode: "daily-task:view", Sort: 69, Visible: true},
|
||||||
{ParentID: &activityID, Title: "注册奖励", Code: "registration-reward", Path: "/activities/registration-reward", Icon: "gift", PermissionCode: "registration-reward:view", Sort: 70, Visible: true},
|
{ParentID: &activityID, Title: "注册奖励", Code: "registration-reward", Path: "/activities/registration-reward", Icon: "gift", PermissionCode: "registration-reward:view", Sort: 70, Visible: true},
|
||||||
{ParentID: &activityID, Title: "成就配置", Code: "achievement-config", Path: "/activities/achievements", Icon: "military_tech", PermissionCode: "achievement:view", Sort: 71, Visible: true},
|
{ParentID: &activityID, Title: "成就配置", Code: "achievement-config", Path: "/activities/achievements", Icon: "military_tech", PermissionCode: "achievement:view", Sort: 71, Visible: true},
|
||||||
|
|||||||
@ -57,64 +57,6 @@ type WithdrawalApplicationAuditAction func(application model.UserWithdrawalAppli
|
|||||||
// 校验必须留在持行锁的短事务内,避免坏数据先进入 rejecting 后再永久卡住人工流程。
|
// 校验必须留在持行锁的短事务内,避免坏数据先进入 rejecting 后再永久卡住人工流程。
|
||||||
type WithdrawalApplicationClaimValidator func(application model.UserWithdrawalApplication) error
|
type WithdrawalApplicationClaimValidator func(application model.UserWithdrawalApplication) error
|
||||||
|
|
||||||
// CreateExternalWithdrawalApplication 以 App、来源系统、来源申请号作为业务幂等键。
|
|
||||||
// 来源钱包采用持久重试投递;重复提交必须返回同一 admin 申请,不能制造两张可独立审核的资金单。
|
|
||||||
func (s *Store) CreateExternalWithdrawalApplication(application model.UserWithdrawalApplication) (*model.UserWithdrawalApplication, error) {
|
|
||||||
application.AppCode = strings.TrimSpace(application.AppCode)
|
|
||||||
application.SourceSystem = strings.TrimSpace(application.SourceSystem)
|
|
||||||
application.SourceApplicationID = strings.TrimSpace(application.SourceApplicationID)
|
|
||||||
if application.AppCode == "" || application.SourceSystem == "" || application.SourceApplicationID == "" {
|
|
||||||
return nil, errors.New("external withdrawal source identity is incomplete")
|
|
||||||
}
|
|
||||||
|
|
||||||
var existing model.UserWithdrawalApplication
|
|
||||||
lookup := s.db.Where(
|
|
||||||
"app_code = ? AND source_system = ? AND source_application_id = ?",
|
|
||||||
application.AppCode,
|
|
||||||
application.SourceSystem,
|
|
||||||
application.SourceApplicationID,
|
|
||||||
).First(&existing)
|
|
||||||
if lookup.Error == nil {
|
|
||||||
if err := validateExternalWithdrawalReplay(existing, application); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &existing, nil
|
|
||||||
}
|
|
||||||
if !errors.Is(lookup.Error, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, lookup.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
result := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&application)
|
|
||||||
if result.Error != nil {
|
|
||||||
return nil, result.Error
|
|
||||||
}
|
|
||||||
// MySQL 对 ON DUPLICATE KEY 的 RowsAffected 会受连接参数影响;无论本次创建还是并发重放,都按业务键回读唯一事实。
|
|
||||||
if err := s.db.Where(
|
|
||||||
"app_code = ? AND source_system = ? AND source_application_id = ?",
|
|
||||||
application.AppCode,
|
|
||||||
application.SourceSystem,
|
|
||||||
application.SourceApplicationID,
|
|
||||||
).First(&existing).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := validateExternalWithdrawalReplay(existing, application); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &existing, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateExternalWithdrawalReplay(existing model.UserWithdrawalApplication, replay model.UserWithdrawalApplication) error {
|
|
||||||
if existing.UserID != replay.UserID ||
|
|
||||||
existing.SalaryAssetType != replay.SalaryAssetType ||
|
|
||||||
existing.WithdrawAmountMinor != replay.WithdrawAmountMinor ||
|
|
||||||
existing.WithdrawMethod != replay.WithdrawMethod ||
|
|
||||||
existing.WithdrawAddress != replay.WithdrawAddress {
|
|
||||||
// 相同来源单号只能重放完全一致的资金事实,不能用幂等接口覆盖已进入人工审核的金额或收款地址。
|
|
||||||
return errors.New("external withdrawal replay payload conflicts with existing application")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetWithdrawalApplication(id uint) (*model.UserWithdrawalApplication, error) {
|
func (s *Store) GetWithdrawalApplication(id uint) (*model.UserWithdrawalApplication, error) {
|
||||||
var application model.UserWithdrawalApplication
|
var application model.UserWithdrawalApplication
|
||||||
if err := s.db.First(&application, id).Error; err != nil {
|
if err := s.db.First(&application, id).Error; err != nil {
|
||||||
|
|||||||
@ -154,7 +154,6 @@ func New(cfg config.Config, auth *service.AuthService, store *repository.Store,
|
|||||||
appProtected.Use(middleware.RequireAppScope(store))
|
appProtected.Use(middleware.RequireAppScope(store))
|
||||||
|
|
||||||
authroutes.RegisterRoutes(api, protected, h.Auth)
|
authroutes.RegisterRoutes(api, protected, h.Auth)
|
||||||
financewithdrawal.RegisterIntegrationRoutes(api, h.FinanceWithdrawal)
|
|
||||||
externaladmin.RegisterExternalRoutes(api, h.ExternalAdmin, externaladmin.BusinessHandlers{
|
externaladmin.RegisterExternalRoutes(api, h.ExternalAdmin, externaladmin.BusinessHandlers{
|
||||||
AppUser: h.AppUser, HostOrg: h.HostOrg, RoomAdmin: h.RoomAdmin, Resource: h.Resource,
|
AppUser: h.AppUser, HostOrg: h.HostOrg, RoomAdmin: h.RoomAdmin, Resource: h.Resource,
|
||||||
PrettyID: h.PrettyID, AppConfig: h.AppConfig, VIPConfig: h.VIPConfig, Upload: h.Upload,
|
PrettyID: h.PrettyID, AppConfig: h.AppConfig, VIPConfig: h.VIPConfig, Upload: h.Upload,
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- 两列都是短 VARCHAR;线上使用 INPLACE/LOCK=NONE,避免跨 App 接入时阻塞现有 Lalu 提现写入。
|
|
||||||
SET @source_system_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'admin_user_withdrawal_applications'
|
|
||||||
AND COLUMN_NAME = 'source_system'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @source_system_ddl = IF(
|
|
||||||
@source_system_exists = 0,
|
|
||||||
'ALTER TABLE admin_user_withdrawal_applications ADD COLUMN source_system VARCHAR(32) NOT NULL DEFAULT ''hyapp_wallet'' AFTER app_code, ALGORITHM=INPLACE, LOCK=NONE',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @source_system_ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @source_application_id_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'admin_user_withdrawal_applications'
|
|
||||||
AND COLUMN_NAME = 'source_application_id'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @source_application_id_ddl = IF(
|
|
||||||
@source_application_id_exists = 0,
|
|
||||||
'ALTER TABLE admin_user_withdrawal_applications ADD COLUMN source_application_id VARCHAR(128) NOT NULL DEFAULT '''' AFTER source_system, ALGORITHM=INPLACE, LOCK=NONE',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @source_application_id_ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
-- 存量 Lalu 单使用已经唯一的 freeze_command_id 作为来源单号,确保新增唯一索引不会发生空值碰撞。
|
|
||||||
UPDATE admin_user_withdrawal_applications
|
|
||||||
SET source_system = 'hyapp_wallet',
|
|
||||||
source_application_id = freeze_command_id
|
|
||||||
WHERE source_application_id = '';
|
|
||||||
|
|
||||||
SET @source_index_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM information_schema.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'admin_user_withdrawal_applications'
|
|
||||||
AND INDEX_NAME = 'uk_admin_withdrawal_source_application'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 唯一索引同时承担来源重试的幂等查找;列选择性高,不增加列表查询扫描成本。
|
|
||||||
SET @source_index_ddl = IF(
|
|
||||||
@source_index_exists = 0,
|
|
||||||
'ALTER TABLE admin_user_withdrawal_applications ADD UNIQUE KEY uk_admin_withdrawal_source_application (app_code, source_system, source_application_id), ALGORITHM=INPLACE, LOCK=NONE',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @source_index_ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- 活动模版工作台迁入 ops-center,权限和旧路径仍保留用于接口鉴权及历史链接兼容。
|
|
||||||
-- admin_menus.code 是唯一索引,本次更新只命中一行,不扫描菜单表,也不改角色权限关系。
|
|
||||||
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
|
||||||
|
|
||||||
UPDATE admin_menus
|
|
||||||
SET visible = FALSE,
|
|
||||||
updated_at_ms = @now_ms
|
|
||||||
WHERE code = 'activity-template'
|
|
||||||
AND visible = TRUE;
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
SET time_zone = '+00:00';
|
|
||||||
|
|
||||||
-- 工资政策是小型配置表;迁移只回填政策行并创建周期索引,不扫描用户、工资进度或钱包流水。
|
|
||||||
SET @ddl := IF(
|
|
||||||
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_host_agency_salary_policies' AND COLUMN_NAME = 'cycle_key') = 0,
|
|
||||||
'ALTER TABLE admin_host_agency_salary_policies ADD COLUMN cycle_key VARCHAR(7) NOT NULL DEFAULT '''' COMMENT ''政策归属工资周期,UTC yyyy-MM'' AFTER region_id, ALGORITHM=INPLACE, LOCK=NONE',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @ddl := IF(
|
|
||||||
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_host_agency_salary_policies' AND COLUMN_NAME = 'policy_version') = 0,
|
|
||||||
'ALTER TABLE admin_host_agency_salary_policies ADD COLUMN policy_version BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''不可变政策版本,等于政策 ID'' AFTER cycle_key, ALGORITHM=INPLACE, LOCK=NONE',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
-- 老记录优先按原有效期起点归档;没有起点时依次使用发布时间、创建时间,全部按 UTC 计算。
|
|
||||||
UPDATE admin_host_agency_salary_policies
|
|
||||||
SET cycle_key = DATE_FORMAT(FROM_UNIXTIME(
|
|
||||||
COALESCE(NULLIF(effective_from_ms, 0), NULLIF(published_at_ms, 0), created_at_ms) / 1000
|
|
||||||
), '%Y-%m'),
|
|
||||||
policy_version = id
|
|
||||||
WHERE cycle_key = '' OR policy_version = 0;
|
|
||||||
|
|
||||||
SET @ddl := IF(
|
|
||||||
(SELECT COUNT(*) FROM information_schema.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_host_agency_salary_policies'
|
|
||||||
AND INDEX_NAME = 'uk_admin_host_agency_salary_policy_version') = 0,
|
|
||||||
'ALTER TABLE admin_host_agency_salary_policies ADD UNIQUE INDEX uk_admin_host_agency_salary_policy_version (app_code, region_id, cycle_key, policy_version), ALGORITHM=INPLACE, LOCK=NONE',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @ddl := IF(
|
|
||||||
(SELECT COUNT(*) FROM information_schema.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_host_agency_salary_policies'
|
|
||||||
AND INDEX_NAME = 'idx_admin_host_agency_salary_policy_cycle') = 0,
|
|
||||||
'ALTER TABLE admin_host_agency_salary_policies ADD INDEX idx_admin_host_agency_salary_policy_cycle (app_code, region_id, cycle_key, status), ALGORITHM=INPLACE, LOCK=NONE',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @ddl;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
@ -647,7 +647,7 @@ func (r *Repository) PrepareActivityTemplateTaskClaim(ctx context.Context, runti
|
|||||||
app_code, claim_id, command_id, template_id, template_code, version_no, task_day, task_key, user_id,
|
app_code, claim_id, command_id, template_id, template_code, version_no, task_day, task_key, user_id,
|
||||||
earning_region_id, attribution_at_ms, reward_resource_group_id, reward_snapshot_id, reward_snapshot_json, wallet_command_id,
|
earning_region_id, attribution_at_ms, reward_resource_group_id, reward_snapshot_id, reward_snapshot_json, wallet_command_id,
|
||||||
status, attempt_count, created_at_ms, updated_at_ms
|
status, attempt_count, created_at_ms, updated_at_ms
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?, ?)`,
|
||||||
claim.AppCode, claim.ClaimID, claim.CommandID, claim.TemplateID, claim.TemplateCode, claim.VersionNo,
|
claim.AppCode, claim.ClaimID, claim.CommandID, claim.TemplateID, claim.TemplateCode, claim.VersionNo,
|
||||||
claim.TaskDay, claim.TaskKey, claim.UserID, claim.EarningRegionID, claim.AttributionAtMS,
|
claim.TaskDay, claim.TaskKey, claim.UserID, claim.EarningRegionID, claim.AttributionAtMS,
|
||||||
claim.RewardResourceGroupID, claim.RewardSnapshot.SnapshotID,
|
claim.RewardResourceGroupID, claim.RewardSnapshot.SnapshotID,
|
||||||
|
|||||||
@ -43,18 +43,6 @@ type RoomGuardClient interface {
|
|||||||
ResolveRoomAppCode(ctx context.Context, req *roomv1.ResolveRoomAppCodeRequest) (*roomv1.ResolveRoomAppCodeResponse, error)
|
ResolveRoomAppCode(ctx context.Context, req *roomv1.ResolveRoomAppCodeRequest) (*roomv1.ResolveRoomAppCodeResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomVIPFeatureClient 是新版房间 VIP 展示能力的窄接口,避免扩大旧 RoomClient fake。
|
|
||||||
type RoomVIPFeatureClient interface {
|
|
||||||
ApplyRoomDecoration(ctx context.Context, req *roomv1.ApplyRoomDecorationRequest) (*roomv1.ApplyRoomDecorationResponse, error)
|
|
||||||
SendRoomImage(ctx context.Context, req *roomv1.SendRoomImageRequest) (*roomv1.SendRoomImageResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomMediaGuardClient 在 COS 写入前向 room owner 校验上传权限。
|
|
||||||
type RoomMediaGuardClient interface {
|
|
||||||
AuthorizeRoomMediaUpload(ctx context.Context, req *roomv1.AuthorizeRoomMediaUploadRequest) (*roomv1.AuthorizeRoomMediaUploadResponse, error)
|
|
||||||
CompleteRoomMediaUpload(ctx context.Context, req *roomv1.CompleteRoomMediaUploadRequest) (*roomv1.CompleteRoomMediaUploadResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomQueryClient 抽象 gateway 对 room-service 读模型查询的依赖。
|
// RoomQueryClient 抽象 gateway 对 room-service 读模型查询的依赖。
|
||||||
type RoomQueryClient interface {
|
type RoomQueryClient interface {
|
||||||
ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error)
|
ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error)
|
||||||
@ -120,14 +108,6 @@ func (c *grpcRoomClient) SetRoomBackground(ctx context.Context, req *roomv1.SetR
|
|||||||
return c.client.SetRoomBackground(ctx, req)
|
return c.client.SetRoomBackground(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *grpcRoomClient) ApplyRoomDecoration(ctx context.Context, req *roomv1.ApplyRoomDecorationRequest) (*roomv1.ApplyRoomDecorationResponse, error) {
|
|
||||||
return c.client.ApplyRoomDecoration(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *grpcRoomClient) SendRoomImage(ctx context.Context, req *roomv1.SendRoomImageRequest) (*roomv1.SendRoomImageResponse, error) {
|
|
||||||
return c.client.SendRoomImage(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *grpcRoomClient) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
|
func (c *grpcRoomClient) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
|
||||||
return c.client.JoinRoom(ctx, req)
|
return c.client.JoinRoom(ctx, req)
|
||||||
}
|
}
|
||||||
@ -220,14 +200,6 @@ func (c *grpcRoomGuardClient) ResolveRoomAppCode(ctx context.Context, req *roomv
|
|||||||
return c.client.ResolveRoomAppCode(ctx, req)
|
return c.client.ResolveRoomAppCode(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *grpcRoomGuardClient) AuthorizeRoomMediaUpload(ctx context.Context, req *roomv1.AuthorizeRoomMediaUploadRequest) (*roomv1.AuthorizeRoomMediaUploadResponse, error) {
|
|
||||||
return c.client.AuthorizeRoomMediaUpload(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *grpcRoomGuardClient) CompleteRoomMediaUpload(ctx context.Context, req *roomv1.CompleteRoomMediaUploadRequest) (*roomv1.CompleteRoomMediaUploadResponse, error) {
|
|
||||||
return c.client.CompleteRoomMediaUpload(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *grpcRoomQueryClient) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) {
|
func (c *grpcRoomQueryClient) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) {
|
||||||
return c.client.ListRooms(ctx, req)
|
return c.client.ListRooms(ctx, req)
|
||||||
}
|
}
|
||||||
@ -256,11 +228,6 @@ func (c *grpcRoomQueryClient) GetCurrentRoom(ctx context.Context, req *roomv1.Ge
|
|||||||
return c.client.GetCurrentRoom(ctx, req)
|
return c.client.GetCurrentRoom(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchGetUserVoiceRoomPresences 保持为具体 client 能力;只有个人资料页入口依赖它,避免扩大所有房间查询 mock 的接口面。
|
|
||||||
func (c *grpcRoomQueryClient) BatchGetUserVoiceRoomPresences(ctx context.Context, req *roomv1.BatchGetUserVoiceRoomPresencesRequest) (*roomv1.BatchGetUserVoiceRoomPresencesResponse, error) {
|
|
||||||
return c.client.BatchGetUserVoiceRoomPresences(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *grpcRoomQueryClient) GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) {
|
func (c *grpcRoomQueryClient) GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) {
|
||||||
return c.client.GetRoomSnapshot(ctx, req)
|
return c.client.GetRoomSnapshot(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,16 +26,15 @@ func (n *dingTalkNotifier) NotifyApplicationCreated(ctx context.Context, applica
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return n.client.SendMarkdown(ctx, dingtalkrobot.MarkdownMessage{
|
return n.client.SendMarkdown(ctx, dingtalkrobot.MarkdownMessage{
|
||||||
Title: "用户提现运营审核待处理【运营审核阶段】",
|
Title: "用户提现运营审核待处理",
|
||||||
Text: withdrawalApplicationCreatedMarkdown(application),
|
Text: withdrawalApplicationCreatedMarkdown(application),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func withdrawalApplicationCreatedMarkdown(application Application) string {
|
func withdrawalApplicationCreatedMarkdown(application Application) string {
|
||||||
return strings.Join([]string{
|
return strings.Join([]string{
|
||||||
"### 用户提现运营审核待处理【运营审核阶段】",
|
"### 用户提现运营审核待处理",
|
||||||
"",
|
"",
|
||||||
"- 当前阶段:**运营审核阶段**",
|
|
||||||
fmt.Sprintf("- 申请ID:%d", application.ID),
|
fmt.Sprintf("- 申请ID:%d", application.ID),
|
||||||
fmt.Sprintf("- APP:%s", markdownValue(application.AppCode)),
|
fmt.Sprintf("- APP:%s", markdownValue(application.AppCode)),
|
||||||
fmt.Sprintf("- 用户ID:%d", application.UserID),
|
fmt.Sprintf("- 用户ID:%d", application.UserID),
|
||||||
|
|||||||
@ -119,14 +119,12 @@ func (w *MySQLWriter) CreateApplication(ctx context.Context, command CreateAppli
|
|||||||
|
|
||||||
result, err := w.db.ExecContext(ctx, `
|
result, err := w.db.ExecContext(ctx, `
|
||||||
INSERT INTO admin_user_withdrawal_applications
|
INSERT INTO admin_user_withdrawal_applications
|
||||||
(app_code, source_system, source_application_id, user_id, salary_asset_type, withdraw_amount, withdraw_amount_minor,
|
(app_code, user_id, salary_asset_type, withdraw_amount, withdraw_amount_minor,
|
||||||
point_fee_amount, point_net_amount, points_per_usd, point_fee_bps, point_policy_instance_code,
|
point_fee_amount, point_net_amount, points_per_usd, point_fee_bps, point_policy_instance_code,
|
||||||
withdraw_method, withdraw_address, freeze_command_id, freeze_transaction_id, status, operations_status, created_at_ms, updated_at_ms)
|
withdraw_method, withdraw_address, freeze_command_id, freeze_transaction_id, status, operations_status, created_at_ms, updated_at_ms)
|
||||||
VALUES
|
VALUES
|
||||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
command.AppCode,
|
command.AppCode,
|
||||||
"hyapp_wallet",
|
|
||||||
command.FreezeCommandID,
|
|
||||||
formatUserID(command.UserID),
|
formatUserID(command.UserID),
|
||||||
command.SalaryAssetType,
|
command.SalaryAssetType,
|
||||||
command.WithdrawAmount,
|
command.WithdrawAmount,
|
||||||
|
|||||||
@ -93,8 +93,6 @@ func TestCreateApplicationInsertsAfterAppScopedMiss(t *testing.T) {
|
|||||||
mock.ExpectExec(`(?s)INSERT INTO admin_user_withdrawal_applications`).
|
mock.ExpectExec(`(?s)INSERT INTO admin_user_withdrawal_applications`).
|
||||||
WithArgs(
|
WithArgs(
|
||||||
"huwaa",
|
"huwaa",
|
||||||
"hyapp_wallet",
|
|
||||||
"cmd-point-freeze",
|
|
||||||
"42001",
|
"42001",
|
||||||
"POINT",
|
"POINT",
|
||||||
"9.50",
|
"9.50",
|
||||||
|
|||||||
@ -101,17 +101,10 @@ func WriteOK(writer http.ResponseWriter, request *http.Request, data any) {
|
|||||||
// WriteError 写失败 envelope。
|
// WriteError 写失败 envelope。
|
||||||
// 错误 message 保持稳定、短小,排障依赖 request_id 追日志,而不是把内部错误直接暴露给客户端。
|
// 错误 message 保持稳定、短小,排障依赖 request_id 追日志,而不是把内部错误直接暴露给客户端。
|
||||||
func WriteError(writer http.ResponseWriter, request *http.Request, statusCode int, code string, message string) {
|
func WriteError(writer http.ResponseWriter, request *http.Request, statusCode int, code string, message string) {
|
||||||
WriteErrorData(writer, request, statusCode, code, message, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteErrorData 写入客户端可执行的结构化错误详情。
|
|
||||||
// data 只能来自业务错误的显式安全 metadata 或 handler 已判定的公开状态,不能放原始 err。
|
|
||||||
func WriteErrorData(writer http.ResponseWriter, request *http.Request, statusCode int, code string, message string, data any) {
|
|
||||||
WriteEnvelope(writer, statusCode, ResponseEnvelope{
|
WriteEnvelope(writer, statusCode, ResponseEnvelope{
|
||||||
Code: code,
|
Code: code,
|
||||||
Message: message,
|
Message: message,
|
||||||
RequestID: ResponseRequestID(request),
|
RequestID: ResponseRequestID(request),
|
||||||
Data: data,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,30 +127,7 @@ func WriteRPCError(writer http.ResponseWriter, request *http.Request, err error)
|
|||||||
message = detail
|
message = detail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WriteErrorData(writer, request, statusCode, code, message, rpcErrorData(err))
|
WriteError(writer, request, statusCode, code, message)
|
||||||
}
|
|
||||||
|
|
||||||
// rpcErrorData 将 ErrorInfo.metadata 转成 HTTP JSON data,并只对已登记的等级字段做数值化。
|
|
||||||
// google.rpc 的 metadata wire 类型固定为 string;这里显式 ParseInt 后,Flutter 才能按整数
|
|
||||||
// 比较 required_level/current_effective_level,其余字段继续保持稳定字符串语义。
|
|
||||||
func rpcErrorData(err error) map[string]any {
|
|
||||||
metadata := xerr.MetadataFromGRPC(err)
|
|
||||||
if len(metadata) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
data := make(map[string]any, len(metadata))
|
|
||||||
for key, value := range metadata {
|
|
||||||
switch key {
|
|
||||||
case "required_level", "current_effective_level":
|
|
||||||
level, parseErr := strconv.ParseInt(value, 10, 32)
|
|
||||||
if parseErr == nil {
|
|
||||||
data[key] = level
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data[key] = value
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write 统一把内部 gRPC 调用结果转换为 HTTP JSON 响应。
|
// Write 统一把内部 gRPC 调用结果转换为 HTTP JSON 响应。
|
||||||
|
|||||||
@ -75,7 +75,6 @@ type UserHandlers struct {
|
|||||||
ResolveDisplayUserID http.HandlerFunc
|
ResolveDisplayUserID http.HandlerFunc
|
||||||
BatchUserProfiles http.HandlerFunc
|
BatchUserProfiles http.HandlerFunc
|
||||||
BatchRoomDisplayProfiles http.HandlerFunc
|
BatchRoomDisplayProfiles http.HandlerFunc
|
||||||
BatchUserVoiceRoomPresences http.HandlerFunc
|
|
||||||
GetMyOverview http.HandlerFunc
|
GetMyOverview http.HandlerFunc
|
||||||
GetMyInviteOverview http.HandlerFunc
|
GetMyInviteOverview http.HandlerFunc
|
||||||
GetMyInviteReferrer http.HandlerFunc
|
GetMyInviteReferrer http.HandlerFunc
|
||||||
@ -180,10 +179,6 @@ type RoomHandlers struct {
|
|||||||
SaveRoomBackground http.HandlerFunc
|
SaveRoomBackground http.HandlerFunc
|
||||||
ListRoomBackgrounds http.HandlerFunc
|
ListRoomBackgrounds http.HandlerFunc
|
||||||
SetRoomBackground http.HandlerFunc
|
SetRoomBackground http.HandlerFunc
|
||||||
UploadRoomBackground http.HandlerFunc
|
|
||||||
UploadRoomImage http.HandlerFunc
|
|
||||||
ApplyRoomDecoration http.HandlerFunc
|
|
||||||
SendRoomImage http.HandlerFunc
|
|
||||||
JoinRoom http.HandlerFunc
|
JoinRoom http.HandlerFunc
|
||||||
RoomHeartbeat http.HandlerFunc
|
RoomHeartbeat http.HandlerFunc
|
||||||
LeaveRoom http.HandlerFunc
|
LeaveRoom http.HandlerFunc
|
||||||
@ -474,7 +469,6 @@ func (r routes) registerUserRoutes() {
|
|||||||
r.public("/users/by-id/{user_id}/appearance", http.MethodGet, h.GetUserAppearance)
|
r.public("/users/by-id/{user_id}/appearance", http.MethodGet, h.GetUserAppearance)
|
||||||
r.profile("/users/profiles:batch", "", h.BatchUserProfiles)
|
r.profile("/users/profiles:batch", "", h.BatchUserProfiles)
|
||||||
r.profile("/users/room-display-profiles:batch", http.MethodGet, h.BatchRoomDisplayProfiles)
|
r.profile("/users/room-display-profiles:batch", http.MethodGet, h.BatchRoomDisplayProfiles)
|
||||||
r.profile("/users/voice-room-presence:batch", http.MethodGet, h.BatchUserVoiceRoomPresences)
|
|
||||||
r.profile("/users/me/overview", "", h.GetMyOverview)
|
r.profile("/users/me/overview", "", h.GetMyOverview)
|
||||||
r.profile("/users/me/invite-overview", http.MethodGet, h.GetMyInviteOverview)
|
r.profile("/users/me/invite-overview", http.MethodGet, h.GetMyInviteOverview)
|
||||||
r.profile("/users/me/invite-referrer", http.MethodGet, h.GetMyInviteReferrer)
|
r.profile("/users/me/invite-referrer", http.MethodGet, h.GetMyInviteReferrer)
|
||||||
@ -574,10 +568,6 @@ func (r routes) registerRoomRoutes() {
|
|||||||
r.profile("/rooms/{room_id}/rocket", http.MethodGet, h.GetRoomRocket)
|
r.profile("/rooms/{room_id}/rocket", http.MethodGet, h.GetRoomRocket)
|
||||||
r.profile("/rooms/{room_id}/follow", "", h.FollowRoom)
|
r.profile("/rooms/{room_id}/follow", "", h.FollowRoom)
|
||||||
r.profile("/rooms/{room_id}/backgrounds", http.MethodGet, h.ListRoomBackgrounds)
|
r.profile("/rooms/{room_id}/backgrounds", http.MethodGet, h.ListRoomBackgrounds)
|
||||||
r.profile("/rooms/{room_id}/backgrounds/upload", http.MethodPost, h.UploadRoomBackground)
|
|
||||||
r.profile("/rooms/{room_id}/messages/images/upload", http.MethodPost, h.UploadRoomImage)
|
|
||||||
r.profile("/rooms/{room_id}/messages/image", http.MethodPost, h.SendRoomImage)
|
|
||||||
r.profile("/rooms/{room_id}/decorations/apply", http.MethodPost, h.ApplyRoomDecoration)
|
|
||||||
r.profile("/rooms/create", http.MethodPost, h.CreateRoom)
|
r.profile("/rooms/create", http.MethodPost, h.CreateRoom)
|
||||||
r.profile("/rooms/profile/update", http.MethodPost, h.UpdateRoomProfile)
|
r.profile("/rooms/profile/update", http.MethodPost, h.UpdateRoomProfile)
|
||||||
r.profile("/rooms/backgrounds/save", http.MethodPost, h.SaveRoomBackground)
|
r.profile("/rooms/backgrounds/save", http.MethodPost, h.SaveRoomBackground)
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import (
|
|||||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
"hyapp/services/gateway-service/internal/transport/http/resourceview"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const resourceTypeEmojiPack = "emoji_pack"
|
const resourceTypeEmojiPack = "emoji_pack"
|
||||||
@ -40,7 +39,6 @@ type resourceData struct {
|
|||||||
AssetURL string `json:"asset_url"`
|
AssetURL string `json:"asset_url"`
|
||||||
PreviewURL string `json:"preview_url"`
|
PreviewURL string `json:"preview_url"`
|
||||||
AnimationURL string `json:"animation_url"`
|
AnimationURL string `json:"animation_url"`
|
||||||
Format string `json:"format,omitempty"`
|
|
||||||
MetadataJSON string `json:"metadata_json"`
|
MetadataJSON string `json:"metadata_json"`
|
||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
CreatedAtMS int64 `json:"created_at_ms"`
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
@ -90,12 +88,6 @@ type resourceShopPurchaseRequestBody struct {
|
|||||||
CommandIDAlt string `json:"commandId"`
|
CommandIDAlt string `json:"commandId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type resourceEquipRequestBody struct {
|
|
||||||
EntitlementID string `json:"entitlement_id"`
|
|
||||||
CommandID string `json:"command_id"`
|
|
||||||
CommandIDAlt string `json:"commandId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type resourceShopPurchaseData struct {
|
type resourceShopPurchaseData struct {
|
||||||
OrderID string `json:"order_id"`
|
OrderID string `json:"order_id"`
|
||||||
TransactionID string `json:"transaction_id"`
|
TransactionID string `json:"transaction_id"`
|
||||||
@ -483,31 +475,18 @@ func (h *Handler) equipMyResource(writer http.ResponseWriter, request *http.Requ
|
|||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body resourceEquipRequestBody
|
var body struct {
|
||||||
|
EntitlementID string `json:"entitlement_id"`
|
||||||
|
}
|
||||||
if request.Body != nil && request.Body != http.NoBody {
|
if request.Body != nil && request.Body != http.NoBody {
|
||||||
if err := httpkit.DecodeJSON(request.Body, &body); err != nil && !errors.Is(err, io.EOF) {
|
if err := httpkit.DecodeJSON(request.Body, &body); err != nil && !errors.Is(err, io.EOF) {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidJSON, "invalid request body")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidJSON, "invalid request body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
commandID := strings.TrimSpace(body.CommandID)
|
|
||||||
if commandID == "" {
|
|
||||||
commandID = strings.TrimSpace(body.CommandIDAlt)
|
|
||||||
}
|
|
||||||
appCode := appcode.FromContext(request.Context())
|
|
||||||
if commandID == "" {
|
|
||||||
if appCode == "fami" {
|
|
||||||
// Fami tiered VIP 是本次新契约,不能静默用每次都会变化的 request_id 伪装业务幂等。
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Lalu 等旧客户端没有业务 command_id;用本次 request_id 保持原有可调用性,但不承诺跨请求重放。
|
|
||||||
commandID = "legacy_equip:" + httpkit.RequestIDFromContext(request.Context())
|
|
||||||
}
|
|
||||||
resp, err := h.walletClient.EquipUserResource(request.Context(), &walletv1.EquipUserResourceRequest{
|
resp, err := h.walletClient.EquipUserResource(request.Context(), &walletv1.EquipUserResourceRequest{
|
||||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
CommandId: commandID,
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
AppCode: appCode,
|
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
ResourceId: resourceID,
|
ResourceId: resourceID,
|
||||||
EntitlementId: strings.TrimSpace(body.EntitlementID),
|
EntitlementId: strings.TrimSpace(body.EntitlementID),
|
||||||
@ -571,7 +550,6 @@ func resourceFromProto(item *walletv1.Resource) resourceData {
|
|||||||
AssetURL: item.GetAssetUrl(),
|
AssetURL: item.GetAssetUrl(),
|
||||||
PreviewURL: item.GetPreviewUrl(),
|
PreviewURL: item.GetPreviewUrl(),
|
||||||
AnimationURL: item.GetAnimationUrl(),
|
AnimationURL: item.GetAnimationUrl(),
|
||||||
Format: resourceview.FormatFromMetadata(item.GetMetadataJson()),
|
|
||||||
MetadataJSON: item.GetMetadataJson(),
|
MetadataJSON: item.GetMetadataJson(),
|
||||||
SortOrder: item.GetSortOrder(),
|
SortOrder: item.GetSortOrder(),
|
||||||
CreatedAtMS: item.GetCreatedAtMs(),
|
CreatedAtMS: item.GetCreatedAtMs(),
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
// Package resourceview contains the stable client-facing projection rules for wallet resources.
|
|
||||||
package resourceview
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FormatFromMetadata returns only an explicitly configured renderer identifier. format is the
|
|
||||||
// current contract and animation_format remains a legacy fallback; URL suffixes are intentionally
|
|
||||||
// never inspected because signed/CDN URLs are not a trustworthy media-format fact.
|
|
||||||
func FormatFromMetadata(raw string) string {
|
|
||||||
var metadata struct {
|
|
||||||
Format string `json:"format"`
|
|
||||||
AnimationFormat string `json:"animation_format"`
|
|
||||||
}
|
|
||||||
if json.Unmarshal([]byte(strings.TrimSpace(raw)), &metadata) != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if format := strings.ToLower(strings.TrimSpace(metadata.Format)); format != "" {
|
|
||||||
return format
|
|
||||||
}
|
|
||||||
return strings.ToLower(strings.TrimSpace(metadata.AnimationFormat))
|
|
||||||
}
|
|
||||||
@ -392,7 +392,6 @@ type fakeRoomQueryClient struct {
|
|||||||
lastFeeds *roomv1.ListRoomFeedsRequest
|
lastFeeds *roomv1.ListRoomFeedsRequest
|
||||||
lastMyRoom *roomv1.GetMyRoomRequest
|
lastMyRoom *roomv1.GetMyRoomRequest
|
||||||
lastCurrent *roomv1.GetCurrentRoomRequest
|
lastCurrent *roomv1.GetCurrentRoomRequest
|
||||||
lastVoicePresence *roomv1.BatchGetUserVoiceRoomPresencesRequest
|
|
||||||
lastSnapshot *roomv1.GetRoomSnapshotRequest
|
lastSnapshot *roomv1.GetRoomSnapshotRequest
|
||||||
lastBackgrounds *roomv1.ListRoomBackgroundsRequest
|
lastBackgrounds *roomv1.ListRoomBackgroundsRequest
|
||||||
lastRocket *roomv1.GetRoomRocketRequest
|
lastRocket *roomv1.GetRoomRocketRequest
|
||||||
@ -404,7 +403,6 @@ type fakeRoomQueryClient struct {
|
|||||||
feedsResp *roomv1.ListRoomsResponse
|
feedsResp *roomv1.ListRoomsResponse
|
||||||
myRoomResp *roomv1.GetMyRoomResponse
|
myRoomResp *roomv1.GetMyRoomResponse
|
||||||
currentResp *roomv1.GetCurrentRoomResponse
|
currentResp *roomv1.GetCurrentRoomResponse
|
||||||
voicePresenceResp *roomv1.BatchGetUserVoiceRoomPresencesResponse
|
|
||||||
snapshotResp *roomv1.GetRoomSnapshotResponse
|
snapshotResp *roomv1.GetRoomSnapshotResponse
|
||||||
backgroundsResp *roomv1.ListRoomBackgroundsResponse
|
backgroundsResp *roomv1.ListRoomBackgroundsResponse
|
||||||
rocketResp *roomv1.GetRoomRocketResponse
|
rocketResp *roomv1.GetRoomRocketResponse
|
||||||
@ -416,7 +414,6 @@ type fakeRoomQueryClient struct {
|
|||||||
feedsErr error
|
feedsErr error
|
||||||
myRoomErr error
|
myRoomErr error
|
||||||
currentErr error
|
currentErr error
|
||||||
voicePresenceErr error
|
|
||||||
snapshotErr error
|
snapshotErr error
|
||||||
backgroundsErr error
|
backgroundsErr error
|
||||||
rocketErr error
|
rocketErr error
|
||||||
@ -1393,17 +1390,6 @@ func (f *fakeRoomQueryClient) GetCurrentRoom(_ context.Context, req *roomv1.GetC
|
|||||||
return &roomv1.GetCurrentRoomResponse{}, nil
|
return &roomv1.GetCurrentRoomResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeRoomQueryClient) BatchGetUserVoiceRoomPresences(_ context.Context, req *roomv1.BatchGetUserVoiceRoomPresencesRequest) (*roomv1.BatchGetUserVoiceRoomPresencesResponse, error) {
|
|
||||||
f.lastVoicePresence = req
|
|
||||||
if f.voicePresenceErr != nil {
|
|
||||||
return nil, f.voicePresenceErr
|
|
||||||
}
|
|
||||||
if f.voicePresenceResp != nil {
|
|
||||||
return f.voicePresenceResp, nil
|
|
||||||
}
|
|
||||||
return &roomv1.BatchGetUserVoiceRoomPresencesResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeRoomQueryClient) GetRoomSnapshot(_ context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) {
|
func (f *fakeRoomQueryClient) GetRoomSnapshot(_ context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) {
|
||||||
f.lastSnapshot = req
|
f.lastSnapshot = req
|
||||||
if f.snapshotErr != nil {
|
if f.snapshotErr != nil {
|
||||||
@ -4825,87 +4811,6 @@ func TestGetCurrentRoomUsesAuthenticatedUser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBatchUserVoiceRoomPresencesUsesAuthenticatedViewerAndStableJSON(t *testing.T) {
|
|
||||||
queryClient := &fakeRoomQueryClient{voicePresenceResp: &roomv1.BatchGetUserVoiceRoomPresencesResponse{
|
|
||||||
Items: []*roomv1.UserVoiceRoomPresence{
|
|
||||||
{
|
|
||||||
UserId: 1002,
|
|
||||||
State: "on_mic",
|
|
||||||
SeatNo: 3,
|
|
||||||
Joinable: true,
|
|
||||||
ObservedAtMs: 1_784_265_600_000,
|
|
||||||
Room: &roomv1.UserVoiceRoomPresenceRoom{
|
|
||||||
RoomId: "room-profile-live",
|
|
||||||
RoomShortId: "886688",
|
|
||||||
OwnerUserId: 1001,
|
|
||||||
HostUserId: 1001,
|
|
||||||
Title: "Live Room",
|
|
||||||
CoverUrl: "https://cdn.example.com/room.png",
|
|
||||||
BackgroundUrl: "https://cdn.example.com/background.png",
|
|
||||||
Mode: "voice",
|
|
||||||
OnlineCount: 56,
|
|
||||||
SeatCount: 10,
|
|
||||||
Locked: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{UserId: 1003, State: "not_on_mic", ObservedAtMs: 1_784_265_600_000},
|
|
||||||
},
|
|
||||||
ServerTimeMs: 1_784_265_600_123,
|
|
||||||
}}
|
|
||||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
|
||||||
handler.SetRoomQueryClient(queryClient)
|
|
||||||
router := handler.Routes(auth.NewVerifier("secret"))
|
|
||||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/users/voice-room-presence:batch?user_ids=1002,1003,1002", nil)
|
|
||||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
|
||||||
request.Header.Set("X-Request-ID", "req-profile-live")
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
|
||||||
|
|
||||||
if recorder.Code != http.StatusOK {
|
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
|
||||||
}
|
|
||||||
if queryClient.lastVoicePresence == nil || queryClient.lastVoicePresence.GetViewerUserId() != 42 {
|
|
||||||
t.Fatalf("voice presence query must use authenticated viewer: %+v", queryClient.lastVoicePresence)
|
|
||||||
}
|
|
||||||
if got := queryClient.lastVoicePresence.GetUserIds(); len(got) != 2 || got[0] != 1002 || got[1] != 1003 {
|
|
||||||
t.Fatalf("voice presence query must preserve first-seen deduplicated ids: %v", got)
|
|
||||||
}
|
|
||||||
if queryClient.lastVoicePresence.GetMeta().GetRequestId() != assertGeneratedRequestID(t, recorder, "req-profile-live") {
|
|
||||||
t.Fatalf("voice presence query must propagate generated request id: %+v", queryClient.lastVoicePresence.GetMeta())
|
|
||||||
}
|
|
||||||
|
|
||||||
var response httpkit.ResponseEnvelope
|
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
|
||||||
t.Fatalf("decode voice presence response failed: %v", err)
|
|
||||||
}
|
|
||||||
data := response.Data.(map[string]any)
|
|
||||||
items := data["items"].([]any)
|
|
||||||
onMic := items[0].(map[string]any)
|
|
||||||
room := onMic["room"].(map[string]any)
|
|
||||||
if onMic["user_id"] != "1002" || onMic["state"] != "on_mic" || onMic["joinable"] != true || room["room_id"] != "room-profile-live" || room["rtc_room_id"] != "room-profile-live" || room["locked"] != true {
|
|
||||||
t.Fatalf("on-mic response contract mismatch: %+v", onMic)
|
|
||||||
}
|
|
||||||
notOnMic := items[1].(map[string]any)
|
|
||||||
if notOnMic["state"] != "not_on_mic" || notOnMic["joinable"] != false || notOnMic["room"] != nil {
|
|
||||||
t.Fatalf("not-on-mic response must keep false and null fields: %+v", notOnMic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBatchUserVoiceRoomPresencesRejectsInvalidIDs(t *testing.T) {
|
|
||||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
|
||||||
handler.SetRoomQueryClient(&fakeRoomQueryClient{})
|
|
||||||
router := handler.Routes(auth.NewVerifier("secret"))
|
|
||||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/users/voice-room-presence:batch?user_ids=1002,,1003", nil)
|
|
||||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
|
||||||
request.Header.Set("X-Request-ID", "req-profile-live-invalid")
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
|
||||||
|
|
||||||
assertEnvelope(t, recorder, http.StatusBadRequest, "INVALID_USER_IDS", "req-profile-live-invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRoomSnapshotUsesAuthenticatedUser(t *testing.T) {
|
func TestGetRoomSnapshotUsesAuthenticatedUser(t *testing.T) {
|
||||||
queryClient := &fakeRoomQueryClient{snapshotResp: &roomv1.GetRoomSnapshotResponse{
|
queryClient := &fakeRoomQueryClient{snapshotResp: &roomv1.GetRoomSnapshotResponse{
|
||||||
Room: &roomv1.RoomSnapshot{
|
Room: &roomv1.RoomSnapshot{
|
||||||
@ -6762,7 +6667,6 @@ func TestUpdateAgencyProfileForwardsOwnerCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHostCenterPlatformPolicyUsesCurrentHostRegion(t *testing.T) {
|
func TestHostCenterPlatformPolicyUsesCurrentHostRegion(t *testing.T) {
|
||||||
expectedCycle := time.Now().UTC().Format("2006-01")
|
|
||||||
hostClient := &fakeUserHostClient{hostProfile: &userv1.HostProfile{
|
hostClient := &fakeUserHostClient{hostProfile: &userv1.HostProfile{
|
||||||
UserId: 42,
|
UserId: 42,
|
||||||
Status: "active",
|
Status: "active",
|
||||||
@ -6774,8 +6678,6 @@ func TestHostCenterPlatformPolicyUsesCurrentHostRegion(t *testing.T) {
|
|||||||
Found: true,
|
Found: true,
|
||||||
Policy: &walletv1.HostSalaryPolicy{
|
Policy: &walletv1.HostSalaryPolicy{
|
||||||
PolicyId: 9001,
|
PolicyId: 9001,
|
||||||
CycleKey: expectedCycle,
|
|
||||||
PolicyVersion: 9001,
|
|
||||||
Name: "Host Growth Policy",
|
Name: "Host Growth Policy",
|
||||||
RegionId: 30,
|
RegionId: 30,
|
||||||
Status: "active",
|
Status: "active",
|
||||||
@ -6792,7 +6694,7 @@ func TestHostCenterPlatformPolicyUsesCurrentHostRegion(t *testing.T) {
|
|||||||
},
|
},
|
||||||
hostSalaryProgressResp: &walletv1.GetHostSalaryProgressResponse{Progress: &walletv1.HostSalaryProgress{
|
hostSalaryProgressResp: &walletv1.GetHostSalaryProgressResponse{Progress: &walletv1.HostSalaryProgress{
|
||||||
HostUserId: 42,
|
HostUserId: 42,
|
||||||
CycleKey: expectedCycle,
|
CycleKey: "2026-06",
|
||||||
RegionId: 30,
|
RegionId: 30,
|
||||||
AgencyOwnerUserId: 99,
|
AgencyOwnerUserId: 99,
|
||||||
TotalDiamonds: 1200,
|
TotalDiamonds: 1200,
|
||||||
@ -6816,10 +6718,10 @@ func TestHostCenterPlatformPolicyUsesCurrentHostRegion(t *testing.T) {
|
|||||||
if hostClient.lastHost == nil || hostClient.lastHost.GetUserId() != 42 {
|
if hostClient.lastHost == nil || hostClient.lastHost.GetUserId() != 42 {
|
||||||
t.Fatalf("host profile request mismatch: %+v", hostClient.lastHost)
|
t.Fatalf("host profile request mismatch: %+v", hostClient.lastHost)
|
||||||
}
|
}
|
||||||
if walletClient.lastHostSalaryPolicy == nil || walletClient.lastHostSalaryPolicy.GetRegionId() != 30 || walletClient.lastHostSalaryPolicy.GetCycleKey() != expectedCycle || walletClient.lastHostSalaryPolicy.GetSettlementTriggerMode() != "" || walletClient.lastHostSalaryPolicy.GetAppCode() == "" || walletClient.lastHostSalaryPolicy.GetRequestId() == "" {
|
if walletClient.lastHostSalaryPolicy == nil || walletClient.lastHostSalaryPolicy.GetRegionId() != 30 || walletClient.lastHostSalaryPolicy.GetSettlementTriggerMode() != "" || walletClient.lastHostSalaryPolicy.GetAppCode() == "" || walletClient.lastHostSalaryPolicy.GetRequestId() == "" {
|
||||||
t.Fatalf("host salary policy request mismatch: %+v", walletClient.lastHostSalaryPolicy)
|
t.Fatalf("host salary policy request mismatch: %+v", walletClient.lastHostSalaryPolicy)
|
||||||
}
|
}
|
||||||
if walletClient.lastHostSalaryProgress == nil || walletClient.lastHostSalaryProgress.GetHostUserId() != 42 || walletClient.lastHostSalaryProgress.GetCycleKey() != expectedCycle || walletClient.lastHostSalaryProgress.GetAppCode() == "" || walletClient.lastHostSalaryProgress.GetRequestId() == "" {
|
if walletClient.lastHostSalaryProgress == nil || walletClient.lastHostSalaryProgress.GetHostUserId() != 42 || walletClient.lastHostSalaryProgress.GetAppCode() == "" || walletClient.lastHostSalaryProgress.GetRequestId() == "" {
|
||||||
t.Fatalf("host salary progress request mismatch: %+v", walletClient.lastHostSalaryProgress)
|
t.Fatalf("host salary progress request mismatch: %+v", walletClient.lastHostSalaryProgress)
|
||||||
}
|
}
|
||||||
var response httpkit.ResponseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
@ -6832,10 +6734,10 @@ func TestHostCenterPlatformPolicyUsesCurrentHostRegion(t *testing.T) {
|
|||||||
firstLevel := levels[0].(map[string]any)
|
firstLevel := levels[0].(map[string]any)
|
||||||
progress := data["progress"].(map[string]any)
|
progress := data["progress"].(map[string]any)
|
||||||
levelProgress := data["level_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["cycle_key"] != expectedCycle || policy["policy_version"] != float64(9001) || policy["settlement_mode"] != "half_month" || policy["settlement_trigger_mode"] != "manual" || firstLevel["host_salary_usd"] != 15.0 || firstLevel["agency_salary_usd"] != 3.0 {
|
if data["found"] != true || data["host_region_id"] != float64(30) || policy["policy_id"] != "9001" || policy["settlement_mode"] != "half_month" || policy["settlement_trigger_mode"] != "manual" || firstLevel["host_salary_usd"] != 15.0 || firstLevel["agency_salary_usd"] != 3.0 {
|
||||||
t.Fatalf("host policy response mismatch: %+v", data)
|
t.Fatalf("host policy response mismatch: %+v", data)
|
||||||
}
|
}
|
||||||
if progress["cycle_key"] != expectedCycle || progress["total_diamonds"] != float64(1200) {
|
if progress["cycle_key"] != "2026-06" || progress["total_diamonds"] != float64(1200) {
|
||||||
t.Fatalf("host salary progress response mismatch: %+v", progress)
|
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) {
|
if levelProgress["current_level"] != float64(1) || levelProgress["current_value"] != float64(1200) || levelProgress["next_level"] != float64(2) || levelProgress["needed_for_next_level"] != float64(1800) {
|
||||||
@ -6930,7 +6832,6 @@ func TestAgencyCenterOverviewAllowsOwnerWithoutManagerProfile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAgencyCenterPlatformPolicyUsesAgencyRegion(t *testing.T) {
|
func TestAgencyCenterPlatformPolicyUsesAgencyRegion(t *testing.T) {
|
||||||
expectedCycle := time.Now().UTC().Format("2006-01")
|
|
||||||
hostClient := &fakeUserHostClient{
|
hostClient := &fakeUserHostClient{
|
||||||
roleSummary: &userv1.UserRoleSummary{UserId: 42, IsAgency: true, IsManager: false, AgencyId: 7001},
|
roleSummary: &userv1.UserRoleSummary{UserId: 42, IsAgency: true, IsManager: false, AgencyId: 7001},
|
||||||
getAgencyResp: &userv1.GetAgencyResponse{Agency: &userv1.Agency{
|
getAgencyResp: &userv1.GetAgencyResponse{Agency: &userv1.Agency{
|
||||||
@ -6946,8 +6847,6 @@ func TestAgencyCenterPlatformPolicyUsesAgencyRegion(t *testing.T) {
|
|||||||
Found: true,
|
Found: true,
|
||||||
Policy: &walletv1.HostSalaryPolicy{
|
Policy: &walletv1.HostSalaryPolicy{
|
||||||
PolicyId: 9101,
|
PolicyId: 9101,
|
||||||
CycleKey: expectedCycle,
|
|
||||||
PolicyVersion: 9101,
|
|
||||||
Name: "Agency Region Policy",
|
Name: "Agency Region Policy",
|
||||||
RegionId: 31,
|
RegionId: 31,
|
||||||
Status: "active",
|
Status: "active",
|
||||||
@ -6979,7 +6878,7 @@ func TestAgencyCenterPlatformPolicyUsesAgencyRegion(t *testing.T) {
|
|||||||
if hostClient.lastRoleSummary == nil || hostClient.lastRoleSummary.GetUserId() != 42 || hostClient.lastGetAgency == nil || hostClient.lastGetAgency.GetAgencyId() != 7001 {
|
if hostClient.lastRoleSummary == nil || hostClient.lastRoleSummary.GetUserId() != 42 || hostClient.lastGetAgency == nil || hostClient.lastGetAgency.GetAgencyId() != 7001 {
|
||||||
t.Fatalf("agency owner resolution mismatch: role=%+v agency=%+v", hostClient.lastRoleSummary, hostClient.lastGetAgency)
|
t.Fatalf("agency owner resolution mismatch: role=%+v agency=%+v", hostClient.lastRoleSummary, hostClient.lastGetAgency)
|
||||||
}
|
}
|
||||||
if walletClient.lastHostSalaryPolicy == nil || walletClient.lastHostSalaryPolicy.GetRegionId() != 31 || walletClient.lastHostSalaryPolicy.GetCycleKey() != expectedCycle || walletClient.lastHostSalaryPolicy.GetSettlementTriggerMode() != "" || walletClient.lastHostSalaryPolicy.GetAppCode() == "" || walletClient.lastHostSalaryPolicy.GetRequestId() == "" {
|
if walletClient.lastHostSalaryPolicy == nil || walletClient.lastHostSalaryPolicy.GetRegionId() != 31 || walletClient.lastHostSalaryPolicy.GetSettlementTriggerMode() != "" || walletClient.lastHostSalaryPolicy.GetAppCode() == "" || walletClient.lastHostSalaryPolicy.GetRequestId() == "" {
|
||||||
t.Fatalf("agency policy request mismatch: %+v", walletClient.lastHostSalaryPolicy)
|
t.Fatalf("agency policy request mismatch: %+v", walletClient.lastHostSalaryPolicy)
|
||||||
}
|
}
|
||||||
if walletClient.lastHostSalaryProgress != nil {
|
if walletClient.lastHostSalaryProgress != nil {
|
||||||
@ -6993,7 +6892,7 @@ func TestAgencyCenterPlatformPolicyUsesAgencyRegion(t *testing.T) {
|
|||||||
policy := data["policy"].(map[string]any)
|
policy := data["policy"].(map[string]any)
|
||||||
levels := policy["levels"].([]any)
|
levels := policy["levels"].([]any)
|
||||||
secondLevel := levels[1].(map[string]any)
|
secondLevel := levels[1].(map[string]any)
|
||||||
if data["found"] != true || data["agency_region_id"] != float64(31) || policy["policy_id"] != "9101" || policy["cycle_key"] != expectedCycle || policy["policy_version"] != float64(9101) || len(levels) != 2 || secondLevel["agency_salary_usd"] != 7.2 {
|
if data["found"] != true || data["agency_region_id"] != float64(31) || policy["policy_id"] != "9101" || len(levels) != 2 || secondLevel["agency_salary_usd"] != 7.2 {
|
||||||
t.Fatalf("agency policy response mismatch: %+v", data)
|
t.Fatalf("agency policy response mismatch: %+v", data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11130,7 +11029,6 @@ func TestProfileGateRejectsIncompleteUsersForRoomIMRTCPaidCapabilities(t *testin
|
|||||||
{name: "message_delete", method: http.MethodDelete, path: "/api/v1/messages/msg-1"},
|
{name: "message_delete", method: http.MethodDelete, path: "/api/v1/messages/msg-1"},
|
||||||
{name: "direct_gift", method: http.MethodPost, path: "/api/v1/messages/direct-gifts/send", body: `{"command_id":"cmd-direct-gift","target_user_id":900002,"gift_id":"rose","gift_count":1}`},
|
{name: "direct_gift", method: http.MethodPost, path: "/api/v1/messages/direct-gifts/send", body: `{"command_id":"cmd-direct-gift","target_user_id":900002,"gift_id":"rose","gift_count":1}`},
|
||||||
{name: "profiles_batch", method: http.MethodGet, path: "/api/v1/users/profiles:batch?user_ids=42"},
|
{name: "profiles_batch", method: http.MethodGet, path: "/api/v1/users/profiles:batch?user_ids=42"},
|
||||||
{name: "voice_room_presence_batch", method: http.MethodGet, path: "/api/v1/users/voice-room-presence:batch?user_ids=43"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
package roomapi
|
package roomapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -13,25 +11,12 @@ import (
|
|||||||
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
"hyapp/services/gateway-service/internal/transport/http/httproutes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ObjectUploader 是房间专用媒体上传所需的最小 COS 能力。
|
|
||||||
type ObjectUploader interface {
|
|
||||||
PutObject(ctx context.Context, key string, reader io.Reader, sizeBytes int64, contentType string) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type objectURLVerifier interface {
|
|
||||||
OwnsObjectURL(objectURL string, objectKey string) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler owns room-facing HTTP endpoints in gateway.
|
// Handler owns room-facing HTTP endpoints in gateway.
|
||||||
// It keeps gateway as protocol adapter only: room state still belongs to room-service.
|
// It keeps gateway as protocol adapter only: room state still belongs to room-service.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
roomClient client.RoomClient
|
roomClient client.RoomClient
|
||||||
roomGuardClient client.RoomGuardClient
|
roomGuardClient client.RoomGuardClient
|
||||||
roomQueryClient client.RoomQueryClient
|
roomQueryClient client.RoomQueryClient
|
||||||
roomVIPClient client.RoomVIPFeatureClient
|
|
||||||
roomMediaGuard client.RoomMediaGuardClient
|
|
||||||
objectUploader ObjectUploader
|
|
||||||
objectURLVerifier objectURLVerifier
|
|
||||||
userProfileClient client.UserProfileClient
|
userProfileClient client.UserProfileClient
|
||||||
userCountryClient client.UserCountryQueryClient
|
userCountryClient client.UserCountryQueryClient
|
||||||
userRegionClient client.UserRegionClient
|
userRegionClient client.UserRegionClient
|
||||||
@ -77,7 +62,6 @@ type Config struct {
|
|||||||
GiftMaxCount int
|
GiftMaxCount int
|
||||||
GiftMaxUnits int
|
GiftMaxUnits int
|
||||||
RTCTokenConfig tencentrtc.TokenConfig
|
RTCTokenConfig tencentrtc.TokenConfig
|
||||||
ObjectUploader ObjectUploader
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config Config) *Handler {
|
func New(config Config) *Handler {
|
||||||
@ -98,17 +82,10 @@ func New(config Config) *Handler {
|
|||||||
giftMaxUnits = 5_000
|
giftMaxUnits = 5_000
|
||||||
}
|
}
|
||||||
comboReader, _ := any(config.AppConfigReader).(giftComboConfigReader)
|
comboReader, _ := any(config.AppConfigReader).(giftComboConfigReader)
|
||||||
roomVIPClient, _ := any(config.RoomClient).(client.RoomVIPFeatureClient)
|
|
||||||
roomMediaGuard, _ := any(config.RoomGuardClient).(client.RoomMediaGuardClient)
|
|
||||||
urlVerifier, _ := any(config.ObjectUploader).(objectURLVerifier)
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
roomClient: config.RoomClient,
|
roomClient: config.RoomClient,
|
||||||
roomGuardClient: config.RoomGuardClient,
|
roomGuardClient: config.RoomGuardClient,
|
||||||
roomQueryClient: config.RoomQueryClient,
|
roomQueryClient: config.RoomQueryClient,
|
||||||
roomVIPClient: roomVIPClient,
|
|
||||||
roomMediaGuard: roomMediaGuard,
|
|
||||||
objectUploader: config.ObjectUploader,
|
|
||||||
objectURLVerifier: urlVerifier,
|
|
||||||
userProfileClient: config.UserProfileClient,
|
userProfileClient: config.UserProfileClient,
|
||||||
userCountryClient: config.UserCountryClient,
|
userCountryClient: config.UserCountryClient,
|
||||||
userRegionClient: config.UserRegionClient,
|
userRegionClient: config.UserRegionClient,
|
||||||
@ -154,10 +131,6 @@ func (h *Handler) RoomHandlers() httproutes.RoomHandlers {
|
|||||||
SaveRoomBackground: h.saveRoomBackground,
|
SaveRoomBackground: h.saveRoomBackground,
|
||||||
ListRoomBackgrounds: h.listRoomBackgrounds,
|
ListRoomBackgrounds: h.listRoomBackgrounds,
|
||||||
SetRoomBackground: h.setRoomBackground,
|
SetRoomBackground: h.setRoomBackground,
|
||||||
UploadRoomBackground: h.uploadRoomBackground,
|
|
||||||
UploadRoomImage: h.uploadRoomImage,
|
|
||||||
ApplyRoomDecoration: h.applyRoomDecoration,
|
|
||||||
SendRoomImage: h.sendRoomImage,
|
|
||||||
JoinRoom: h.joinRoom,
|
JoinRoom: h.joinRoom,
|
||||||
RoomHeartbeat: h.roomHeartbeat,
|
RoomHeartbeat: h.roomHeartbeat,
|
||||||
LeaveRoom: h.leaveRoom,
|
LeaveRoom: h.leaveRoom,
|
||||||
@ -188,7 +161,3 @@ func (h *Handler) IssueTencentRTCToken(writer http.ResponseWriter, request *http
|
|||||||
func (h *Handler) BatchRoomDisplayProfiles(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) BatchRoomDisplayProfiles(writer http.ResponseWriter, request *http.Request) {
|
||||||
h.batchRoomDisplayProfiles(writer, request)
|
h.batchRoomDisplayProfiles(writer, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) BatchUserVoiceRoomPresences(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
h.batchUserVoiceRoomPresences(writer, request)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import (
|
|||||||
"hyapp/pkg/roomid"
|
"hyapp/pkg/roomid"
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
"hyapp/services/gateway-service/internal/transport/http/resourceview"
|
|
||||||
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
@ -1235,43 +1234,23 @@ func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Re
|
|||||||
func (h *Handler) saveRoomBackground(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) saveRoomBackground(writer http.ResponseWriter, request *http.Request) {
|
||||||
var body struct {
|
var body struct {
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
CommandID string `json:"command_id"`
|
|
||||||
ImageURL string `json:"image_url"`
|
ImageURL string `json:"image_url"`
|
||||||
Media *roomMediaData `json:"media"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !httpkit.Decode(writer, request, &body) {
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
body.RoomID = strings.TrimSpace(body.RoomID)
|
body.RoomID = strings.TrimSpace(body.RoomID)
|
||||||
body.CommandID = strings.TrimSpace(body.CommandID)
|
|
||||||
body.ImageURL = strings.TrimSpace(body.ImageURL)
|
body.ImageURL = strings.TrimSpace(body.ImageURL)
|
||||||
if body.Media != nil && body.ImageURL == "" {
|
|
||||||
body.ImageURL = strings.TrimSpace(body.Media.URL)
|
|
||||||
}
|
|
||||||
if body.Media != nil && body.ImageURL != strings.TrimSpace(body.Media.URL) {
|
|
||||||
// 新版请求的 image_url 只是 media.url 的兼容镜像;两者不一致时拒绝,避免调用方误以为保存了另一张图。
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "image_url does not match media.url")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !roomid.ValidStringID(body.RoomID) || body.ImageURL == "" {
|
if !roomid.ValidStringID(body.RoomID) || body.ImageURL == "" {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var media *roomv1.RoomMedia
|
|
||||||
if body.Media != nil {
|
|
||||||
if h.objectURLVerifier == nil || !h.objectURLVerifier.OwnsObjectURL(body.Media.URL, body.Media.ObjectKey) {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid room background media")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
media = body.Media.proto()
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := h.roomClient.SaveRoomBackground(request.Context(), &roomv1.SaveRoomBackgroundRequest{
|
resp, err := h.roomClient.SaveRoomBackground(request.Context(), &roomv1.SaveRoomBackgroundRequest{
|
||||||
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
Meta: httpkit.RoomMeta(request, body.RoomID, ""),
|
||||||
RoomId: body.RoomID,
|
RoomId: body.RoomID,
|
||||||
ImageUrl: body.ImageURL,
|
ImageUrl: body.ImageURL,
|
||||||
Media: media,
|
|
||||||
})
|
})
|
||||||
httpkit.Write(writer, request, saveRoomBackgroundDataFromProto(resp), err)
|
httpkit.Write(writer, request, saveRoomBackgroundDataFromProto(resp), err)
|
||||||
}
|
}
|
||||||
@ -2845,11 +2824,6 @@ func roomAppearanceResourceFromProto(item *walletv1.Resource, entitlementID stri
|
|||||||
"entitlement_id": entitlementID,
|
"entitlement_id": entitlementID,
|
||||||
"expires_at_ms": expiresAtMS,
|
"expires_at_ms": expiresAtMS,
|
||||||
}
|
}
|
||||||
if format := resourceview.FormatFromMetadata(item.GetMetadataJson()); format != "" {
|
|
||||||
// 房间首屏/profile 是其他用户渲染麦位动效的权威快照,因此必须投影显式格式;
|
|
||||||
// 缺省时不猜 animation_url 后缀,避免 CDN 地址变化导致客户端选择错误 renderer。
|
|
||||||
resource["format"] = format
|
|
||||||
}
|
|
||||||
if item.GetResourceType() == "badge" {
|
if item.GetResourceType() == "badge" {
|
||||||
badgeForm, badgeKind, levelTrack := roomBadgeMetadataFields(item.GetMetadataJson())
|
badgeForm, badgeKind, levelTrack := roomBadgeMetadataFields(item.GetMetadataJson())
|
||||||
resource["badge_form"] = badgeForm
|
resource["badge_form"] = badgeForm
|
||||||
|
|||||||
@ -1,564 +0,0 @@
|
|||||||
package roomapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"image/jpeg"
|
|
||||||
"image/png"
|
|
||||||
"io"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
|
||||||
"hyapp/pkg/giftlimits"
|
|
||||||
"hyapp/pkg/roomid"
|
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
roomBackgroundUploadMaxBytes = int64(12 << 20)
|
|
||||||
roomImageUploadMaxBytes = int64(8 << 20)
|
|
||||||
roomUploadEnvelopeOverhead = int64(1 << 20)
|
|
||||||
roomUploadMaxFrames = int32(120)
|
|
||||||
roomUploadMaxDurationMS = int64(15_000)
|
|
||||||
)
|
|
||||||
|
|
||||||
type roomMediaData struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
ObjectKey string `json:"object_key"`
|
|
||||||
ContentType string `json:"content_type"`
|
|
||||||
Format string `json:"format"`
|
|
||||||
SizeBytes int64 `json:"size_bytes"`
|
|
||||||
Width int32 `json:"width"`
|
|
||||||
Height int32 `json:"height"`
|
|
||||||
Animated bool `json:"animated"`
|
|
||||||
FrameCount int32 `json:"frame_count"`
|
|
||||||
DurationMS int64 `json:"duration_ms"`
|
|
||||||
PosterURL string `json:"poster_url,omitempty"`
|
|
||||||
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
|
||||||
SHA256 string `json:"sha256"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type roomMediaUploadData struct {
|
|
||||||
Purpose string `json:"purpose"`
|
|
||||||
RoomID string `json:"room_id"`
|
|
||||||
CommandID string `json:"command_id"`
|
|
||||||
RoomVersion int64 `json:"room_version"`
|
|
||||||
Media roomMediaData `json:"media"`
|
|
||||||
ServerTimeMS int64 `json:"server_time_ms"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (data roomMediaData) proto() *roomv1.RoomMedia {
|
|
||||||
return &roomv1.RoomMedia{
|
|
||||||
Url: strings.TrimSpace(data.URL), ObjectKey: strings.TrimLeft(strings.TrimSpace(data.ObjectKey), "/"),
|
|
||||||
ContentType: strings.ToLower(strings.TrimSpace(data.ContentType)), Format: strings.ToLower(strings.TrimSpace(data.Format)),
|
|
||||||
SizeBytes: data.SizeBytes, Width: data.Width, Height: data.Height, Animated: data.Animated,
|
|
||||||
FrameCount: data.FrameCount, DurationMs: data.DurationMS, PosterUrl: strings.TrimSpace(data.PosterURL),
|
|
||||||
ThumbnailUrl: strings.TrimSpace(data.ThumbnailURL), Sha256: strings.ToLower(strings.TrimSpace(data.SHA256)),
|
|
||||||
Status: strings.ToLower(strings.TrimSpace(data.Status)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func roomMediaDataFromProto(media *roomv1.RoomMedia) *roomMediaData {
|
|
||||||
if media == nil || (strings.TrimSpace(media.GetUrl()) == "" && strings.TrimSpace(media.GetObjectKey()) == "") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &roomMediaData{
|
|
||||||
URL: media.GetUrl(), ObjectKey: media.GetObjectKey(), ContentType: media.GetContentType(), Format: media.GetFormat(),
|
|
||||||
SizeBytes: media.GetSizeBytes(), Width: media.GetWidth(), Height: media.GetHeight(), Animated: media.GetAnimated(),
|
|
||||||
FrameCount: media.GetFrameCount(), DurationMS: media.GetDurationMs(), PosterURL: media.GetPosterUrl(),
|
|
||||||
ThumbnailURL: media.GetThumbnailUrl(), SHA256: media.GetSha256(), Status: media.GetStatus(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) uploadRoomBackground(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
h.uploadRoomMedia(writer, request, "room_background", roomBackgroundUploadMaxBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) uploadRoomImage(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
h.uploadRoomMedia(writer, request, "room_image", roomImageUploadMaxBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) uploadRoomMedia(writer http.ResponseWriter, request *http.Request, purpose string, maxBytes int64) {
|
|
||||||
roomID := strings.TrimSpace(request.PathValue("room_id"))
|
|
||||||
if !roomid.ValidStringID(roomID) {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "room_id is invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if h.objectUploader == nil || h.objectURLVerifier == nil || h.roomMediaGuard == nil {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "room media upload is unavailable")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
request.Body = http.MaxBytesReader(writer, request.Body, maxBytes+roomUploadEnvelopeOverhead)
|
|
||||||
if err := request.ParseMultipartForm(maxBytes + roomUploadEnvelopeOverhead); err != nil {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusRequestEntityTooLarge, "FILE_TOO_LARGE", "file exceeds upload limit")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if request.MultipartForm != nil {
|
|
||||||
defer request.MultipartForm.RemoveAll()
|
|
||||||
}
|
|
||||||
commandID := strings.TrimSpace(request.FormValue("command_id"))
|
|
||||||
if !giftlimits.ValidCommandID(commandID) {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "command_id is invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
file, header, err := request.FormFile("file")
|
|
||||||
if err != nil {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "file is required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
content, err := io.ReadAll(io.LimitReader(file, maxBytes+1))
|
|
||||||
if err != nil || int64(len(content)) == 0 {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "file is invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if int64(len(content)) > maxBytes {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusRequestEntityTooLarge, "FILE_TOO_LARGE", "file exceeds upload limit")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
media, _, err := inspectRoomImage(content, header.Filename, header.Header.Get("Content-Type"))
|
|
||||||
if err != nil {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusUnsupportedMediaType, "UNSUPPORTED_MEDIA_TYPE", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := validateRoomUploadDimensions(purpose, media); err != nil {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, "INVALID_MEDIA_DIMENSIONS", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
authorization, err := h.roomMediaGuard.AuthorizeRoomMediaUpload(request.Context(), &roomv1.AuthorizeRoomMediaUploadRequest{
|
|
||||||
Meta: httpkit.RoomMeta(request, roomID, commandID), Purpose: purpose, Media: media.proto(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if authorization == nil || !authorization.GetAllowed() || strings.TrimSpace(authorization.GetObjectKeyPrefix()) == "" ||
|
|
||||||
strings.TrimSpace(authorization.GetObjectKey()) == "" {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "room media upload denied")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
objectKey := strings.TrimLeft(strings.TrimSpace(authorization.GetObjectKey()), "/")
|
|
||||||
objectURL, err := h.objectUploader.PutObject(request.Context(), objectKey, bytes.NewReader(content), int64(len(content)), media.ContentType)
|
|
||||||
if err != nil {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "object upload failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !h.objectURLVerifier.OwnsObjectURL(objectURL, objectKey) {
|
|
||||||
// 上传成功但返回地址不属于配置 access_url 时拒绝下发,避免把不可信 URL 固化进房间状态。
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "object upload url verification failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
media.URL, media.ObjectKey, media.Status = objectURL, objectKey, "active"
|
|
||||||
completed, err := h.roomMediaGuard.CompleteRoomMediaUpload(request.Context(), &roomv1.CompleteRoomMediaUploadRequest{
|
|
||||||
Meta: httpkit.RoomMeta(request, roomID, commandID), Purpose: purpose, Media: media.proto(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if completed == nil || !completed.GetActive() {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "room media upload completion failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completedMedia := roomMediaDataFromProto(completed.GetMedia())
|
|
||||||
if completedMedia == nil {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "room media upload completion failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
media = *completedMedia
|
|
||||||
httpkit.WriteOK(writer, request, roomMediaUploadData{
|
|
||||||
Purpose: purpose, RoomID: roomID, CommandID: commandID, RoomVersion: authorization.GetRoomVersion(),
|
|
||||||
Media: media, ServerTimeMS: completed.GetServerTimeMs(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func inspectRoomImage(content []byte, filename string, partContentType string) (roomMediaData, string, error) {
|
|
||||||
var data roomMediaData
|
|
||||||
var extension string
|
|
||||||
switch {
|
|
||||||
case len(content) >= 3 && content[0] == 0xff && content[1] == 0xd8 && content[2] == 0xff:
|
|
||||||
cfg, err := jpeg.DecodeConfig(bytes.NewReader(content))
|
|
||||||
if err != nil {
|
|
||||||
return data, "", fmt.Errorf("invalid jpeg")
|
|
||||||
}
|
|
||||||
data.Format, data.ContentType, data.Width, data.Height, data.FrameCount = "jpeg", "image/jpeg", int32(cfg.Width), int32(cfg.Height), 1
|
|
||||||
extension = "jpg"
|
|
||||||
case len(content) >= 8 && bytes.Equal(content[:8], []byte{137, 80, 78, 71, 13, 10, 26, 10}):
|
|
||||||
if pngHasAnimationChunk(content) {
|
|
||||||
return data, "", fmt.Errorf("animated png is not supported")
|
|
||||||
}
|
|
||||||
cfg, err := png.DecodeConfig(bytes.NewReader(content))
|
|
||||||
if err != nil {
|
|
||||||
return data, "", fmt.Errorf("invalid png")
|
|
||||||
}
|
|
||||||
data.Format, data.ContentType, data.Width, data.Height, data.FrameCount = "png", "image/png", int32(cfg.Width), int32(cfg.Height), 1
|
|
||||||
extension = "png"
|
|
||||||
case len(content) >= 6 && (string(content[:6]) == "GIF87a" || string(content[:6]) == "GIF89a"):
|
|
||||||
width, height, frames, durationMS, err := inspectGIF(content)
|
|
||||||
if err != nil {
|
|
||||||
return data, "", err
|
|
||||||
}
|
|
||||||
data.Format, data.ContentType, data.Width, data.Height = "gif", "image/gif", width, height
|
|
||||||
data.FrameCount, data.Animated, data.DurationMS = frames, frames > 1, durationMS
|
|
||||||
if !data.Animated {
|
|
||||||
data.DurationMS = 0
|
|
||||||
}
|
|
||||||
extension = "gif"
|
|
||||||
case len(content) >= 12 && string(content[:4]) == "RIFF" && string(content[8:12]) == "WEBP":
|
|
||||||
width, height, frames, durationMS, animated, err := inspectWebP(content)
|
|
||||||
if err != nil {
|
|
||||||
return data, "", err
|
|
||||||
}
|
|
||||||
data.Format, data.ContentType, data.Width, data.Height = "webp", "image/webp", width, height
|
|
||||||
data.FrameCount, data.DurationMS, data.Animated = frames, durationMS, animated
|
|
||||||
extension = "webp"
|
|
||||||
default:
|
|
||||||
return data, "", fmt.Errorf("only jpeg, png, gif and webp are supported")
|
|
||||||
}
|
|
||||||
declared, _, _ := mime.ParseMediaType(strings.TrimSpace(partContentType))
|
|
||||||
if !strings.EqualFold(declared, data.ContentType) {
|
|
||||||
return roomMediaData{}, "", fmt.Errorf("file content_type does not match signature")
|
|
||||||
}
|
|
||||||
fileExtension := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
|
|
||||||
validExtension := fileExtension == extension || (data.Format == "jpeg" && fileExtension == "jpeg")
|
|
||||||
if !validExtension {
|
|
||||||
return roomMediaData{}, "", fmt.Errorf("filename extension does not match signature")
|
|
||||||
}
|
|
||||||
sum := sha256.Sum256(content)
|
|
||||||
data.SizeBytes, data.SHA256, data.Status = int64(len(content)), hex.EncodeToString(sum[:]), "active"
|
|
||||||
return data, extension, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateRoomUploadDimensions(purpose string, media roomMediaData) error {
|
|
||||||
if media.Width <= 0 || media.Height <= 0 || media.FrameCount <= 0 {
|
|
||||||
return fmt.Errorf("image dimensions are invalid")
|
|
||||||
}
|
|
||||||
if media.Animated && (media.FrameCount > roomUploadMaxFrames || media.DurationMS <= 0 || media.DurationMS > roomUploadMaxDurationMS) {
|
|
||||||
return fmt.Errorf("animation exceeds frame or duration limit")
|
|
||||||
}
|
|
||||||
pixels := int64(media.Width) * int64(media.Height)
|
|
||||||
if purpose == "room_background" {
|
|
||||||
if media.Width > 2160 || media.Height > 3840 || pixels > 8_294_400 {
|
|
||||||
return fmt.Errorf("room background dimensions exceed limit")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if media.Width > 4096 || media.Height > 4096 || pixels > 16_777_216 {
|
|
||||||
return fmt.Errorf("room image dimensions exceed limit")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pngHasAnimationChunk(content []byte) bool {
|
|
||||||
for offset := 8; offset+12 <= len(content); {
|
|
||||||
size := int(binary.BigEndian.Uint32(content[offset : offset+4]))
|
|
||||||
if size < 0 || offset+12+size > len(content) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if string(content[offset+4:offset+8]) == "acTL" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
offset += 12 + size
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func inspectGIF(content []byte) (int32, int32, int32, int64, error) {
|
|
||||||
if len(content) < 13 || (string(content[:6]) != "GIF87a" && string(content[:6]) != "GIF89a") {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("invalid gif")
|
|
||||||
}
|
|
||||||
width := int32(binary.LittleEndian.Uint16(content[6:8]))
|
|
||||||
height := int32(binary.LittleEndian.Uint16(content[8:10]))
|
|
||||||
if width <= 0 || height <= 0 || width > 4096 || height > 4096 || int64(width)*int64(height) > 16_777_216 {
|
|
||||||
// 先检查逻辑画布,后续只扫描块边界,不按攻击者声明的尺寸分配像素缓冲区。
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("gif dimensions exceed limit")
|
|
||||||
}
|
|
||||||
|
|
||||||
offset := 13
|
|
||||||
if content[10]&0x80 != 0 {
|
|
||||||
colorTableBytes := 3 * (1 << (int(content[10]&0x07) + 1))
|
|
||||||
if offset+colorTableBytes > len(content) {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("invalid gif color table")
|
|
||||||
}
|
|
||||||
offset += colorTableBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
var frames int32
|
|
||||||
var durationMS int64
|
|
||||||
var pendingDelayMS int64
|
|
||||||
for offset < len(content) {
|
|
||||||
marker := content[offset]
|
|
||||||
offset++
|
|
||||||
switch marker {
|
|
||||||
case 0x3b: // trailer
|
|
||||||
if frames <= 0 {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("gif has no image frame")
|
|
||||||
}
|
|
||||||
if frames == 1 {
|
|
||||||
durationMS = 0
|
|
||||||
}
|
|
||||||
return width, height, frames, durationMS, nil
|
|
||||||
case 0x21: // extension
|
|
||||||
if offset >= len(content) {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("invalid gif extension")
|
|
||||||
}
|
|
||||||
label := content[offset]
|
|
||||||
offset++
|
|
||||||
if label == 0xf9 {
|
|
||||||
// Graphic Control Extension 固定为 4 字节数据和 0 终止符;delay 单位是 1/100 秒。
|
|
||||||
if offset+6 > len(content) || content[offset] != 4 || content[offset+5] != 0 {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("invalid gif graphic control extension")
|
|
||||||
}
|
|
||||||
pendingDelayMS = int64(binary.LittleEndian.Uint16(content[offset+2:offset+4])) * 10
|
|
||||||
offset += 6
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var skipErr error
|
|
||||||
offset, _, skipErr = skipGIFSubBlocks(content, offset)
|
|
||||||
if skipErr != nil {
|
|
||||||
return 0, 0, 0, 0, skipErr
|
|
||||||
}
|
|
||||||
case 0x2c: // image descriptor
|
|
||||||
if offset+9 > len(content) {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("invalid gif image descriptor")
|
|
||||||
}
|
|
||||||
left := int(binary.LittleEndian.Uint16(content[offset : offset+2]))
|
|
||||||
top := int(binary.LittleEndian.Uint16(content[offset+2 : offset+4]))
|
|
||||||
frameWidth := int(binary.LittleEndian.Uint16(content[offset+4 : offset+6]))
|
|
||||||
frameHeight := int(binary.LittleEndian.Uint16(content[offset+6 : offset+8]))
|
|
||||||
packed := content[offset+8]
|
|
||||||
if frameWidth <= 0 || frameHeight <= 0 || left+frameWidth > int(width) || top+frameHeight > int(height) {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("invalid gif frame dimensions")
|
|
||||||
}
|
|
||||||
offset += 9
|
|
||||||
if packed&0x80 != 0 {
|
|
||||||
colorTableBytes := 3 * (1 << (int(packed&0x07) + 1))
|
|
||||||
if offset+colorTableBytes > len(content) {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("invalid gif local color table")
|
|
||||||
}
|
|
||||||
offset += colorTableBytes
|
|
||||||
}
|
|
||||||
if offset >= len(content) || content[offset] < 2 || content[offset] > 8 {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("invalid gif lzw code size")
|
|
||||||
}
|
|
||||||
offset++
|
|
||||||
var hasImageData bool
|
|
||||||
var skipErr error
|
|
||||||
offset, hasImageData, skipErr = skipGIFSubBlocks(content, offset)
|
|
||||||
if skipErr != nil || !hasImageData {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("invalid gif image data")
|
|
||||||
}
|
|
||||||
frames++
|
|
||||||
durationMS += pendingDelayMS
|
|
||||||
pendingDelayMS = 0
|
|
||||||
if frames > roomUploadMaxFrames || durationMS > roomUploadMaxDurationMS {
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("gif animation exceeds frame or duration limit")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("invalid gif block marker")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, 0, 0, 0, fmt.Errorf("gif trailer is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipGIFSubBlocks(content []byte, offset int) (int, bool, error) {
|
|
||||||
hasData := false
|
|
||||||
for {
|
|
||||||
if offset >= len(content) {
|
|
||||||
return 0, false, fmt.Errorf("invalid gif sub-block")
|
|
||||||
}
|
|
||||||
blockSize := int(content[offset])
|
|
||||||
offset++
|
|
||||||
if blockSize == 0 {
|
|
||||||
return offset, hasData, nil
|
|
||||||
}
|
|
||||||
if offset+blockSize > len(content) {
|
|
||||||
return 0, false, fmt.Errorf("invalid gif sub-block")
|
|
||||||
}
|
|
||||||
hasData = true
|
|
||||||
offset += blockSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func inspectWebP(content []byte) (int32, int32, int32, int64, bool, error) {
|
|
||||||
if len(content) < 20 || string(content[:4]) != "RIFF" || string(content[8:12]) != "WEBP" {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid webp header")
|
|
||||||
}
|
|
||||||
declaredSize := uint64(binary.LittleEndian.Uint32(content[4:8])) + 8
|
|
||||||
if declaredSize != uint64(len(content)) {
|
|
||||||
// RIFF size 不匹配通常表示截断或尾部拼接;禁止只扫到某个看似合法的前缀就接受整个对象。
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid webp riff size")
|
|
||||||
}
|
|
||||||
|
|
||||||
var width, height int32
|
|
||||||
var frames int32
|
|
||||||
var durationMS int64
|
|
||||||
var animatedFlag, hasVP8X, hasANIM, hasStaticImage bool
|
|
||||||
for offset := 12; offset < len(content); {
|
|
||||||
if offset+8 > len(content) {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid webp trailing chunk")
|
|
||||||
}
|
|
||||||
chunkType := string(content[offset : offset+4])
|
|
||||||
size := uint64(binary.LittleEndian.Uint32(content[offset+4 : offset+8]))
|
|
||||||
start := uint64(offset + 8)
|
|
||||||
end := start + size
|
|
||||||
paddedEnd := end + size%2
|
|
||||||
if end < start || paddedEnd > uint64(len(content)) {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid webp chunk")
|
|
||||||
}
|
|
||||||
if size%2 == 1 && content[end] != 0 {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid webp chunk padding")
|
|
||||||
}
|
|
||||||
chunk := content[int(start):int(end)]
|
|
||||||
switch chunkType {
|
|
||||||
case "VP8X":
|
|
||||||
if hasVP8X || offset != 12 || len(chunk) != 10 || chunk[0]&0xc1 != 0 || chunk[1] != 0 || chunk[2] != 0 || chunk[3] != 0 {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid webp header")
|
|
||||||
}
|
|
||||||
hasVP8X = true
|
|
||||||
animatedFlag = chunk[0]&0x02 != 0
|
|
||||||
width = int32(readUint24LE(chunk[4:7])) + 1
|
|
||||||
height = int32(readUint24LE(chunk[7:10])) + 1
|
|
||||||
case "VP8 ", "VP8L":
|
|
||||||
if hasStaticImage {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("webp has multiple top-level image payloads")
|
|
||||||
}
|
|
||||||
if !hasVP8X && offset != 12 {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid simple webp chunk order")
|
|
||||||
}
|
|
||||||
imageWidth, imageHeight, err := inspectWebPImagePayload(chunkType, chunk)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, 0, 0, false, err
|
|
||||||
}
|
|
||||||
hasStaticImage = true
|
|
||||||
if width == 0 {
|
|
||||||
width, height = imageWidth, imageHeight
|
|
||||||
} else if imageWidth > width || imageHeight > height {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("webp image exceeds canvas")
|
|
||||||
}
|
|
||||||
case "ANIM":
|
|
||||||
if !hasVP8X || !animatedFlag || hasANIM || len(chunk) != 6 {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid webp animation header")
|
|
||||||
}
|
|
||||||
hasANIM = true
|
|
||||||
case "ANMF":
|
|
||||||
if !hasVP8X || !animatedFlag || !hasANIM || width <= 0 || height <= 0 || len(chunk) < 16 || chunk[15]&0xfc != 0 {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid webp animation frame")
|
|
||||||
}
|
|
||||||
frameX := int64(readUint24LE(chunk[0:3])) * 2
|
|
||||||
frameY := int64(readUint24LE(chunk[3:6])) * 2
|
|
||||||
frameWidth := int32(readUint24LE(chunk[6:9])) + 1
|
|
||||||
frameHeight := int32(readUint24LE(chunk[9:12])) + 1
|
|
||||||
if frameWidth <= 0 || frameHeight <= 0 || frameX+int64(frameWidth) > int64(width) || frameY+int64(frameHeight) > int64(height) {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid webp animation frame geometry")
|
|
||||||
}
|
|
||||||
if err := inspectWebPFramePayload(chunk[16:], frameWidth, frameHeight); err != nil {
|
|
||||||
return 0, 0, 0, 0, false, err
|
|
||||||
}
|
|
||||||
frames++
|
|
||||||
durationMS += int64(readUint24LE(chunk[12:15]))
|
|
||||||
if frames > roomUploadMaxFrames || durationMS > roomUploadMaxDurationMS {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("webp animation exceeds frame or duration limit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
offset = int(paddedEnd)
|
|
||||||
}
|
|
||||||
if width <= 0 || height <= 0 {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid webp dimensions")
|
|
||||||
}
|
|
||||||
if animatedFlag {
|
|
||||||
if !hasANIM || hasStaticImage || frames < 2 || durationMS <= 0 {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid animated webp")
|
|
||||||
}
|
|
||||||
return width, height, frames, durationMS, true, nil
|
|
||||||
}
|
|
||||||
if hasANIM || frames != 0 || !hasStaticImage {
|
|
||||||
return 0, 0, 0, 0, false, fmt.Errorf("invalid static webp")
|
|
||||||
}
|
|
||||||
return width, height, 1, 0, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func inspectWebPImagePayload(chunkType string, chunk []byte) (int32, int32, error) {
|
|
||||||
switch chunkType {
|
|
||||||
case "VP8 ":
|
|
||||||
// 10 字节只覆盖 lossy key-frame header;至少再有一个压缩 payload 字节,防止伪造尺寸头通过。
|
|
||||||
if len(chunk) <= 10 || chunk[0]&0x01 != 0 || !bytes.Equal(chunk[3:6], []byte{0x9d, 0x01, 0x2a}) {
|
|
||||||
return 0, 0, fmt.Errorf("invalid webp vp8 payload")
|
|
||||||
}
|
|
||||||
width := int32(binary.LittleEndian.Uint16(chunk[6:8]) & 0x3fff)
|
|
||||||
height := int32(binary.LittleEndian.Uint16(chunk[8:10]) & 0x3fff)
|
|
||||||
if width <= 0 || height <= 0 {
|
|
||||||
return 0, 0, fmt.Errorf("invalid webp vp8 dimensions")
|
|
||||||
}
|
|
||||||
return width, height, nil
|
|
||||||
case "VP8L":
|
|
||||||
// VP8L signature+尺寸头为 5 字节;version 位必须为 0,并要求存在实际压缩 payload。
|
|
||||||
if len(chunk) <= 5 || chunk[0] != 0x2f {
|
|
||||||
return 0, 0, fmt.Errorf("invalid webp vp8l payload")
|
|
||||||
}
|
|
||||||
bits := binary.LittleEndian.Uint32(chunk[1:5])
|
|
||||||
if bits>>29 != 0 {
|
|
||||||
return 0, 0, fmt.Errorf("invalid webp vp8l version")
|
|
||||||
}
|
|
||||||
return int32(bits&0x3fff) + 1, int32((bits>>14)&0x3fff) + 1, nil
|
|
||||||
default:
|
|
||||||
return 0, 0, fmt.Errorf("unsupported webp image payload")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func inspectWebPFramePayload(content []byte, expectedWidth int32, expectedHeight int32) error {
|
|
||||||
if len(content) < 8 {
|
|
||||||
return fmt.Errorf("webp animation frame has no image payload")
|
|
||||||
}
|
|
||||||
foundImage := false
|
|
||||||
for offset := 0; offset < len(content); {
|
|
||||||
if offset+8 > len(content) {
|
|
||||||
return fmt.Errorf("invalid webp animation frame chunk")
|
|
||||||
}
|
|
||||||
chunkType := string(content[offset : offset+4])
|
|
||||||
size := uint64(binary.LittleEndian.Uint32(content[offset+4 : offset+8]))
|
|
||||||
start := uint64(offset + 8)
|
|
||||||
end := start + size
|
|
||||||
paddedEnd := end + size%2
|
|
||||||
if end < start || paddedEnd > uint64(len(content)) {
|
|
||||||
return fmt.Errorf("invalid webp animation frame chunk")
|
|
||||||
}
|
|
||||||
if size%2 == 1 && content[end] != 0 {
|
|
||||||
return fmt.Errorf("invalid webp animation frame padding")
|
|
||||||
}
|
|
||||||
chunk := content[int(start):int(end)]
|
|
||||||
switch chunkType {
|
|
||||||
case "ALPH":
|
|
||||||
if foundImage || len(chunk) == 0 {
|
|
||||||
return fmt.Errorf("invalid webp animation alpha payload")
|
|
||||||
}
|
|
||||||
case "VP8 ", "VP8L":
|
|
||||||
if foundImage {
|
|
||||||
return fmt.Errorf("webp animation frame has multiple image payloads")
|
|
||||||
}
|
|
||||||
width, height, err := inspectWebPImagePayload(chunkType, chunk)
|
|
||||||
if err != nil || width != expectedWidth || height != expectedHeight {
|
|
||||||
return fmt.Errorf("webp animation image dimensions mismatch")
|
|
||||||
}
|
|
||||||
foundImage = true
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported webp animation frame chunk")
|
|
||||||
}
|
|
||||||
offset = int(paddedEnd)
|
|
||||||
}
|
|
||||||
if !foundImage {
|
|
||||||
return fmt.Errorf("webp animation frame has no image payload")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readUint24LE(value []byte) uint32 {
|
|
||||||
return uint32(value[0]) | uint32(value[1])<<8 | uint32(value[2])<<16
|
|
||||||
}
|
|
||||||
@ -52,8 +52,6 @@ type roomListItemData struct {
|
|||||||
RoomShortID string `json:"room_short_id,omitempty"`
|
RoomShortID string `json:"room_short_id,omitempty"`
|
||||||
CountryCode string `json:"country_code,omitempty"`
|
CountryCode string `json:"country_code,omitempty"`
|
||||||
CountryFlag string `json:"country_flag,omitempty"`
|
CountryFlag string `json:"country_flag,omitempty"`
|
||||||
RoomBorder *roomDecorationData `json:"room_border,omitempty"`
|
|
||||||
RoomNameStyle *roomDecorationData `json:"room_name_style,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type createRoomData struct {
|
type createRoomData struct {
|
||||||
@ -386,8 +384,6 @@ type roomBackgroundData struct {
|
|||||||
CreatedByUserID string `json:"created_by_user_id,omitempty"`
|
CreatedByUserID string `json:"created_by_user_id,omitempty"`
|
||||||
CreatedAtMS int64 `json:"created_at_ms"`
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
CommandID string `json:"command_id,omitempty"`
|
|
||||||
Media *roomMediaData `json:"media,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type saveRoomBackgroundData struct {
|
type saveRoomBackgroundData struct {
|
||||||
@ -398,7 +394,6 @@ type saveRoomBackgroundData struct {
|
|||||||
type listRoomBackgroundsData struct {
|
type listRoomBackgroundsData struct {
|
||||||
Backgrounds []roomBackgroundData `json:"backgrounds"`
|
Backgrounds []roomBackgroundData `json:"backgrounds"`
|
||||||
SelectedBackgroundURL string `json:"selected_background_url,omitempty"`
|
SelectedBackgroundURL string `json:"selected_background_url,omitempty"`
|
||||||
SelectedBackground *roomBackgroundData `json:"selected_background,omitempty"`
|
|
||||||
ServerTimeMS int64 `json:"server_time_ms"`
|
ServerTimeMS int64 `json:"server_time_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -449,9 +444,6 @@ type roomInitialData struct {
|
|||||||
OccupiedSeatCount int32 `json:"occupied_seat_count"`
|
OccupiedSeatCount int32 `json:"occupied_seat_count"`
|
||||||
VisibleRegionID int64 `json:"visible_region_id,omitempty"`
|
VisibleRegionID int64 `json:"visible_region_id,omitempty"`
|
||||||
Version int64 `json:"version"`
|
Version int64 `json:"version"`
|
||||||
ActiveBackground *roomBackgroundData `json:"active_background,omitempty"`
|
|
||||||
RoomBorder *roomDecorationData `json:"room_border,omitempty"`
|
|
||||||
RoomNameStyle *roomDecorationData `json:"room_name_style,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type roomViewerData struct {
|
type roomViewerData struct {
|
||||||
@ -752,8 +744,6 @@ func roomListItemDataFromProto(room *roomv1.RoomListItem) roomListItemData {
|
|||||||
RoomShortID: room.GetRoomShortId(),
|
RoomShortID: room.GetRoomShortId(),
|
||||||
CountryCode: room.GetCountryCode(),
|
CountryCode: room.GetCountryCode(),
|
||||||
CountryFlag: roomProfileCountryFlag(room.GetCountryCode()),
|
CountryFlag: roomProfileCountryFlag(room.GetCountryCode()),
|
||||||
RoomBorder: roomDecorationDataFromProto(room.GetRoomBorder()),
|
|
||||||
RoomNameStyle: roomDecorationDataFromProto(room.GetRoomNameStyle()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -802,7 +792,6 @@ func listRoomBackgroundsDataFromProto(resp *roomv1.ListRoomBackgroundsResponse)
|
|||||||
return listRoomBackgroundsData{
|
return listRoomBackgroundsData{
|
||||||
Backgrounds: backgrounds,
|
Backgrounds: backgrounds,
|
||||||
SelectedBackgroundURL: resp.GetSelectedBackgroundUrl(),
|
SelectedBackgroundURL: resp.GetSelectedBackgroundUrl(),
|
||||||
SelectedBackground: roomBackgroundDataPointerFromProto(resp.GetSelectedBackground()),
|
|
||||||
ServerTimeMS: resp.GetServerTimeMs(),
|
ServerTimeMS: resp.GetServerTimeMs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -831,19 +820,9 @@ func roomBackgroundDataFromProto(background *roomv1.RoomBackgroundImage) roomBac
|
|||||||
CreatedByUserID: formatOptionalUserID(background.GetCreatedByUserId()),
|
CreatedByUserID: formatOptionalUserID(background.GetCreatedByUserId()),
|
||||||
CreatedAtMS: background.GetCreatedAtMs(),
|
CreatedAtMS: background.GetCreatedAtMs(),
|
||||||
UpdatedAtMS: background.GetUpdatedAtMs(),
|
UpdatedAtMS: background.GetUpdatedAtMs(),
|
||||||
CommandID: background.GetCommandId(),
|
|
||||||
Media: roomMediaDataFromProto(background.GetMedia()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func roomBackgroundDataPointerFromProto(background *roomv1.RoomBackgroundImage) *roomBackgroundData {
|
|
||||||
if background == nil || background.GetBackgroundId() <= 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
data := roomBackgroundDataFromProto(background)
|
|
||||||
return &data
|
|
||||||
}
|
|
||||||
|
|
||||||
func roomSnapshotDataFromProto(resp *roomv1.GetRoomSnapshotResponse) roomSnapshotData {
|
func roomSnapshotDataFromProto(resp *roomv1.GetRoomSnapshotResponse) roomSnapshotData {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
return roomSnapshotData{}
|
return roomSnapshotData{}
|
||||||
@ -895,9 +874,6 @@ func roomInitialRoomDataFromSnapshot(snapshot *roomv1.RoomSnapshot, roomID strin
|
|||||||
OccupiedSeatCount: occupiedSeatCount,
|
OccupiedSeatCount: occupiedSeatCount,
|
||||||
VisibleRegionID: snapshot.GetVisibleRegionId(),
|
VisibleRegionID: snapshot.GetVisibleRegionId(),
|
||||||
Version: snapshot.GetVersion(),
|
Version: snapshot.GetVersion(),
|
||||||
ActiveBackground: roomBackgroundDataPointerFromProto(snapshot.GetActiveBackground()),
|
|
||||||
RoomBorder: roomDecorationDataFromProto(snapshot.GetRoomBorder()),
|
|
||||||
RoomNameStyle: roomDecorationDataFromProto(snapshot.GetRoomNameStyle()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,134 +0,0 @@
|
|||||||
package roomapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
|
||||||
"hyapp/pkg/giftlimits"
|
|
||||||
"hyapp/pkg/roomid"
|
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
|
||||||
)
|
|
||||||
|
|
||||||
type roomDecorationData struct {
|
|
||||||
ResourceID int64 `json:"resource_id"`
|
|
||||||
ResourceCode string `json:"resource_code"`
|
|
||||||
ResourceType string `json:"resource_type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
AssetURL string `json:"asset_url"`
|
|
||||||
PreviewURL string `json:"preview_url,omitempty"`
|
|
||||||
AnimationURL string `json:"animation_url,omitempty"`
|
|
||||||
Format string `json:"format,omitempty"`
|
|
||||||
MetadataJSON string `json:"metadata_json,omitempty"`
|
|
||||||
EntitlementID string `json:"entitlement_id"`
|
|
||||||
TextColors []string `json:"text_colors,omitempty"`
|
|
||||||
Stops []float64 `json:"stops,omitempty"`
|
|
||||||
AngleDegrees float64 `json:"angle_degrees,omitempty"`
|
|
||||||
ExpiresAtMS int64 `json:"expires_at_ms,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type applyRoomDecorationData struct {
|
|
||||||
Result roomCommandResultData `json:"result"`
|
|
||||||
Room roomInitialData `json:"room"`
|
|
||||||
DecorationType string `json:"decoration_type"`
|
|
||||||
Resource *roomDecorationData `json:"resource,omitempty"`
|
|
||||||
ServerTimeMS int64 `json:"server_time_ms"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type sendRoomImageData struct {
|
|
||||||
Result roomCommandResultData `json:"result"`
|
|
||||||
Accepted bool `json:"accepted"`
|
|
||||||
MessageID string `json:"message_id"`
|
|
||||||
RoomID string `json:"room_id"`
|
|
||||||
SenderUserID string `json:"sender_user_id"`
|
|
||||||
ServerTimeMS int64 `json:"server_time_ms"`
|
|
||||||
Image *roomMediaData `json:"image,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) applyRoomDecoration(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
if h.roomVIPClient == nil {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "room vip feature is unavailable")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
roomID := strings.TrimSpace(request.PathValue("room_id"))
|
|
||||||
var body struct {
|
|
||||||
CommandID string `json:"command_id"`
|
|
||||||
DecorationType string `json:"decoration_type"`
|
|
||||||
ResourceID flexibleInt64 `json:"resource_id"`
|
|
||||||
EntitlementID string `json:"entitlement_id"`
|
|
||||||
}
|
|
||||||
if !httpkit.Decode(writer, request, &body) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
body.CommandID = strings.TrimSpace(body.CommandID)
|
|
||||||
body.DecorationType = strings.ToLower(strings.TrimSpace(body.DecorationType))
|
|
||||||
body.EntitlementID = strings.TrimSpace(body.EntitlementID)
|
|
||||||
if !roomid.ValidStringID(roomID) || !giftlimits.ValidCommandID(body.CommandID) || int64(body.ResourceID) <= 0 {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.roomVIPClient.ApplyRoomDecoration(request.Context(), &roomv1.ApplyRoomDecorationRequest{
|
|
||||||
Meta: httpkit.RoomMeta(request, roomID, body.CommandID), DecorationType: body.DecorationType,
|
|
||||||
ResourceId: int64(body.ResourceID), EntitlementId: body.EntitlementID,
|
|
||||||
})
|
|
||||||
httpkit.Write(writer, request, applyRoomDecorationDataFromProto(resp), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) sendRoomImage(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
if h.roomVIPClient == nil || h.objectURLVerifier == nil {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "room image message is unavailable")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
roomID := strings.TrimSpace(request.PathValue("room_id"))
|
|
||||||
var body struct {
|
|
||||||
CommandID string `json:"command_id"`
|
|
||||||
Image roomMediaData `json:"image"`
|
|
||||||
}
|
|
||||||
if !httpkit.Decode(writer, request, &body) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
body.CommandID = strings.TrimSpace(body.CommandID)
|
|
||||||
if !roomid.ValidStringID(roomID) || !giftlimits.ValidCommandID(body.CommandID) ||
|
|
||||||
!h.objectURLVerifier.OwnsObjectURL(body.Image.URL, body.Image.ObjectKey) {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid room image")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.roomVIPClient.SendRoomImage(request.Context(), &roomv1.SendRoomImageRequest{
|
|
||||||
Meta: httpkit.RoomMeta(request, roomID, body.CommandID), Image: body.Image.proto(),
|
|
||||||
})
|
|
||||||
httpkit.Write(writer, request, sendRoomImageDataFromProto(resp), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyRoomDecorationDataFromProto(resp *roomv1.ApplyRoomDecorationResponse) applyRoomDecorationData {
|
|
||||||
if resp == nil {
|
|
||||||
return applyRoomDecorationData{}
|
|
||||||
}
|
|
||||||
return applyRoomDecorationData{
|
|
||||||
Result: commandResultDataFromProto(resp.GetResult()), Room: roomInitialRoomDataFromSnapshot(resp.GetRoom(), resp.GetRoom().GetRoomId()),
|
|
||||||
DecorationType: resp.GetDecorationType(), Resource: roomDecorationDataFromProto(resp.GetResource()), ServerTimeMS: resp.GetServerTimeMs(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendRoomImageDataFromProto(resp *roomv1.SendRoomImageResponse) sendRoomImageData {
|
|
||||||
if resp == nil {
|
|
||||||
return sendRoomImageData{}
|
|
||||||
}
|
|
||||||
return sendRoomImageData{
|
|
||||||
Result: commandResultDataFromProto(resp.GetResult()), Accepted: resp.GetAccepted(), MessageID: resp.GetMessageId(),
|
|
||||||
RoomID: resp.GetRoomId(), SenderUserID: formatOptionalUserID(resp.GetSenderUserId()),
|
|
||||||
ServerTimeMS: resp.GetServerTimeMs(), Image: roomMediaDataFromProto(resp.GetImage()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func roomDecorationDataFromProto(resource *roomv1.RoomDecorationResource) *roomDecorationData {
|
|
||||||
if resource == nil || resource.GetResourceId() <= 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &roomDecorationData{
|
|
||||||
ResourceID: resource.GetResourceId(), ResourceCode: resource.GetResourceCode(), ResourceType: resource.GetResourceType(),
|
|
||||||
Name: resource.GetName(), AssetURL: resource.GetAssetUrl(), PreviewURL: resource.GetPreviewUrl(), AnimationURL: resource.GetAnimationUrl(),
|
|
||||||
Format: resource.GetFormat(), MetadataJSON: resource.GetMetadataJson(), EntitlementID: resource.GetEntitlementId(),
|
|
||||||
TextColors: append([]string(nil), resource.GetTextColors()...), Stops: append([]float64(nil), resource.GetStops()...),
|
|
||||||
AngleDegrees: resource.GetAngleDegrees(), ExpiresAtMS: resource.GetExpiresAtMs(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,146 +0,0 @@
|
|||||||
package roomapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxUserVoiceRoomPresenceBatchSize = 100
|
|
||||||
codeInvalidUserIDs = "INVALID_USER_IDS"
|
|
||||||
codeVoiceRoomPresenceUnavailable = "VOICE_ROOM_PRESENCE_UNAVAILABLE"
|
|
||||||
)
|
|
||||||
|
|
||||||
// userVoiceRoomPresenceQueryClient 是个人资料页独占的窄查询能力。
|
|
||||||
// 生产 grpcRoomQueryClient 实现该接口;其他只使用房间列表的调用方无需为新 RPC 扩大 mock 面。
|
|
||||||
type userVoiceRoomPresenceQueryClient interface {
|
|
||||||
BatchGetUserVoiceRoomPresences(context.Context, *roomv1.BatchGetUserVoiceRoomPresencesRequest) (*roomv1.BatchGetUserVoiceRoomPresencesResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type userVoiceRoomPresenceBatchData struct {
|
|
||||||
Items []userVoiceRoomPresenceData `json:"items"`
|
|
||||||
ServerTimeMS int64 `json:"server_time_ms"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userVoiceRoomPresenceData struct {
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
State string `json:"state"`
|
|
||||||
SeatNo int32 `json:"seat_no"`
|
|
||||||
Joinable bool `json:"joinable"`
|
|
||||||
ObservedAtMS int64 `json:"observed_at_ms"`
|
|
||||||
Room *userVoiceRoomPresenceRoomData `json:"room"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type userVoiceRoomPresenceRoomData struct {
|
|
||||||
RoomID string `json:"room_id"`
|
|
||||||
IMGroupID string `json:"im_group_id"`
|
|
||||||
RTCRoomID string `json:"rtc_room_id"`
|
|
||||||
RoomShortID string `json:"room_short_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
OwnerUserID string `json:"owner_user_id"`
|
|
||||||
HostUserID string `json:"host_user_id"`
|
|
||||||
Mode string `json:"mode"`
|
|
||||||
CoverURL string `json:"cover_url"`
|
|
||||||
BackgroundURL string `json:"background_url"`
|
|
||||||
OnlineCount int32 `json:"online_count"`
|
|
||||||
SeatCount int32 `json:"seat_count"`
|
|
||||||
Locked bool `json:"locked"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// batchUserVoiceRoomPresences 为个人资料页返回当前登录用户可见的 Live 入口。
|
|
||||||
// 客户端只提交目标 user_ids;viewer_user_id 和 app_code 必须来自已验签请求上下文。
|
|
||||||
func (h *Handler) batchUserVoiceRoomPresences(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
queryClient, ok := h.roomQueryClient.(userVoiceRoomPresenceQueryClient)
|
|
||||||
if !ok || queryClient == nil {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusServiceUnavailable, codeVoiceRoomPresenceUnavailable, "voice room presence unavailable")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userIDs, ok := parseUserVoiceRoomPresenceUserIDs(request)
|
|
||||||
if !ok {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, codeInvalidUserIDs, "invalid user ids")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := queryClient.BatchGetUserVoiceRoomPresences(request.Context(), &roomv1.BatchGetUserVoiceRoomPresencesRequest{
|
|
||||||
Meta: httpkit.RoomMeta(request, "", ""),
|
|
||||||
ViewerUserId: auth.UserIDFromContext(request.Context()),
|
|
||||||
UserIds: userIDs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
// 单个未上麦或不可见用户由 room-service 返回 not_on_mic;RPC 失败代表整批实时事实不可用,客户端必须隐藏 Live。
|
|
||||||
httpkit.WriteError(writer, request, http.StatusServiceUnavailable, codeVoiceRoomPresenceUnavailable, "voice room presence unavailable")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
httpkit.WriteOK(writer, request, userVoiceRoomPresenceBatchDataFromProto(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseUserVoiceRoomPresenceUserIDs(request *http.Request) ([]int64, bool) {
|
|
||||||
rawValues, exists := request.URL.Query()["user_ids"]
|
|
||||||
if !exists || len(rawValues) == 0 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
seen := make(map[int64]struct{})
|
|
||||||
result := make([]int64, 0, len(rawValues))
|
|
||||||
for _, raw := range rawValues {
|
|
||||||
if strings.TrimSpace(raw) == "" {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
for piece := range strings.SplitSeq(raw, ",") {
|
|
||||||
piece = strings.TrimSpace(piece)
|
|
||||||
userID, err := strconv.ParseInt(piece, 10, 64)
|
|
||||||
if piece == "" || err != nil || userID <= 0 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if _, duplicate := seen[userID]; duplicate {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[userID] = struct{}{}
|
|
||||||
result = append(result, userID)
|
|
||||||
if len(result) > maxUserVoiceRoomPresenceBatchSize {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, len(result) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func userVoiceRoomPresenceBatchDataFromProto(resp *roomv1.BatchGetUserVoiceRoomPresencesResponse) userVoiceRoomPresenceBatchData {
|
|
||||||
if resp == nil {
|
|
||||||
return userVoiceRoomPresenceBatchData{Items: []userVoiceRoomPresenceData{}}
|
|
||||||
}
|
|
||||||
items := make([]userVoiceRoomPresenceData, 0, len(resp.GetItems()))
|
|
||||||
for _, item := range resp.GetItems() {
|
|
||||||
view := userVoiceRoomPresenceData{
|
|
||||||
UserID: httpkit.UserIDString(item.GetUserId()),
|
|
||||||
State: item.GetState(),
|
|
||||||
SeatNo: item.GetSeatNo(),
|
|
||||||
Joinable: item.GetJoinable(),
|
|
||||||
ObservedAtMS: item.GetObservedAtMs(),
|
|
||||||
}
|
|
||||||
if room := item.GetRoom(); room != nil {
|
|
||||||
view.Room = &userVoiceRoomPresenceRoomData{
|
|
||||||
RoomID: room.GetRoomId(),
|
|
||||||
IMGroupID: roomIMGroupID(room.GetRoomId()),
|
|
||||||
RTCRoomID: room.GetRoomId(),
|
|
||||||
RoomShortID: room.GetRoomShortId(),
|
|
||||||
Title: room.GetTitle(),
|
|
||||||
OwnerUserID: httpkit.UserIDString(room.GetOwnerUserId()),
|
|
||||||
HostUserID: httpkit.UserIDString(room.GetHostUserId()),
|
|
||||||
Mode: room.GetMode(),
|
|
||||||
CoverURL: room.GetCoverUrl(),
|
|
||||||
BackgroundURL: room.GetBackgroundUrl(),
|
|
||||||
OnlineCount: room.GetOnlineCount(),
|
|
||||||
SeatCount: room.GetSeatCount(),
|
|
||||||
Locked: room.GetLocked(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items = append(items, view)
|
|
||||||
}
|
|
||||||
return userVoiceRoomPresenceBatchData{Items: items, ServerTimeMS: resp.GetServerTimeMs()}
|
|
||||||
}
|
|
||||||
@ -41,7 +41,6 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
|||||||
GiftMaxTargets: h.giftMaxTargets,
|
GiftMaxTargets: h.giftMaxTargets,
|
||||||
GiftMaxCount: h.giftMaxCount,
|
GiftMaxCount: h.giftMaxCount,
|
||||||
GiftMaxUnits: h.giftMaxUnits,
|
GiftMaxUnits: h.giftMaxUnits,
|
||||||
ObjectUploader: h.objectUploader,
|
|
||||||
RTCTokenConfig: tencentrtc.TokenConfig{
|
RTCTokenConfig: tencentrtc.TokenConfig{
|
||||||
Enabled: h.tencentRTC.Enabled,
|
Enabled: h.tencentRTC.Enabled,
|
||||||
SDKAppID: h.tencentRTC.SDKAppID,
|
SDKAppID: h.tencentRTC.SDKAppID,
|
||||||
@ -154,7 +153,6 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
|||||||
userHandlers := userAPI.UserHandlers()
|
userHandlers := userAPI.UserHandlers()
|
||||||
userHandlers.BatchUserProfiles = messageAPI.BatchUserProfiles
|
userHandlers.BatchUserProfiles = messageAPI.BatchUserProfiles
|
||||||
userHandlers.BatchRoomDisplayProfiles = roomAPI.BatchRoomDisplayProfiles
|
userHandlers.BatchRoomDisplayProfiles = roomAPI.BatchRoomDisplayProfiles
|
||||||
userHandlers.BatchUserVoiceRoomPresences = roomAPI.BatchUserVoiceRoomPresences
|
|
||||||
userHandlers.GetMyGiftWall = walletAPI.GetMyGiftWall
|
userHandlers.GetMyGiftWall = walletAPI.GetMyGiftWall
|
||||||
userHandlers.ListMyResources = resourceAPI.ListMyResources
|
userHandlers.ListMyResources = resourceAPI.ListMyResources
|
||||||
userHandlers.EquipMyResource = resourceAPI.EquipMyResource
|
userHandlers.EquipMyResource = resourceAPI.EquipMyResource
|
||||||
|
|||||||
@ -126,7 +126,6 @@ func (h *Handler) getAgencyCenterPlatformPolicy(writer http.ResponseWriter, requ
|
|||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
RegionId: agency.GetRegionId(),
|
RegionId: agency.GetRegionId(),
|
||||||
SettlementTriggerMode: "",
|
SettlementTriggerMode: "",
|
||||||
CycleKey: time.Now().UTC().Format("2006-01"),
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import (
|
|||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"hyapp/services/gateway-service/internal/transport/http/resourceview"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -384,11 +383,6 @@ func appearanceResourceFromProto(item *walletv1.Resource, entitlementID string,
|
|||||||
"entitlement_id": entitlementID,
|
"entitlement_id": entitlementID,
|
||||||
"expires_at_ms": expiresAtMS,
|
"expires_at_ms": expiresAtMS,
|
||||||
}
|
}
|
||||||
if format := resourceview.FormatFromMetadata(item.GetMetadataJson()); format != "" {
|
|
||||||
// 佩戴后的 appearance 仍要携带与资源列表相同的显式 renderer;Flutter 不解析 URL 后缀,
|
|
||||||
// 缺少后台格式配置时保持字段省略,让客户端明确走“不支持”分支。
|
|
||||||
resource["format"] = format
|
|
||||||
}
|
|
||||||
if item.GetResourceType() == appearanceResourceBadge {
|
if item.GetResourceType() == appearanceResourceBadge {
|
||||||
badgeForm, badgeKind, levelTrack := appearanceBadgeMetadataFields(item.GetMetadataJson())
|
badgeForm, badgeKind, levelTrack := appearanceBadgeMetadataFields(item.GetMetadataJson())
|
||||||
resource["badge_form"] = badgeForm
|
resource["badge_form"] = badgeForm
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package userapi
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
|
||||||
|
|
||||||
userv1 "hyapp.local/api/proto/user/v1"
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
@ -16,8 +15,6 @@ const hostProfileStatusActive = "active"
|
|||||||
|
|
||||||
type hostCenterPolicyData struct {
|
type hostCenterPolicyData struct {
|
||||||
PolicyID string `json:"policy_id"`
|
PolicyID string `json:"policy_id"`
|
||||||
CycleKey string `json:"cycle_key"`
|
|
||||||
PolicyVersion uint64 `json:"policy_version"`
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
RegionID int64 `json:"region_id"`
|
RegionID int64 `json:"region_id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@ -71,13 +68,11 @@ func (h *Handler) getHostCenterPlatformPolicy(writer http.ResponseWriter, reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cycleKey := time.Now().UTC().Format("2006-01")
|
// H5 不传 region、policy id 或 trigger;gateway 用当前主播身份区域读取 active 政策,让 automatic/manual 都能展示。
|
||||||
// 政策和进度共用同一个 UTC 周期锚点,避免月界瞬间两个调用读取到不同月份。
|
|
||||||
resp, err := h.walletClient.GetActiveHostSalaryPolicy(request.Context(), &walletv1.GetActiveHostSalaryPolicyRequest{
|
resp, err := h.walletClient.GetActiveHostSalaryPolicy(request.Context(), &walletv1.GetActiveHostSalaryPolicyRequest{
|
||||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
RegionId: profile.GetRegionId(),
|
RegionId: profile.GetRegionId(),
|
||||||
CycleKey: cycleKey,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
@ -89,7 +84,6 @@ func (h *Handler) getHostCenterPlatformPolicy(writer http.ResponseWriter, reques
|
|||||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
HostUserId: profile.GetUserId(),
|
HostUserId: profile.GetUserId(),
|
||||||
CycleKey: cycleKey,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
@ -138,8 +132,6 @@ func hostCenterPolicyFromProto(policy *walletv1.HostSalaryPolicy) *hostCenterPol
|
|||||||
}
|
}
|
||||||
return &hostCenterPolicyData{
|
return &hostCenterPolicyData{
|
||||||
PolicyID: strconv.FormatUint(policy.GetPolicyId(), 10),
|
PolicyID: strconv.FormatUint(policy.GetPolicyId(), 10),
|
||||||
CycleKey: policy.GetCycleKey(),
|
|
||||||
PolicyVersion: policy.GetPolicyVersion(),
|
|
||||||
Name: policy.GetName(),
|
Name: policy.GetName(),
|
||||||
RegionID: policy.GetRegionId(),
|
RegionID: policy.GetRegionId(),
|
||||||
Status: policy.GetStatus(),
|
Status: policy.GetStatus(),
|
||||||
|
|||||||
@ -1,23 +1,16 @@
|
|||||||
package userapi
|
package userapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
userv1 "hyapp.local/api/proto/user/v1"
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
"hyapp/services/gateway-service/internal/auth"
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
profileVisitorPageMax int32 = 100
|
|
||||||
profileVisitorPageSizeMax int32 = 100
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type profileVisitRecordData struct {
|
type profileVisitRecordData struct {
|
||||||
VisitorUserID string `json:"visitor_user_id,omitempty"`
|
VisitorUserID string `json:"visitor_user_id"`
|
||||||
AnonymousVisitID string `json:"anonymous_visit_id,omitempty"`
|
|
||||||
VisitorIdentityHidden bool `json:"visitor_identity_hidden"`
|
|
||||||
TargetUserID string `json:"target_user_id"`
|
TargetUserID string `json:"target_user_id"`
|
||||||
VisitCount int64 `json:"visit_count"`
|
VisitCount int64 `json:"visit_count"`
|
||||||
LastVisitedAtMS int64 `json:"last_visited_at_ms"`
|
LastVisitedAtMS int64 `json:"last_visited_at_ms"`
|
||||||
@ -54,18 +47,6 @@ func (h *Handler) listMyProfileVisitors(writer http.ResponseWriter, request *htt
|
|||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if page > profileVisitorPageMax {
|
|
||||||
// 当前 page 协议需要从公开、匿名两个有序分支读取到目标 offset 后再合并;限制页码可把
|
|
||||||
// 单次读取窗口硬限制在 10,000 条,避免客户端构造高页把 user-service 内存变成无界缓存。
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "page must be less than or equal to 100")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if pageSize > profileVisitorPageSizeMax {
|
|
||||||
// 访客列表的页宽是对外契约;网关必须在进入 gRPC 前拒绝超限请求,
|
|
||||||
// 不依赖 user-service 静默截断,否则 Flutter 无法区分参数错误与服务端分页。
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "page_size must be less than or equal to 100")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.userSocialClient.ListProfileVisitors(request.Context(), &userv1.ListProfileVisitorsRequest{
|
resp, err := h.userSocialClient.ListProfileVisitors(request.Context(), &userv1.ListProfileVisitorsRequest{
|
||||||
Meta: httpkit.UserMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
UserId: auth.UserIDFromContext(request.Context()),
|
||||||
@ -211,14 +192,7 @@ func (h *Handler) recordProfileVisit(writer http.ResponseWriter, request *http.R
|
|||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data := map[string]any{
|
data := map[string]any{"recorded": resp.GetRecorded(), "target_stats": overviewStatsFromProto(resp.GetTargetStats())}
|
||||||
"recorded": resp.GetRecorded(),
|
|
||||||
"target_stats_hidden": resp.GetTargetStatsHidden(),
|
|
||||||
}
|
|
||||||
if !resp.GetTargetStatsHidden() {
|
|
||||||
// 隐藏标志为 true 时不写 target_stats key;不能把 nil 转成零值 DTO,否则客户端会把伪造的 0 当成真实统计。
|
|
||||||
data["target_stats"] = overviewStatsFromProto(resp.GetTargetStats())
|
|
||||||
}
|
|
||||||
if h.userProfileClient != nil {
|
if h.userProfileClient != nil {
|
||||||
profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
Meta: httpkit.UserMeta(request, ""),
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
@ -329,19 +303,12 @@ func profileVisitData(record *userv1.ProfileVisitRecord) profileVisitRecordData
|
|||||||
if record == nil {
|
if record == nil {
|
||||||
return profileVisitRecordData{}
|
return profileVisitRecordData{}
|
||||||
}
|
}
|
||||||
data := profileVisitRecordData{
|
return profileVisitRecordData{
|
||||||
VisitorIdentityHidden: record.GetVisitorIdentityHidden(),
|
VisitorUserID: userIDString(record.GetVisitorUserId()),
|
||||||
TargetUserID: userIDString(record.GetTargetUserId()),
|
TargetUserID: userIDString(record.GetTargetUserId()),
|
||||||
VisitCount: record.GetVisitCount(),
|
VisitCount: record.GetVisitCount(),
|
||||||
LastVisitedAtMS: record.GetLastVisitedAtMs(),
|
LastVisitedAtMS: record.GetLastVisitedAtMs(),
|
||||||
}
|
}
|
||||||
if record.GetVisitorIdentityHidden() {
|
|
||||||
// 匿名记录只暴露随机展示 ID;visitor_user_id 使用 omitempty 完全省略,也不存在可继续拉取的 profile。
|
|
||||||
data.AnonymousVisitID = record.GetAnonymousVisitId()
|
|
||||||
} else {
|
|
||||||
data.VisitorUserID = userIDString(record.GetVisitorUserId())
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func followData(record *userv1.FollowRecord) followRecordData {
|
func followData(record *userv1.FollowRecord) followRecordData {
|
||||||
|
|||||||
@ -230,8 +230,6 @@ type vipBenefitData struct {
|
|||||||
CreatedAtMS int64 `json:"created_at_ms"`
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
Presentation *vipBenefitPresentationData `json:"presentation"`
|
Presentation *vipBenefitPresentationData `json:"presentation"`
|
||||||
// Resource 只有在 wallet 已确认同 App、active 且类型匹配时才非空。
|
|
||||||
Resource *resourceData `json:"resource,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type vipBenefitPresentationData struct {
|
type vipBenefitPresentationData struct {
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
package walletapi
|
package walletapi
|
||||||
|
|
||||||
import (
|
import walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
|
||||||
"hyapp/services/gateway-service/internal/transport/http/resourceview"
|
|
||||||
)
|
|
||||||
|
|
||||||
type overviewVIPData struct {
|
type overviewVIPData struct {
|
||||||
Level int32 `json:"level"`
|
Level int32 `json:"level"`
|
||||||
@ -33,7 +30,6 @@ type resourceData struct {
|
|||||||
AssetURL string `json:"asset_url"`
|
AssetURL string `json:"asset_url"`
|
||||||
PreviewURL string `json:"preview_url"`
|
PreviewURL string `json:"preview_url"`
|
||||||
AnimationURL string `json:"animation_url"`
|
AnimationURL string `json:"animation_url"`
|
||||||
Format string `json:"format,omitempty"`
|
|
||||||
MetadataJSON string `json:"metadata_json"`
|
MetadataJSON string `json:"metadata_json"`
|
||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
CreatedAtMS int64 `json:"created_at_ms"`
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
@ -69,7 +65,6 @@ func resourceFromProto(item *walletv1.Resource) resourceData {
|
|||||||
AssetURL: item.GetAssetUrl(),
|
AssetURL: item.GetAssetUrl(),
|
||||||
PreviewURL: item.GetPreviewUrl(),
|
PreviewURL: item.GetPreviewUrl(),
|
||||||
AnimationURL: item.GetAnimationUrl(),
|
AnimationURL: item.GetAnimationUrl(),
|
||||||
Format: resourceview.FormatFromMetadata(item.GetMetadataJson()),
|
|
||||||
MetadataJSON: item.GetMetadataJson(),
|
MetadataJSON: item.GetMetadataJson(),
|
||||||
SortOrder: item.GetSortOrder(),
|
SortOrder: item.GetSortOrder(),
|
||||||
CreatedAtMS: item.GetCreatedAtMs(),
|
CreatedAtMS: item.GetCreatedAtMs(),
|
||||||
|
|||||||
@ -233,12 +233,7 @@ func (h *Handler) triggerVIPOnlineNotice(writer http.ResponseWriter, request *ht
|
|||||||
// 客户端展示态可能滞后,执行前必须重新查 effective benefit;体验卡排除、过期和后台停用
|
// 客户端展示态可能滞后,执行前必须重新查 effective benefit;体验卡排除、过期和后台停用
|
||||||
// 都以 wallet-service 此刻的判定为准,不能仅比较 VIP9 等级。
|
// 都以 wallet-service 此刻的判定为准,不能仅比较 VIP9 等级。
|
||||||
statusCode, code, message := httpkit.MapReasonToHTTP(xerr.VIPBenefitRequired)
|
statusCode, code, message := httpkit.MapReasonToHTTP(xerr.VIPBenefitRequired)
|
||||||
httpkit.WriteErrorData(writer, request, statusCode, code, message, map[string]any{
|
httpkit.WriteError(writer, request, statusCode, code, message)
|
||||||
"benefit_code": vipOnlineNoticeBenefitCode,
|
|
||||||
"required_level": benefitResp.GetRequiredLevel(),
|
|
||||||
"current_effective_level": benefitResp.GetCurrentEffectiveLevel(),
|
|
||||||
"action": "send_online_global_notice",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
equippedResp, err := h.walletClient.BatchGetUserEquippedResources(ctx, &walletv1.BatchGetUserEquippedResourcesRequest{
|
equippedResp, err := h.walletClient.BatchGetUserEquippedResources(ctx, &walletv1.BatchGetUserEquippedResourcesRequest{
|
||||||
@ -443,7 +438,6 @@ func vipBenefitsFromProto(items []*walletv1.VipBenefit) []vipBenefitData {
|
|||||||
CreatedAtMS: item.GetCreatedAtMs(),
|
CreatedAtMS: item.GetCreatedAtMs(),
|
||||||
UpdatedAtMS: item.GetUpdatedAtMs(),
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
Presentation: vipBenefitPresentationFromProto(item.GetPresentation()),
|
Presentation: vipBenefitPresentationFromProto(item.GetPresentation()),
|
||||||
Resource: resourcePointerFromProto(item.GetResource()),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@ -211,8 +211,7 @@ type DrawCommand struct {
|
|||||||
Recharge7DCoins int64
|
Recharge7DCoins int64
|
||||||
Recharge30DCoins int64
|
Recharge30DCoins int64
|
||||||
LastRechargedAtMS int64
|
LastRechargedAtMS int64
|
||||||
// HostPeriodDiamondAdded 是 wallet 按后台礼物比例配置实际入账的主播钻石,资金拆分必须以该 owner 事实为准。
|
GiftIncomeCoins int64
|
||||||
HostPeriodDiamondAdded int64
|
|
||||||
// Sender* 是扣费时刻的公开展示快照;owner outbox 必须携带快照,避免下游为一条飘屏同步反查 user-service。
|
// Sender* 是扣费时刻的公开展示快照;owner outbox 必须携带快照,避免下游为一条飘屏同步反查 user-service。
|
||||||
SenderName string
|
SenderName string
|
||||||
SenderAvatar string
|
SenderAvatar string
|
||||||
|
|||||||
@ -68,13 +68,6 @@ type luckyGiftUserProfileRefresher interface {
|
|||||||
RefreshLuckyGiftUserProfiles(ctx context.Context, nowMS int64, batchSize int) (int, error)
|
RefreshLuckyGiftUserProfiles(ctx context.Context, nowMS int64, batchSize int) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// luckyGiftRuleVersionRepository 把历史版本能力保持为生产 MySQL 仓储的可选扩展,避免为只服务
|
|
||||||
// 单元测试的最小 Repository 伪实现扩散后台运维方法。
|
|
||||||
type luckyGiftRuleVersionRepository interface {
|
|
||||||
ListLuckyGiftRuleConfigVersions(ctx context.Context, poolID string) ([]domain.RuleConfig, error)
|
|
||||||
RollbackLuckyGiftRuleConfig(ctx context.Context, poolID string, targetRuleVersion int64, operatorAdminID int64, nowMS int64) (domain.RuleConfig, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WalletClient 是幸运礼物返奖唯一账务依赖;wallet-service 仍负责持久 outbox,activity 只做成功落库后的低延迟余额 IM。
|
// WalletClient 是幸运礼物返奖唯一账务依赖;wallet-service 仍负责持久 outbox,activity 只做成功落库后的低延迟余额 IM。
|
||||||
type WalletClient interface {
|
type WalletClient interface {
|
||||||
CreditLuckyGiftReward(ctx context.Context, req *walletv1.CreditLuckyGiftRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditLuckyGiftRewardResponse, error)
|
CreditLuckyGiftReward(ctx context.Context, req *walletv1.CreditLuckyGiftRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditLuckyGiftRewardResponse, error)
|
||||||
@ -794,42 +787,6 @@ func (s *Service) ListConfigs(ctx context.Context, appCode string) ([]domain.Rul
|
|||||||
return s.repository.ListLuckyGiftRuleConfigs(ctx, strings.TrimSpace(appCode))
|
return s.repository.ListLuckyGiftRuleConfigs(ctx, strings.TrimSpace(appCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ListConfigVersions(ctx context.Context, poolID string) ([]domain.RuleConfig, error) {
|
|
||||||
if err := s.requireRepository(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
versionRepository, ok := s.repository.(luckyGiftRuleVersionRepository)
|
|
||||||
if !ok {
|
|
||||||
return nil, xerr.New(xerr.Unavailable, "lucky gift rule version repository is unavailable")
|
|
||||||
}
|
|
||||||
poolID = normalizePoolID(poolID)
|
|
||||||
if poolID == "" {
|
|
||||||
return nil, xerr.New(xerr.InvalidArgument, "lucky gift pool id is required")
|
|
||||||
}
|
|
||||||
return versionRepository.ListLuckyGiftRuleConfigVersions(ctx, poolID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) RollbackConfig(ctx context.Context, poolID string, targetRuleVersion int64, operatorAdminID int64) (domain.RuleConfig, error) {
|
|
||||||
if err := s.requireRepository(); err != nil {
|
|
||||||
return domain.RuleConfig{}, err
|
|
||||||
}
|
|
||||||
versionRepository, ok := s.repository.(luckyGiftRuleVersionRepository)
|
|
||||||
if !ok {
|
|
||||||
return domain.RuleConfig{}, xerr.New(xerr.Unavailable, "lucky gift rule version repository is unavailable")
|
|
||||||
}
|
|
||||||
poolID = normalizePoolID(poolID)
|
|
||||||
if poolID == "" || targetRuleVersion <= 0 || operatorAdminID <= 0 {
|
|
||||||
return domain.RuleConfig{}, xerr.New(xerr.InvalidArgument, "lucky gift rollback request is incomplete")
|
|
||||||
}
|
|
||||||
return versionRepository.RollbackLuckyGiftRuleConfig(
|
|
||||||
ctx,
|
|
||||||
poolID,
|
|
||||||
targetRuleVersion,
|
|
||||||
operatorAdminID,
|
|
||||||
s.now().UTC().UnixMilli(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListDraws(ctx context.Context, query domain.DrawQuery) ([]domain.DrawResult, int64, error) {
|
func (s *Service) ListDraws(ctx context.Context, query domain.DrawQuery) ([]domain.DrawResult, int64, error) {
|
||||||
if err := s.requireRepository(); err != nil {
|
if err := s.requireRepository(); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|||||||
@ -332,8 +332,7 @@ func (r *Repository) executeExternalDynamicGiftEconomy(ctx context.Context, tx *
|
|||||||
CoinSpent: cmd.TotalAmount,
|
CoinSpent: cmd.TotalAmount,
|
||||||
PaidAtMS: cmd.PaidAtMS,
|
PaidAtMS: cmd.PaidAtMS,
|
||||||
UserRegisteredAtMS: cmd.UserRegisteredAtMS,
|
UserRegisteredAtMS: cmd.UserRegisteredAtMS,
|
||||||
// 外部 App 没有 HyApp wallet 回执,只能继续使用已签名外部金额按本规则拆出的主播份额。
|
GiftIncomeCoins: split.AnchorReturnCoins,
|
||||||
HostPeriodDiamondAdded: split.AnchorReturnCoins,
|
|
||||||
}
|
}
|
||||||
// 当前外部协议没有可验证的充值 owner 快照;保持 0/0 会落 novice 且关闭五分钟加成,
|
// 当前外部协议没有可验证的充值 owner 快照;保持 0/0 会落 novice 且关闭五分钟加成,
|
||||||
// 比从 metadata 或客户端自报字段猜测更安全。后续若扩协议,必须把签名覆盖的充值事实显式入契约。
|
// 比从 metadata 或客户端自报字段猜测更安全。后续若扩协议,必须把签名覆盖的充值事实显式入契约。
|
||||||
|
|||||||
@ -147,7 +147,7 @@ func (r *Repository) executeDynamicLuckyGiftDrawBatch(ctx context.Context, tx *s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fundSplit, err := luckyDynamicFundSplit(cmd.CoinSpent, cmd.HostPeriodDiamondAdded, strategyConfig)
|
fundSplit, err := luckyDynamicFundSplit(cmd.CoinSpent, cmd.GiftIncomeCoins, strategyConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -204,7 +204,7 @@ func (r *Repository) executeDynamicLuckyGiftDrawBatch(ctx context.Context, tx *s
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
unitSplit := unitSplits[index-1]
|
unitSplit := unitSplits[index-1]
|
||||||
unitCommand.HostPeriodDiamondAdded = unitSplit.AnchorReturnCoins
|
unitCommand.GiftIncomeCoins = unitSplit.AnchorReturnCoins
|
||||||
if unitCommand.CoinSpent <= 0 {
|
if unitCommand.CoinSpent <= 0 {
|
||||||
return nil, xerr.New(xerr.InvalidArgument, "dynamic lucky gift unit spend must be positive")
|
return nil, xerr.New(xerr.InvalidArgument, "dynamic lucky gift unit spend must be positive")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,7 @@ func TestDynamicLuckyGiftMySQLBatchAndExternalPaths(t *testing.T) {
|
|||||||
command := domain.DrawCommand{
|
command := domain.DrawCommand{
|
||||||
CommandID: "dynamic-99", PoolID: internalRule.PoolID, UserID: 1001, TargetUserID: 2001,
|
CommandID: "dynamic-99", PoolID: internalRule.PoolID, UserID: 1001, TargetUserID: 2001,
|
||||||
DeviceID: "device-1001", RoomID: "room-1", AnchorID: "anchor-1", GiftID: "super-lucky",
|
DeviceID: "device-1001", RoomID: "room-1", AnchorID: "anchor-1", GiftID: "super-lucky",
|
||||||
GiftCount: 99, CoinSpent: 100, HostPeriodDiamondAdded: 1, PaidAtMS: now,
|
GiftCount: 99, CoinSpent: 100, GiftIncomeCoins: 1, PaidAtMS: now,
|
||||||
}
|
}
|
||||||
missingWalletPaidAt := command
|
missingWalletPaidAt := command
|
||||||
missingWalletPaidAt.CommandID = "dynamic-missing-wallet-paid-at"
|
missingWalletPaidAt.CommandID = "dynamic-missing-wallet-paid-at"
|
||||||
@ -330,7 +330,7 @@ func assertDynamicTokenSurvivesUTCDayBoundary(t *testing.T, repo *Repository, db
|
|||||||
earn := domain.DrawCommand{
|
earn := domain.DrawCommand{
|
||||||
CommandID: "token-earn", PoolID: rule.PoolID, UserID: userID, TargetUserID: 1,
|
CommandID: "token-earn", PoolID: rule.PoolID, UserID: userID, TargetUserID: 1,
|
||||||
DeviceID: "device-token", RoomID: "room-token", AnchorID: "anchor-token", GiftID: "super-lucky",
|
DeviceID: "device-token", RoomID: "room-token", AnchorID: "anchor-token", GiftID: "super-lucky",
|
||||||
GiftCount: 1, CoinSpent: 5_000, HostPeriodDiamondAdded: 50, PaidAtMS: dayOneMS,
|
GiftCount: 1, CoinSpent: 5_000, GiftIncomeCoins: 50, PaidAtMS: dayOneMS,
|
||||||
}
|
}
|
||||||
if _, err := repo.ExecuteLuckyGiftDraw(ctx, earn, dayOneMS); err != nil {
|
if _, err := repo.ExecuteLuckyGiftDraw(ctx, earn, dayOneMS); err != nil {
|
||||||
t.Fatalf("earn milestone token: %v", err)
|
t.Fatalf("earn milestone token: %v", err)
|
||||||
@ -350,7 +350,7 @@ func assertDynamicTokenSurvivesUTCDayBoundary(t *testing.T, repo *Repository, db
|
|||||||
retain := earn
|
retain := earn
|
||||||
retain.CommandID = "token-retain-next-day"
|
retain.CommandID = "token-retain-next-day"
|
||||||
retain.CoinSpent = 1
|
retain.CoinSpent = 1
|
||||||
retain.HostPeriodDiamondAdded = 0
|
retain.GiftIncomeCoins = 0
|
||||||
retain.PaidAtMS = dayTwoMS
|
retain.PaidAtMS = dayTwoMS
|
||||||
if _, err := repo.ExecuteLuckyGiftDraw(ctx, retain, dayTwoMS); err != nil {
|
if _, err := repo.ExecuteLuckyGiftDraw(ctx, retain, dayTwoMS); err != nil {
|
||||||
t.Fatalf("retain token across UTC day: %v", err)
|
t.Fatalf("retain token across UTC day: %v", err)
|
||||||
@ -408,7 +408,7 @@ func assertDynamicSpendBatchRollbackAndRestart(t *testing.T, repo *Repository, d
|
|||||||
command := domain.DrawCommand{
|
command := domain.DrawCommand{
|
||||||
CommandID: "dual-spend-batch-command", PoolID: published.PoolID, UserID: 4_040, TargetUserID: 1,
|
CommandID: "dual-spend-batch-command", PoolID: published.PoolID, UserID: 4_040, TargetUserID: 1,
|
||||||
DeviceID: "dual-spend-batch-device", RoomID: "dual-spend-batch-room", AnchorID: "dual-spend-batch-anchor", GiftID: "super-lucky",
|
DeviceID: "dual-spend-batch-device", RoomID: "dual-spend-batch-room", AnchorID: "dual-spend-batch-anchor", GiftID: "super-lucky",
|
||||||
GiftCount: 3, CoinSpent: 30, HostPeriodDiamondAdded: 0, PaidAtMS: nowMS,
|
GiftCount: 3, CoinSpent: 30, GiftIncomeCoins: 0, PaidAtMS: nowMS,
|
||||||
}
|
}
|
||||||
const rollbackTrigger = "trg_lucky_dual_spend_rollback"
|
const rollbackTrigger = "trg_lucky_dual_spend_rollback"
|
||||||
if _, err := db.Exec("DROP TRIGGER IF EXISTS " + rollbackTrigger); err != nil {
|
if _, err := db.Exec("DROP TRIGGER IF EXISTS " + rollbackTrigger); err != nil {
|
||||||
@ -938,7 +938,7 @@ func TestLuckyDynamicDualSourcesSharePoolRTPRiskAndAuditAccounting(t *testing.T)
|
|||||||
command := domain.DrawCommand{
|
command := domain.DrawCommand{
|
||||||
CommandID: label, PoolID: runtimeConfig.PoolID, GiftID: runtimeConfig.GiftID, GiftCount: 1,
|
CommandID: label, PoolID: runtimeConfig.PoolID, GiftID: runtimeConfig.GiftID, GiftCount: 1,
|
||||||
UserID: 1001, TargetUserID: 2001, RoomID: "room-v5", AnchorID: "anchor-v5",
|
UserID: 1001, TargetUserID: 2001, RoomID: "room-v5", AnchorID: "anchor-v5",
|
||||||
CoinSpent: 100, HostPeriodDiamondAdded: 10, PaidAtMS: paidAtMS,
|
CoinSpent: 100, GiftIncomeCoins: 10, PaidAtMS: paidAtMS,
|
||||||
}
|
}
|
||||||
split := domain.StrategyFundSplit{PublicPoolCoins: 89, ProfitPoolCoins: 1, AnchorReturnCoins: 10}
|
split := domain.StrategyFundSplit{PublicPoolCoins: 89, ProfitPoolCoins: 1, AnchorReturnCoins: 10}
|
||||||
if err := (&Repository{}).stageLuckyDynamicDecision("lalu", runtimeConfig, runtimeConfig.GiftPrice, command, split, decision, paidAtMS, &state); err != nil {
|
if err := (&Repository{}).stageLuckyDynamicDecision("lalu", runtimeConfig, runtimeConfig.GiftPrice, command, split, decision, paidAtMS, &state); err != nil {
|
||||||
|
|||||||
@ -144,22 +144,22 @@ func luckyDynamicStrategyConfig(config domain.Config) domain.StrategyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// luckyDynamicFundSplit 以 wallet 已按后台礼物比例配置实际入账的主播钻石为唯一主播份额。
|
// luckyDynamicFundSplit 以不可变规则做唯一拆账,并校验 wallet 已经发给收礼人的 COIN 与规则主播份额完全一致。
|
||||||
// 幸运礼物规则只决定公共奖池比例;主播份额变化后的剩余金额自然进入平台利润,不能再用规则里的
|
// dynamic_v3 不能把旧 room 缺字段解释成 0,也不能把 App 钱包配置的其他返币比例静默塞进盈利池;
|
||||||
// anchor_rate_ppm 覆盖 wallet owner 事实,也不能把“返还金币比例”误当成主播收益。
|
// 启用新规则前必须先完成 wallet/room 契约滚动升级并让礼物返币配置与本规则一致。
|
||||||
func luckyDynamicFundSplit(totalSpent, actualAnchorIncome int64, config domain.StrategyConfig) (domain.StrategyFundSplit, error) {
|
func luckyDynamicFundSplit(totalSpent, actualAnchorIncome int64, config domain.StrategyConfig) (domain.StrategyFundSplit, error) {
|
||||||
configured, err := domain.SplitLuckyGiftStrategyFunds(totalSpent, config)
|
configured, err := domain.SplitLuckyGiftStrategyFunds(totalSpent, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.StrategyFundSplit{}, err
|
return domain.StrategyFundSplit{}, err
|
||||||
}
|
}
|
||||||
if actualAnchorIncome < 0 || actualAnchorIncome > totalSpent || configured.PublicPoolCoins > totalSpent-actualAnchorIncome {
|
if actualAnchorIncome != configured.AnchorReturnCoins {
|
||||||
return domain.StrategyFundSplit{}, fmt.Errorf("dynamic lucky gift wallet anchor income is invalid: spent=%d anchor=%d", totalSpent, actualAnchorIncome)
|
return domain.StrategyFundSplit{}, fmt.Errorf(
|
||||||
|
"dynamic lucky gift anchor income mismatch: wallet=%d configured=%d",
|
||||||
|
actualAnchorIncome,
|
||||||
|
configured.AnchorReturnCoins,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return domain.StrategyFundSplit{
|
return configured, nil
|
||||||
PublicPoolCoins: configured.PublicPoolCoins,
|
|
||||||
ProfitPoolCoins: totalSpent - configured.PublicPoolCoins - actualAnchorIncome,
|
|
||||||
AnchorReturnCoins: actualAnchorIncome,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// luckyDynamicUnitFundSplits 联合分配整批的三个资金桶:每一抽都先满足
|
// luckyDynamicUnitFundSplits 联合分配整批的三个资金桶:每一抽都先满足
|
||||||
|
|||||||
@ -102,18 +102,15 @@ func TestLuckyDynamicUnitFundSplitsConserveEveryDrawAndBatchBucket(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLuckyDynamicFundSplitUsesWalletAnchorIncome(t *testing.T) {
|
func TestLuckyDynamicFundSplitRejectsWalletAnchorMismatch(t *testing.T) {
|
||||||
config := domain.DefaultLuckyGiftStrategyConfig()
|
config := domain.DefaultLuckyGiftStrategyConfig()
|
||||||
if split, err := luckyDynamicFundSplit(100, 1, config); err != nil || split != (domain.StrategyFundSplit{PublicPoolCoins: 98, ProfitPoolCoins: 1, AnchorReturnCoins: 1}) {
|
if split, err := luckyDynamicFundSplit(100, 1, config); err != nil || split != (domain.StrategyFundSplit{PublicPoolCoins: 98, ProfitPoolCoins: 1, AnchorReturnCoins: 1}) {
|
||||||
t.Fatalf("matching split=%+v err=%v", split, err)
|
t.Fatalf("matching split=%+v err=%v", split, err)
|
||||||
}
|
}
|
||||||
if split, err := luckyDynamicFundSplit(100, 0, config); err != nil || split != (domain.StrategyFundSplit{PublicPoolCoins: 98, ProfitPoolCoins: 2, AnchorReturnCoins: 0}) {
|
if _, err := luckyDynamicFundSplit(100, 0, config); err == nil {
|
||||||
t.Fatalf("zero wallet anchor split=%+v err=%v", split, err)
|
t.Fatal("missing wallet anchor snapshot was accepted")
|
||||||
}
|
}
|
||||||
if split, err := luckyDynamicFundSplit(100, 2, config); err != nil || split != (domain.StrategyFundSplit{PublicPoolCoins: 98, ProfitPoolCoins: 0, AnchorReturnCoins: 2}) {
|
if _, err := luckyDynamicFundSplit(100, 10, config); err == nil {
|
||||||
t.Fatalf("wallet-authoritative split=%+v err=%v", split, err)
|
t.Fatal("wallet anchor ratio different from rule was accepted")
|
||||||
}
|
|
||||||
if _, err := luckyDynamicFundSplit(100, 3, config); err == nil {
|
|
||||||
t.Fatal("wallet anchor that exceeds the conserved remainder was accepted")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -251,95 +251,6 @@ func (r *Repository) ListLuckyGiftRuleConfigs(ctx context.Context, appCode strin
|
|||||||
return configs, rows.Err()
|
return configs, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListLuckyGiftRuleConfigVersions 返回同一奖池的完整不可变版本链。主键前缀
|
|
||||||
// (app_code,pool_id,rule_version) 可直接反向范围扫描,不会扫描其他应用或奖池的规则行。
|
|
||||||
func (r *Repository) ListLuckyGiftRuleConfigVersions(ctx context.Context, poolID string) ([]domain.RuleConfig, error) {
|
|
||||||
if r == nil || r.db == nil {
|
|
||||||
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
|
||||||
}
|
|
||||||
appCode := appcode.FromContext(ctx)
|
|
||||||
poolID = luckyPoolID(poolID)
|
|
||||||
if appCode == "" || poolID == "" {
|
|
||||||
return nil, xerr.New(xerr.InvalidArgument, "lucky gift app and pool are required")
|
|
||||||
}
|
|
||||||
rows, err := r.db.QueryContext(ctx, luckyRuleConfigSelectSQL+`
|
|
||||||
ORDER BY rule_version DESC`, appCode, poolID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
configs := make([]domain.RuleConfig, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
config, err := scanLuckyGiftRuleConfig(rows)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stages, err := r.getLuckyGiftRuleStages(ctx, r.db, appCode, poolID, config.RuleVersion)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.Stages = stages
|
|
||||||
configs = append(configs, config)
|
|
||||||
}
|
|
||||||
return configs, rows.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RollbackLuckyGiftRuleConfig 复制目标快照并追加新版本;旧版本和当前版本都保持不可变。
|
|
||||||
// 首先锁最新规则行,再锁实验行,严格沿用常规发布/实验入口的加锁顺序,避免并发发布与回滚互相覆盖。
|
|
||||||
func (r *Repository) RollbackLuckyGiftRuleConfig(ctx context.Context, poolID string, targetRuleVersion int64, operatorAdminID int64, nowMS int64) (domain.RuleConfig, error) {
|
|
||||||
if r == nil || r.db == nil {
|
|
||||||
return domain.RuleConfig{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
|
||||||
}
|
|
||||||
appCode := appcode.FromContext(ctx)
|
|
||||||
poolID = luckyPoolID(poolID)
|
|
||||||
if appCode == "" || poolID == "" || targetRuleVersion <= 0 || operatorAdminID <= 0 {
|
|
||||||
return domain.RuleConfig{}, xerr.New(xerr.InvalidArgument, "lucky gift rollback identity is incomplete")
|
|
||||||
}
|
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return domain.RuleConfig{}, err
|
|
||||||
}
|
|
||||||
defer func() { _ = tx.Rollback() }()
|
|
||||||
|
|
||||||
current, exists, err := r.getLuckyGiftRuleConfig(ctx, tx, appCode, poolID, true)
|
|
||||||
if err != nil {
|
|
||||||
return domain.RuleConfig{}, err
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return domain.RuleConfig{}, xerr.New(xerr.NotFound, "lucky gift pool config not found")
|
|
||||||
}
|
|
||||||
if targetRuleVersion >= current.RuleVersion {
|
|
||||||
return domain.RuleConfig{}, xerr.New(xerr.InvalidArgument, "rollback target must be older than the current rule version")
|
|
||||||
}
|
|
||||||
if _, active, err := r.getActiveLuckyGiftExperiment(ctx, tx, appCode, poolID, true); err != nil {
|
|
||||||
return domain.RuleConfig{}, err
|
|
||||||
} else if active {
|
|
||||||
return domain.RuleConfig{}, xerr.New(xerr.Conflict, "lucky gift pool has an active experiment; end the experiment before rollback")
|
|
||||||
}
|
|
||||||
target, exists, err := r.getLuckyGiftRuleConfigVersion(ctx, tx, appCode, poolID, targetRuleVersion, false)
|
|
||||||
if err != nil {
|
|
||||||
return domain.RuleConfig{}, err
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return domain.RuleConfig{}, xerr.New(xerr.NotFound, "lucky gift rollback target version not found")
|
|
||||||
}
|
|
||||||
target.CreatedByAdminID = operatorAdminID
|
|
||||||
// 回滚从当前操作时刻生效;不能沿用历史 effective_from_ms,否则审计会把新版本误显示成旧发布时间。
|
|
||||||
target.EffectiveFromMS = 0
|
|
||||||
if target.StrategyVersion == domain.StrategyDynamicV3 {
|
|
||||||
// V3 资金只保留在独立水位账本中。历史快照即使残留 seed,回滚也不能重新注资。
|
|
||||||
target.InitialPoolCoins = 0
|
|
||||||
}
|
|
||||||
published, err := r.publishLuckyGiftRuleConfigTx(ctx, tx, target, nowMS)
|
|
||||||
if err != nil {
|
|
||||||
return domain.RuleConfig{}, err
|
|
||||||
}
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return domain.RuleConfig{}, err
|
|
||||||
}
|
|
||||||
return published, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const luckyRuleConfigSelectSQL = `
|
const luckyRuleConfigSelectSQL = `
|
||||||
SELECT app_code, pool_id, rule_version, enabled, strategy_version, target_rtp_ppm, pool_rate_ppm,
|
SELECT app_code, pool_id, rule_version, enabled, strategy_version, target_rtp_ppm, pool_rate_ppm,
|
||||||
profit_rate_ppm, anchor_rate_ppm,
|
profit_rate_ppm, anchor_rate_ppm,
|
||||||
|
|||||||
@ -132,8 +132,7 @@ func luckyDrawCommandFromProto(meta *luckygiftv1.LuckyGiftMeta) domain.DrawComma
|
|||||||
Recharge7DCoins: meta.GetRecharge_7DCoins(),
|
Recharge7DCoins: meta.GetRecharge_7DCoins(),
|
||||||
Recharge30DCoins: meta.GetRecharge_30DCoins(),
|
Recharge30DCoins: meta.GetRecharge_30DCoins(),
|
||||||
LastRechargedAtMS: meta.GetLastRechargedAtMs(),
|
LastRechargedAtMS: meta.GetLastRechargedAtMs(),
|
||||||
// protobuf 为滚动发布保留旧字段号和 getter 名;domain 内只暴露真实的 wallet owner 语义。
|
GiftIncomeCoins: meta.GetGiftIncomeCoins(),
|
||||||
HostPeriodDiamondAdded: meta.GetGiftIncomeCoins(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,31 +183,6 @@ func (s *AdminLuckyGiftServer) ListLuckyGiftConfigs(ctx context.Context, req *lu
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AdminLuckyGiftServer) ListLuckyGiftConfigVersions(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigVersionsRequest) (*luckygiftv1.ListLuckyGiftConfigVersionsResponse, error) {
|
|
||||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
|
||||||
configs, err := s.svc.ListConfigVersions(ctx, req.GetPoolId())
|
|
||||||
if err != nil {
|
|
||||||
return nil, xerr.ToGRPCError(err)
|
|
||||||
}
|
|
||||||
resp := &luckygiftv1.ListLuckyGiftConfigVersionsResponse{Configs: make([]*luckygiftv1.LuckyGiftRuleConfig, 0, len(configs))}
|
|
||||||
for _, config := range configs {
|
|
||||||
resp.Configs = append(resp.Configs, luckyRuleConfigToProto(config))
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AdminLuckyGiftServer) RollbackLuckyGiftConfig(ctx context.Context, req *luckygiftv1.RollbackLuckyGiftConfigRequest) (*luckygiftv1.RollbackLuckyGiftConfigResponse, error) {
|
|
||||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
|
||||||
config, err := s.svc.RollbackConfig(ctx, req.GetPoolId(), req.GetTargetRuleVersion(), req.GetOperatorAdminId())
|
|
||||||
if err != nil {
|
|
||||||
return nil, xerr.ToGRPCError(err)
|
|
||||||
}
|
|
||||||
return &luckygiftv1.RollbackLuckyGiftConfigResponse{
|
|
||||||
SourceRuleVersion: req.GetTargetRuleVersion(),
|
|
||||||
Config: luckyRuleConfigToProto(config),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AdminLuckyGiftServer) ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error) {
|
func (s *AdminLuckyGiftServer) ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error) {
|
||||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||||
draws, total, err := s.svc.ListDraws(ctx, domain.DrawQuery{
|
draws, total, err := s.svc.ListDraws(ctx, domain.DrawQuery{
|
||||||
|
|||||||
@ -81,12 +81,6 @@ rocketmq:
|
|||||||
topic: "hyapp_user_outbox"
|
topic: "hyapp_user_outbox"
|
||||||
consumer_group: "hyapp-room-user-region-sync"
|
consumer_group: "hyapp-room-user-region-sync"
|
||||||
consumer_max_reconsume_times: 16
|
consumer_max_reconsume_times: 16
|
||||||
# 独立消费 wallet owner 事实,Room Cell 装扮失效和资源素材刷新都走持久命令链路。
|
|
||||||
wallet_outbox:
|
|
||||||
enabled: true
|
|
||||||
topic: "hyapp_wallet_outbox"
|
|
||||||
consumer_group: "hyapp-room-wallet-decoration-sync"
|
|
||||||
consumer_max_reconsume_times: 16
|
|
||||||
# 房间中奖 IM 由 lucky-gift owner 事实可靠派生;独立 group 保证不和其他下游共享消费位点。
|
# 房间中奖 IM 由 lucky-gift owner 事实可靠派生;独立 group 保证不和其他下游共享消费位点。
|
||||||
lucky_gift_outbox:
|
lucky_gift_outbox:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@ -91,12 +91,6 @@ rocketmq:
|
|||||||
topic: "hyapp_user_outbox"
|
topic: "hyapp_user_outbox"
|
||||||
consumer_group: "hyapp-room-user-region-sync"
|
consumer_group: "hyapp-room-user-region-sync"
|
||||||
consumer_max_reconsume_times: 16
|
consumer_max_reconsume_times: 16
|
||||||
# 线上必须使用 room-service 专属 group;不要复用 notice/statistics 的 wallet 消费位点。
|
|
||||||
wallet_outbox:
|
|
||||||
enabled: true
|
|
||||||
topic: "hyapp_wallet_outbox"
|
|
||||||
consumer_group: "hyapp-room-wallet-decoration-sync"
|
|
||||||
consumer_max_reconsume_times: 16
|
|
||||||
# 线上必须使用 room-service 自己的消费组;owner outbox 只通过 MQ 进入房间展示链路。
|
# 线上必须使用 room-service 自己的消费组;owner outbox 只通过 MQ 进入房间展示链路。
|
||||||
lucky_gift_outbox:
|
lucky_gift_outbox:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@ -84,12 +84,6 @@ rocketmq:
|
|||||||
topic: "hyapp_user_outbox"
|
topic: "hyapp_user_outbox"
|
||||||
consumer_group: "hyapp-room-user-region-sync"
|
consumer_group: "hyapp-room-user-region-sync"
|
||||||
consumer_max_reconsume_times: 16
|
consumer_max_reconsume_times: 16
|
||||||
# VIP/资源变更用 room-service 独立消费位点重算当前房间装扮,解绑或禁用后无需等客户端重进房。
|
|
||||||
wallet_outbox:
|
|
||||||
enabled: true
|
|
||||||
topic: "hyapp_wallet_outbox"
|
|
||||||
consumer_group: "hyapp-room-wallet-decoration-sync"
|
|
||||||
consumer_max_reconsume_times: 16
|
|
||||||
# 幸运礼物 owner 事实由独立消费组派生为房间展示 outbox;不能复用 activity 消费位点或直连幸运礼物库。
|
# 幸运礼物 owner 事实由独立消费组派生为房间展示 outbox;不能复用 activity 消费位点或直连幸运礼物库。
|
||||||
lucky_gift_outbox:
|
lucky_gift_outbox:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@ -54,8 +54,6 @@ CREATE TABLE IF NOT EXISTS room_list_entries (
|
|||||||
seat_count INT NOT NULL DEFAULT 0 COMMENT '麦位数量',
|
seat_count INT NOT NULL DEFAULT 0 COMMENT '麦位数量',
|
||||||
occupied_seat_count INT NOT NULL DEFAULT 0 COMMENT '占用麦位数量',
|
occupied_seat_count INT NOT NULL DEFAULT 0 COMMENT '占用麦位数量',
|
||||||
sort_score BIGINT NOT NULL DEFAULT 0 COMMENT '排序分值',
|
sort_score BIGINT NOT NULL DEFAULT 0 COMMENT '排序分值',
|
||||||
room_border_json JSON NULL COMMENT '当前未过期房间边框资源快照',
|
|
||||||
room_name_style_json JSON NULL COMMENT '当前未过期彩色房名资源快照',
|
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
PRIMARY KEY (app_code, room_id),
|
PRIMARY KEY (app_code, room_id),
|
||||||
@ -116,84 +114,15 @@ CREATE TABLE IF NOT EXISTS room_background_images (
|
|||||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||||||
background_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '背景图 ID',
|
background_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '背景图 ID',
|
||||||
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
|
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
|
||||||
image_url VARCHAR(1024) NOT NULL COMMENT '背景图 URL',
|
image_url VARCHAR(256) NOT NULL COMMENT '背景图 URL',
|
||||||
-- utf8mb4 的 1024 字符 URL 无法直接进入 InnoDB 3072 字节唯一键;生成列保留完整 URL 唯一性,避免前缀索引误判。
|
|
||||||
image_url_sha256 BINARY(32) GENERATED ALWAYS AS (UNHEX(SHA2(image_url, 256))) STORED COMMENT '背景图 URL 唯一性哈希',
|
|
||||||
command_id VARCHAR(128) NULL COMMENT '新版素材保存幂等键;历史行保持 NULL',
|
|
||||||
object_key VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'COS 对象 key',
|
|
||||||
content_type VARCHAR(64) NOT NULL DEFAULT '' COMMENT '服务端嗅探 MIME',
|
|
||||||
media_format VARCHAR(16) NOT NULL DEFAULT '' COMMENT 'jpeg/png/gif/webp',
|
|
||||||
size_bytes BIGINT NOT NULL DEFAULT 0 COMMENT '文件字节数',
|
|
||||||
width INT NOT NULL DEFAULT 0 COMMENT '像素宽度',
|
|
||||||
height INT NOT NULL DEFAULT 0 COMMENT '像素高度',
|
|
||||||
animated BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否动图',
|
|
||||||
frame_count INT NOT NULL DEFAULT 0 COMMENT '帧数',
|
|
||||||
duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '动图总时长',
|
|
||||||
poster_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '可选首帧 URL',
|
|
||||||
thumbnail_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '可选缩略图 URL',
|
|
||||||
sha256 CHAR(64) NOT NULL DEFAULT '' COMMENT '原文件 SHA-256',
|
|
||||||
media_status VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'active 表示已通过专用上传入口',
|
|
||||||
created_by_user_id BIGINT NOT NULL COMMENT '保存背景图的用户 ID',
|
created_by_user_id BIGINT NOT NULL COMMENT '保存背景图的用户 ID',
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
PRIMARY KEY (background_id),
|
PRIMARY KEY (background_id),
|
||||||
UNIQUE KEY uk_room_background_url (app_code, room_id, image_url_sha256),
|
UNIQUE KEY uk_room_background_url (app_code, room_id, image_url),
|
||||||
UNIQUE KEY uk_room_background_command (app_code, room_id, command_id),
|
|
||||||
KEY idx_room_background_list (app_code, room_id, created_at_ms DESC, background_id DESC)
|
KEY idx_room_background_list (app_code, room_id, created_at_ms DESC, background_id DESC)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间背景图素材表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间背景图素材表';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS room_media_upload_registrations (
|
|
||||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
|
||||||
room_id VARCHAR(64) NOT NULL COMMENT '目标房间 ID',
|
|
||||||
command_id VARCHAR(128) NOT NULL COMMENT '专用上传强幂等键',
|
|
||||||
actor_user_id BIGINT NOT NULL COMMENT '首次授权用户 ID',
|
|
||||||
purpose VARCHAR(32) NOT NULL COMMENT 'room_background/room_image',
|
|
||||||
media_payload_sha256 CHAR(64) NOT NULL COMMENT '首次服务端解析媒体事实哈希',
|
|
||||||
expected_object_key VARCHAR(512) NOT NULL COMMENT 'room owner 确定的 COS key',
|
|
||||||
object_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT 'PutObject 成功后的访问 URL',
|
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'authorized' COMMENT 'authorized/active',
|
|
||||||
content_type VARCHAR(64) NOT NULL COMMENT '服务端嗅探 MIME',
|
|
||||||
media_format VARCHAR(16) NOT NULL COMMENT 'jpeg/png/gif/webp',
|
|
||||||
size_bytes BIGINT NOT NULL COMMENT '原文件字节数',
|
|
||||||
width INT NOT NULL COMMENT '像素宽度',
|
|
||||||
height INT NOT NULL COMMENT '像素高度',
|
|
||||||
animated BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否动图',
|
|
||||||
frame_count INT NOT NULL COMMENT '帧数',
|
|
||||||
duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '动图总时长',
|
|
||||||
sha256 CHAR(64) NOT NULL COMMENT '原文件 SHA-256',
|
|
||||||
authorized_room_version BIGINT NOT NULL COMMENT '首次授权看到的房间版本',
|
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
|
||||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
|
||||||
PRIMARY KEY (app_code, room_id, command_id),
|
|
||||||
UNIQUE KEY uk_room_media_upload_object (app_code, expected_object_key),
|
|
||||||
KEY idx_room_media_upload_status_created (app_code, status, created_at_ms)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间专用媒体上传授权与完成事实';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS room_decoration_assignments (
|
|
||||||
app_code VARCHAR(32) NOT NULL,
|
|
||||||
room_id VARCHAR(64) NOT NULL,
|
|
||||||
decoration_type VARCHAR(32) NOT NULL,
|
|
||||||
owner_user_id BIGINT NOT NULL,
|
|
||||||
benefit_code VARCHAR(64) NOT NULL,
|
|
||||||
resource_id BIGINT NOT NULL,
|
|
||||||
entitlement_id VARCHAR(128) NOT NULL DEFAULT '',
|
|
||||||
expires_at_ms BIGINT NOT NULL DEFAULT 0,
|
|
||||||
updated_at_ms BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (app_code, room_id, decoration_type),
|
|
||||||
KEY idx_room_decoration_resource (app_code, resource_id, room_id, decoration_type),
|
|
||||||
KEY idx_room_decoration_owner (app_code, owner_user_id, room_id, decoration_type),
|
|
||||||
KEY idx_room_decoration_expiry (app_code, expires_at_ms, room_id, decoration_type)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间当前 VIP 装扮反向索引';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS room_wallet_event_consumption (
|
|
||||||
app_code VARCHAR(32) NOT NULL,
|
|
||||||
event_id VARCHAR(128) NOT NULL,
|
|
||||||
event_type VARCHAR(64) NOT NULL,
|
|
||||||
consumed_at_ms BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (app_code, event_id),
|
|
||||||
KEY idx_room_wallet_event_consumed (app_code, consumed_at_ms)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='room-service 消费 wallet 装扮事实的本地幂等表';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS room_snapshots (
|
CREATE TABLE IF NOT EXISTS room_snapshots (
|
||||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||||||
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
|
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user