Compare commits

...

31 Commits

Author SHA1 Message Date
zhx
bf2735d068 消息相关 2026-06-25 19:57:16 +08:00
zhx
35060cc1fc 公共组件 2026-06-25 17:07:01 +08:00
zhx
72ec2bfcc9 三方汇率支持 2026-06-25 17:00:11 +08:00
zhx
e385704a7d 补充 2026-06-25 16:38:47 +08:00
zhx
a93c3f24d1 v5pay 2026-06-25 16:29:06 +08:00
zhx
7cd08d0967 身份自动发放 2026-06-25 16:14:07 +08:00
zhx
9b4447e158 添加新接口 2026-06-25 15:07:46 +08:00
zhx
13f07d800f feat(statistics): add stat_tz dashboard aggregation 2026-06-25 14:31:15 +08:00
zhx
d57419c99e cp和充值 2026-06-25 13:48:50 +08:00
zhx
1e3d628f57 Merge branch 'main' of gitea.haiyihy.com:hy/hyapp-server 2026-06-25 11:21:31 +08:00
zhx
f8e34770d3 三方支付链接 2026-06-25 11:21:16 +08:00
local
4028928123 fix invite code error message 2026-06-24 21:04:42 +08:00
zhx
6fd0f03895 撤销资源组 2026-06-24 19:49:01 +08:00
zhx
9c547faf0e 限制机器人外观资源池 2026-06-24 18:56:14 +08:00
zhx
9a19267c6a 经理权限和身份自动赠送 2026-06-24 14:46:07 +08:00
zhx
b97e5f0011 fix room pin list type filter 2026-06-24 13:28:40 +08:00
zhx
ac6672f398 修复 2026-06-24 13:02:41 +08:00
zhx
9c39d33bf2 修复banner 2026-06-24 12:58:19 +08:00
zhx
dba6035fbe Merge branch 'test' 2026-06-24 10:45:51 +08:00
zhx
93250e5a7b fix statistics registration active count 2026-06-24 10:43:47 +08:00
zhx
059de783ac fix statistics backfill orphan activity 2026-06-24 10:23:29 +08:00
zhx
d51b735783 Merge branch 'main' of gitea.haiyihy.com:hy/hyapp-server 2026-06-24 10:04:32 +08:00
zhx
d947ebbfa3 增加统计 2026-06-24 10:04:30 +08:00
local
3cf62395e0 fix coin seller stock deduction delta 2026-06-23 23:52:06 +08:00
local
ac767d1c7e fix coin seller stock deduction accounting 2026-06-23 23:33:28 +08:00
local
f455df9192 fix mifapay zero-decimal currency amount 2026-06-23 23:08:18 +08:00
zhx
1ac0cecf63 修复多app登录配置 2026-06-23 19:52:14 +08:00
zhx
395470f3ba fix room pin list sql assembly 2026-06-23 19:29:10 +08:00
zhx
ebc99a097d log room pin list repository failures 2026-06-23 19:23:10 +08:00
zhx
6cfe45f73a fix admin room pin navigation migration 2026-06-23 19:11:00 +08:00
zhx
ee86af4c14 修改为0的判断 2026-06-23 16:14:14 +08:00
210 changed files with 26615 additions and 13686 deletions

File diff suppressed because it is too large Load Diff

View File

@ -178,6 +178,67 @@ message CreateFanoutJobResponse {
bool created = 3;
}
// ActionConfirm App C2C IM
message ActionConfirm {
string confirm_id = 1;
string business_type = 2;
string business_subtype = 3;
string business_id = 4;
int64 sender_user_id = 5;
int64 receiver_user_id = 6;
string status = 7;
string action = 8;
int64 processed_at_ms = 9;
int64 created_at_ms = 10;
int64 updated_at_ms = 11;
}
message AcceptActionConfirmRequest {
RequestMeta meta = 1;
string confirm_id = 2;
int64 actor_user_id = 3;
string command_id = 4;
}
message AcceptActionConfirmResponse {
ActionConfirm confirm = 1;
}
message RejectActionConfirmRequest {
RequestMeta meta = 1;
string confirm_id = 2;
int64 actor_user_id = 3;
string command_id = 4;
string reason = 5;
}
message RejectActionConfirmResponse {
ActionConfirm confirm = 1;
}
message BatchGetActionConfirmStatusRequest {
RequestMeta meta = 1;
int64 user_id = 2;
repeated string confirm_ids = 3;
}
message BatchGetActionConfirmStatusResponse {
repeated ActionConfirm items = 1;
}
message ListConversationActionConfirmsRequest {
RequestMeta meta = 1;
int64 user_id = 2;
int64 peer_user_id = 3;
int32 page_size = 4;
string page_token = 5;
}
message ListConversationActionConfirmsResponse {
repeated ActionConfirm items = 1;
string next_page_token = 2;
}
// CronBatchRequest cron-service activity-service
message CronBatchRequest {
RequestMeta meta = 1;
@ -2256,6 +2317,7 @@ message GetCPWeeklyRankStatusResponse {
int64 period_start_ms = 3;
int64 period_end_ms = 4;
int64 server_time_ms = 5;
repeated CPWeeklyRankEntry previous_entries = 6;
}
message GetCPWeeklyRankConfigRequest {
@ -2464,6 +2526,14 @@ service MessageInboxService {
rpc CreateFanoutJob(CreateFanoutJobRequest) returns (CreateFanoutJobResponse);
}
// MessageActionConfirmService IM owner service
service MessageActionConfirmService {
rpc AcceptActionConfirm(AcceptActionConfirmRequest) returns (AcceptActionConfirmResponse);
rpc RejectActionConfirm(RejectActionConfirmRequest) returns (RejectActionConfirmResponse);
rpc BatchGetActionConfirmStatus(BatchGetActionConfirmStatusRequest) returns (BatchGetActionConfirmStatusResponse);
rpc ListConversationActionConfirms(ListConversationActionConfirmsRequest) returns (ListConversationActionConfirmsResponse);
}
// ActivityCronService cron-service activity-service owner
service ActivityCronService {
rpc ProcessMessageFanoutBatch(CronBatchRequest) returns (CronBatchResponse);

View File

@ -496,6 +496,227 @@ var MessageInboxService_ServiceDesc = grpc.ServiceDesc{
Metadata: "proto/activity/v1/activity.proto",
}
const (
MessageActionConfirmService_AcceptActionConfirm_FullMethodName = "/hyapp.activity.v1.MessageActionConfirmService/AcceptActionConfirm"
MessageActionConfirmService_RejectActionConfirm_FullMethodName = "/hyapp.activity.v1.MessageActionConfirmService/RejectActionConfirm"
MessageActionConfirmService_BatchGetActionConfirmStatus_FullMethodName = "/hyapp.activity.v1.MessageActionConfirmService/BatchGetActionConfirmStatus"
MessageActionConfirmService_ListConversationActionConfirms_FullMethodName = "/hyapp.activity.v1.MessageActionConfirmService/ListConversationActionConfirms"
)
// MessageActionConfirmServiceClient is the client API for MessageActionConfirmService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// MessageActionConfirmService 拥有 IM 操作按钮的确认状态,业务执行仍回到 owner service。
type MessageActionConfirmServiceClient interface {
AcceptActionConfirm(ctx context.Context, in *AcceptActionConfirmRequest, opts ...grpc.CallOption) (*AcceptActionConfirmResponse, error)
RejectActionConfirm(ctx context.Context, in *RejectActionConfirmRequest, opts ...grpc.CallOption) (*RejectActionConfirmResponse, error)
BatchGetActionConfirmStatus(ctx context.Context, in *BatchGetActionConfirmStatusRequest, opts ...grpc.CallOption) (*BatchGetActionConfirmStatusResponse, error)
ListConversationActionConfirms(ctx context.Context, in *ListConversationActionConfirmsRequest, opts ...grpc.CallOption) (*ListConversationActionConfirmsResponse, error)
}
type messageActionConfirmServiceClient struct {
cc grpc.ClientConnInterface
}
func NewMessageActionConfirmServiceClient(cc grpc.ClientConnInterface) MessageActionConfirmServiceClient {
return &messageActionConfirmServiceClient{cc}
}
func (c *messageActionConfirmServiceClient) AcceptActionConfirm(ctx context.Context, in *AcceptActionConfirmRequest, opts ...grpc.CallOption) (*AcceptActionConfirmResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AcceptActionConfirmResponse)
err := c.cc.Invoke(ctx, MessageActionConfirmService_AcceptActionConfirm_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *messageActionConfirmServiceClient) RejectActionConfirm(ctx context.Context, in *RejectActionConfirmRequest, opts ...grpc.CallOption) (*RejectActionConfirmResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RejectActionConfirmResponse)
err := c.cc.Invoke(ctx, MessageActionConfirmService_RejectActionConfirm_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *messageActionConfirmServiceClient) BatchGetActionConfirmStatus(ctx context.Context, in *BatchGetActionConfirmStatusRequest, opts ...grpc.CallOption) (*BatchGetActionConfirmStatusResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(BatchGetActionConfirmStatusResponse)
err := c.cc.Invoke(ctx, MessageActionConfirmService_BatchGetActionConfirmStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *messageActionConfirmServiceClient) ListConversationActionConfirms(ctx context.Context, in *ListConversationActionConfirmsRequest, opts ...grpc.CallOption) (*ListConversationActionConfirmsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListConversationActionConfirmsResponse)
err := c.cc.Invoke(ctx, MessageActionConfirmService_ListConversationActionConfirms_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// MessageActionConfirmServiceServer is the server API for MessageActionConfirmService service.
// All implementations must embed UnimplementedMessageActionConfirmServiceServer
// for forward compatibility.
//
// MessageActionConfirmService 拥有 IM 操作按钮的确认状态,业务执行仍回到 owner service。
type MessageActionConfirmServiceServer interface {
AcceptActionConfirm(context.Context, *AcceptActionConfirmRequest) (*AcceptActionConfirmResponse, error)
RejectActionConfirm(context.Context, *RejectActionConfirmRequest) (*RejectActionConfirmResponse, error)
BatchGetActionConfirmStatus(context.Context, *BatchGetActionConfirmStatusRequest) (*BatchGetActionConfirmStatusResponse, error)
ListConversationActionConfirms(context.Context, *ListConversationActionConfirmsRequest) (*ListConversationActionConfirmsResponse, error)
mustEmbedUnimplementedMessageActionConfirmServiceServer()
}
// UnimplementedMessageActionConfirmServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedMessageActionConfirmServiceServer struct{}
func (UnimplementedMessageActionConfirmServiceServer) AcceptActionConfirm(context.Context, *AcceptActionConfirmRequest) (*AcceptActionConfirmResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AcceptActionConfirm not implemented")
}
func (UnimplementedMessageActionConfirmServiceServer) RejectActionConfirm(context.Context, *RejectActionConfirmRequest) (*RejectActionConfirmResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RejectActionConfirm not implemented")
}
func (UnimplementedMessageActionConfirmServiceServer) BatchGetActionConfirmStatus(context.Context, *BatchGetActionConfirmStatusRequest) (*BatchGetActionConfirmStatusResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BatchGetActionConfirmStatus not implemented")
}
func (UnimplementedMessageActionConfirmServiceServer) ListConversationActionConfirms(context.Context, *ListConversationActionConfirmsRequest) (*ListConversationActionConfirmsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListConversationActionConfirms not implemented")
}
func (UnimplementedMessageActionConfirmServiceServer) mustEmbedUnimplementedMessageActionConfirmServiceServer() {
}
func (UnimplementedMessageActionConfirmServiceServer) testEmbeddedByValue() {}
// UnsafeMessageActionConfirmServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to MessageActionConfirmServiceServer will
// result in compilation errors.
type UnsafeMessageActionConfirmServiceServer interface {
mustEmbedUnimplementedMessageActionConfirmServiceServer()
}
func RegisterMessageActionConfirmServiceServer(s grpc.ServiceRegistrar, srv MessageActionConfirmServiceServer) {
// If the following call pancis, it indicates UnimplementedMessageActionConfirmServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&MessageActionConfirmService_ServiceDesc, srv)
}
func _MessageActionConfirmService_AcceptActionConfirm_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AcceptActionConfirmRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MessageActionConfirmServiceServer).AcceptActionConfirm(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MessageActionConfirmService_AcceptActionConfirm_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MessageActionConfirmServiceServer).AcceptActionConfirm(ctx, req.(*AcceptActionConfirmRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MessageActionConfirmService_RejectActionConfirm_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RejectActionConfirmRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MessageActionConfirmServiceServer).RejectActionConfirm(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MessageActionConfirmService_RejectActionConfirm_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MessageActionConfirmServiceServer).RejectActionConfirm(ctx, req.(*RejectActionConfirmRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MessageActionConfirmService_BatchGetActionConfirmStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BatchGetActionConfirmStatusRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MessageActionConfirmServiceServer).BatchGetActionConfirmStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MessageActionConfirmService_BatchGetActionConfirmStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MessageActionConfirmServiceServer).BatchGetActionConfirmStatus(ctx, req.(*BatchGetActionConfirmStatusRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MessageActionConfirmService_ListConversationActionConfirms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListConversationActionConfirmsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MessageActionConfirmServiceServer).ListConversationActionConfirms(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MessageActionConfirmService_ListConversationActionConfirms_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MessageActionConfirmServiceServer).ListConversationActionConfirms(ctx, req.(*ListConversationActionConfirmsRequest))
}
return interceptor(ctx, in, info, handler)
}
// MessageActionConfirmService_ServiceDesc is the grpc.ServiceDesc for MessageActionConfirmService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var MessageActionConfirmService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "hyapp.activity.v1.MessageActionConfirmService",
HandlerType: (*MessageActionConfirmServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "AcceptActionConfirm",
Handler: _MessageActionConfirmService_AcceptActionConfirm_Handler,
},
{
MethodName: "RejectActionConfirm",
Handler: _MessageActionConfirmService_RejectActionConfirm_Handler,
},
{
MethodName: "BatchGetActionConfirmStatus",
Handler: _MessageActionConfirmService_BatchGetActionConfirmStatus_Handler,
},
{
MethodName: "ListConversationActionConfirms",
Handler: _MessageActionConfirmService_ListConversationActionConfirms_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/activity/v1/activity.proto",
}
const (
ActivityCronService_ProcessMessageFanoutBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessMessageFanoutBatch"
ActivityCronService_ProcessLevelRewardBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessLevelRewardBatch"

View File

@ -472,6 +472,8 @@ type RoomUserJoined struct {
EntryVehicle *RoomEntryVehicleSnapshot `protobuf:"bytes,4,opt,name=entry_vehicle,json=entryVehicle,proto3" json:"entry_vehicle,omitempty"`
CountryId int64 `protobuf:"varint,5,opt,name=country_id,json=countryId,proto3" json:"country_id,omitempty"`
RegionId int64 `protobuf:"varint,6,opt,name=region_id,json=regionId,proto3" json:"region_id,omitempty"`
// is_robot 标记本次进房来自房间机器人调度,统计服务必须跳过活跃用户口径。
IsRobot bool `protobuf:"varint,7,opt,name=is_robot,json=isRobot,proto3" json:"is_robot,omitempty"`
}
func (x *RoomUserJoined) Reset() {
@ -546,6 +548,13 @@ func (x *RoomUserJoined) GetRegionId() int64 {
return 0
}
func (x *RoomUserJoined) GetIsRobot() bool {
if x != nil {
return x.IsRobot
}
return false
}
// RoomUserLeft 表达用户离房成功。
type RoomUserLeft struct {
state protoimpl.MessageState
@ -1233,6 +1242,16 @@ type RoomGiftSent struct {
SyntheticLuckyGift bool `protobuf:"varint,21,opt,name=synthetic_lucky_gift,json=syntheticLuckyGift,proto3" json:"synthetic_lucky_gift,omitempty"`
// real_room_robot_gift 标记真人房机器人送出的真实热度礼物,用于 Databi 单独统计机器人投放价值。
RealRoomRobotGift bool `protobuf:"varint,22,opt,name=real_room_robot_gift,json=realRoomRobotGift,proto3" json:"real_room_robot_gift,omitempty"`
// sender_name 是送礼入口固化的展示昵称,只用于客户端 IM 展示兜底,不作为用户主资料事实。
SenderName string `protobuf:"bytes,23,opt,name=sender_name,json=senderName,proto3" json:"sender_name,omitempty"`
SenderAvatar string `protobuf:"bytes,24,opt,name=sender_avatar,json=senderAvatar,proto3" json:"sender_avatar,omitempty"`
SenderDisplayUserId string `protobuf:"bytes,25,opt,name=sender_display_user_id,json=senderDisplayUserId,proto3" json:"sender_display_user_id,omitempty"`
SenderPrettyDisplayUserId string `protobuf:"bytes,26,opt,name=sender_pretty_display_user_id,json=senderPrettyDisplayUserId,proto3" json:"sender_pretty_display_user_id,omitempty"`
// receiver_nickname 是收礼目标在送礼瞬间的展示昵称;字段名匹配 Flutter 已有解析逻辑。
ReceiverNickname string `protobuf:"bytes,27,opt,name=receiver_nickname,json=receiverNickname,proto3" json:"receiver_nickname,omitempty"`
ReceiverAvatar string `protobuf:"bytes,28,opt,name=receiver_avatar,json=receiverAvatar,proto3" json:"receiver_avatar,omitempty"`
ReceiverDisplayUserId string `protobuf:"bytes,29,opt,name=receiver_display_user_id,json=receiverDisplayUserId,proto3" json:"receiver_display_user_id,omitempty"`
ReceiverPrettyDisplayUserId string `protobuf:"bytes,30,opt,name=receiver_pretty_display_user_id,json=receiverPrettyDisplayUserId,proto3" json:"receiver_pretty_display_user_id,omitempty"`
}
func (x *RoomGiftSent) Reset() {
@ -1419,6 +1438,62 @@ func (x *RoomGiftSent) GetRealRoomRobotGift() bool {
return false
}
func (x *RoomGiftSent) GetSenderName() string {
if x != nil {
return x.SenderName
}
return ""
}
func (x *RoomGiftSent) GetSenderAvatar() string {
if x != nil {
return x.SenderAvatar
}
return ""
}
func (x *RoomGiftSent) GetSenderDisplayUserId() string {
if x != nil {
return x.SenderDisplayUserId
}
return ""
}
func (x *RoomGiftSent) GetSenderPrettyDisplayUserId() string {
if x != nil {
return x.SenderPrettyDisplayUserId
}
return ""
}
func (x *RoomGiftSent) GetReceiverNickname() string {
if x != nil {
return x.ReceiverNickname
}
return ""
}
func (x *RoomGiftSent) GetReceiverAvatar() string {
if x != nil {
return x.ReceiverAvatar
}
return ""
}
func (x *RoomGiftSent) GetReceiverDisplayUserId() string {
if x != nil {
return x.ReceiverDisplayUserId
}
return ""
}
func (x *RoomGiftSent) GetReceiverPrettyDisplayUserId() string {
if x != nil {
return x.ReceiverPrettyDisplayUserId
}
return ""
}
// RoomRobotLuckyGiftDrawn 是机器人房间专用幸运礼物展示事件。
// 它复用客户端 lucky_gift_drawn 表现协议,但不代表真实奖池抽奖和钱包返奖事实。
type RoomRobotLuckyGiftDrawn struct {
@ -2353,7 +2428,7 @@ var file_proto_events_room_v1_events_proto_rawDesc = []byte{
0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74,
0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x65, 0x78, 0x70, 0x69, 0x72,
0x65, 0x73, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xfa, 0x01, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d, 0x55,
0x65, 0x73, 0x41, 0x74, 0x4d, 0x73, 0x22, 0x95, 0x02, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d, 0x55,
0x73, 0x65, 0x72, 0x4a, 0x6f, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72,
0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
@ -2369,309 +2444,335 @@ var file_proto_events_room_v1_events_proto_rawDesc = []byte{
0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e,
0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f,
0x6e, 0x49, 0x64, 0x22, 0x27, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x4c,
0x65, 0x66, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x48, 0x0a, 0x0a,
0x52, 0x6f, 0x6f, 0x6d, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63,
0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16,
0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0xd8, 0x03, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d, 0x4d,
0x69, 0x63, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74,
0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a,
0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65,
0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x73, 0x65, 0x61, 0x74,
0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x72, 0x6f, 0x6d, 0x53, 0x65, 0x61, 0x74,
0x12, 0x17, 0x0a, 0x07, 0x74, 0x6f, 0x5f, 0x73, 0x65, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28,
0x05, 0x52, 0x06, 0x74, 0x6f, 0x53, 0x65, 0x61, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x69, 0x63, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x69, 0x63, 0x53, 0x65,
0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x75, 0x62, 0x6c, 0x69,
0x73, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06,
0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65,
0x61, 0x73, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f,
0x64, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28,
0x03, 0x52, 0x11, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x44, 0x65, 0x61, 0x64, 0x6c, 0x69,
0x6e, 0x65, 0x4d, 0x73, 0x12, 0x31, 0x0a, 0x15, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f,
0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x0a, 0x20,
0x01, 0x28, 0x03, 0x52, 0x12, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x45, 0x76, 0x65, 0x6e,
0x74, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x63, 0x5f, 0x6d,
0x75, 0x74, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6d, 0x69, 0x63, 0x4d,
0x75, 0x74, 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x61, 0x74, 0x5f, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x61, 0x74, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f,
0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x47, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75,
0x65, 0x22, 0x68, 0x0a, 0x11, 0x52, 0x6f, 0x6f, 0x6d, 0x4d, 0x69, 0x63, 0x53, 0x65, 0x61, 0x74,
0x4c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61,
0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x65,
0x61, 0x74, 0x5f, 0x6e, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x65, 0x61,
0x74, 0x4e, 0x6f, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x03, 0x20,
0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x22, 0x56, 0x0a, 0x16, 0x52,
0x6f, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x74, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, 0x68,
0x6e, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x18,
0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x22, 0x27,
0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x65, 0x66, 0x74, 0x12, 0x17,
0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52,
0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x52, 0x6f, 0x6f, 0x6d, 0x43,
0x6c, 0x6f, 0x73, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75,
0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63,
0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61,
0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f,
0x6e, 0x22, 0xd8, 0x03, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d, 0x4d, 0x69, 0x63, 0x43, 0x68, 0x61,
0x6e, 0x67, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74,
0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b,
0x0a, 0x09, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x73, 0x65, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
0x05, 0x52, 0x08, 0x66, 0x72, 0x6f, 0x6d, 0x53, 0x65, 0x61, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74,
0x6f, 0x5f, 0x73, 0x65, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x74, 0x6f,
0x53, 0x65, 0x61, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05,
0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e,
0x6d, 0x69, 0x63, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x06,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x73, 0x74,
0x61, 0x74, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69,
0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f,
0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12,
0x2e, 0x0a, 0x13, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x64, 0x65, 0x61, 0x64, 0x6c,
0x69, 0x6e, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x70, 0x75,
0x62, 0x6c, 0x69, 0x73, 0x68, 0x44, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x4d, 0x73, 0x12,
0x31, 0x0a, 0x15, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74,
0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65,
0x4d, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x63, 0x5f, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x18,
0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6d, 0x69, 0x63, 0x4d, 0x75, 0x74, 0x65, 0x64, 0x12,
0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x61, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0c,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x61, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
0x12, 0x2a, 0x0a, 0x11, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x67, 0x69, 0x66, 0x74, 0x5f,
0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x74, 0x61, 0x72,
0x67, 0x65, 0x74, 0x47, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x68, 0x0a, 0x11,
0x52, 0x6f, 0x6f, 0x6d, 0x4d, 0x69, 0x63, 0x53, 0x65, 0x61, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x65,
0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55,
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x65, 0x61, 0x74, 0x5f, 0x6e, 0x6f,
0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x65, 0x61, 0x74, 0x4e, 0x6f, 0x12, 0x16,
0x0a, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06,
0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x22, 0x56, 0x0a, 0x16, 0x52, 0x6f, 0x6f, 0x6d, 0x43, 0x68,
0x61, 0x74, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64,
0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73,
0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x7d,
0x0a, 0x13, 0x52, 0x6f, 0x6f, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x68,
0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75,
0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63,
0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61,
0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62,
0x6c, 0x65, 0x64, 0x22, 0x7d, 0x0a, 0x13, 0x52, 0x6f, 0x6f, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x77,
0x6f, 0x72, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63,
0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16,
0x0a, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06,
0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c,
0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28,
0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e,
0x49, 0x64, 0x22, 0x76, 0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43,
0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f,
0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x63,
0x6b, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65,
0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67,
0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69,
0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x76, 0x0a,
0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55,
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74,
0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65,
0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e,
0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x6f, 0x0a, 0x0d, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65,
0x72, 0x4d, 0x75, 0x74, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61,
0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61,
0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64,
0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28,
0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x6f, 0x0a, 0x0d, 0x52, 0x6f,
0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x75, 0x74, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61,
0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12,
0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55,
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x18, 0x03,
0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x22, 0x5a, 0x0a, 0x0e, 0x52,
0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x4b, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x22, 0x0a,
0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65,
0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x5c, 0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x55,
0x73, 0x65, 0x72, 0x55, 0x6e, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61,
0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12,
0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55,
0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xbc, 0x06, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d, 0x47, 0x69,
0x66, 0x74, 0x53, 0x65, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72,
0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c,
0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e,
0x12, 0x14, 0x0a, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52,
0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x22, 0x5a, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73,
0x65, 0x72, 0x4b, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f,
0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e,
0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72,
0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67,
0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52,
0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69,
0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
0x67, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x69, 0x6c,
0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18,
0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x52, 0x65,
0x63, 0x65, 0x69, 0x70, 0x74, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62,
0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f,
0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69,
0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63,
0x6f, 0x69, 0x6e, 0x5f, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52,
0x09, 0x63, 0x6f, 0x69, 0x6e, 0x53, 0x70, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f,
0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x67,
0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65,
0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x74,
0x79, 0x70, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
0x67, 0x69, 0x66, 0x74, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x10,
0x63, 0x70, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65,
0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x70, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x67, 0x69, 0x66, 0x74, 0x4e,
0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x63, 0x6f, 0x6e,
0x5f, 0x75, 0x72, 0x6c, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x67, 0x69, 0x66, 0x74,
0x49, 0x63, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x69, 0x66, 0x74, 0x5f,
0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x11, 0x20,
0x01, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x66, 0x74, 0x41, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x2a, 0x0a, 0x11, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f,
0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x47, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75,
0x65, 0x12, 0x2a, 0x0a, 0x11, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74,
0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x13, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x67, 0x69,
0x66, 0x74, 0x45, 0x66, 0x66, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x22, 0x0a,
0x0d, 0x69, 0x73, 0x5f, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x5f, 0x67, 0x69, 0x66, 0x74, 0x18, 0x14,
0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x73, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x47, 0x69, 0x66,
0x74, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x5f, 0x6c,
0x75, 0x63, 0x6b, 0x79, 0x5f, 0x67, 0x69, 0x66, 0x74, 0x18, 0x15, 0x20, 0x01, 0x28, 0x08, 0x52,
0x12, 0x73, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47,
0x69, 0x66, 0x74, 0x12, 0x2f, 0x0a, 0x14, 0x72, 0x65, 0x61, 0x6c, 0x5f, 0x72, 0x6f, 0x6f, 0x6d,
0x5f, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x5f, 0x67, 0x69, 0x66, 0x74, 0x18, 0x16, 0x20, 0x01, 0x28,
0x08, 0x52, 0x11, 0x72, 0x65, 0x61, 0x6c, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74,
0x47, 0x69, 0x66, 0x74, 0x22, 0xf7, 0x04, 0x0a, 0x17, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62,
0x6f, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x6e,
0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x06, 0x64, 0x72, 0x61, 0x77, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d,
0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63,
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64,
0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24,
0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64,
0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73,
0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18,
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a,
0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28,
0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07,
0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70,
0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c,
0x69, 0x65, 0x72, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6d,
0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x50, 0x70, 0x6d, 0x12, 0x2a, 0x0a, 0x11,
0x62, 0x61, 0x73, 0x65, 0x5f, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e,
0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x77,
0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x3f, 0x0a, 0x1c, 0x72, 0x6f, 0x6f, 0x6d,
0x5f, 0x61, 0x74, 0x6d, 0x6f, 0x73, 0x70, 0x68, 0x65, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x77, 0x61,
0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x19,
0x72, 0x6f, 0x6f, 0x6d, 0x41, 0x74, 0x6d, 0x6f, 0x73, 0x70, 0x68, 0x65, 0x72, 0x65, 0x52, 0x65,
0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x61, 0x63, 0x74,
0x69, 0x76, 0x69, 0x74, 0x79, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x69, 0x64, 0x79, 0x5f, 0x63, 0x6f,
0x69, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x61, 0x63, 0x74, 0x69, 0x76,
0x69, 0x74, 0x79, 0x53, 0x75, 0x62, 0x73, 0x69, 0x64, 0x79, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12,
0x34, 0x0a, 0x16, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x72, 0x65, 0x77,
0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52,
0x14, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64,
0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72,
0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73,
0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0e,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67,
0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x72, 0x6f, 0x62, 0x6f,
0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x52, 0x6f, 0x62, 0x6f, 0x74,
0x12, 0x1c, 0x0a, 0x09, 0x73, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x18, 0x10, 0x20,
0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x22, 0x4a,
0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
0x64, 0x12, 0x14, 0x0a, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
0x52, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65,
0x6e, 0x74, 0x5f, 0x68, 0x65, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63,
0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x65, 0x61, 0x74, 0x22, 0x5f, 0x0a, 0x0f, 0x52, 0x6f,
0x6f, 0x6d, 0x52, 0x61, 0x6e, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x17, 0x0a,
0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06,
0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18,
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a,
0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x94, 0x02, 0x0a, 0x15,
0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64,
0x47, 0x72, 0x61, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f,
0x72, 0x6f, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x77, 0x61,
0x72, 0x64, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12,
0x24, 0x0a, 0x0e, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69,
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x49,
0x74, 0x65, 0x6d, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49,
0x64, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d,
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c,
0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x69, 0x63, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12,
0x19, 0x0a, 0x08, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74,
0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74,
0x75, 0x73, 0x22, 0xeb, 0x03, 0x0a, 0x15, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65,
0x74, 0x46, 0x75, 0x65, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09,
0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76,
0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12,
0x1d, 0x0a, 0x0a, 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x03, 0x20,
0x01, 0x28, 0x03, 0x52, 0x09, 0x61, 0x64, 0x64, 0x65, 0x64, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x30,
0x0a, 0x14, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x65,
0x64, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x65, 0x66,
0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x65, 0x64, 0x46, 0x75, 0x65, 0x6c,
0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x66, 0x75, 0x65,
0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f,
0x77, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74,
0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72,
0x72, 0x65, 0x6e, 0x74, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x75, 0x65, 0x6c,
0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0d, 0x66, 0x75, 0x65, 0x6c, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12,
0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52,
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74,
0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65,
0x73, 0x65, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65,
0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a,
0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x49, 0x64, 0x22, 0x5c, 0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x55, 0x6e,
0x62, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61,
0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61,
0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64,
0x22, 0xce, 0x09, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x6e,
0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65,
0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65,
0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a,
0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74,
0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61,
0x6e, 0x64, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f,
0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64,
0x22, 0x82, 0x04, 0x0a, 0x11, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49,
0x67, 0x6e, 0x69, 0x74, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74,
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65,
0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01,
0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72,
0x72, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x0e,
0x66, 0x75, 0x65, 0x6c, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x04,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x66, 0x75, 0x65, 0x6c, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68,
0x6f, 0x6c, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x64, 0x5f, 0x61,
0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x67, 0x6e, 0x69,
0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x6c, 0x61, 0x75, 0x6e, 0x63,
0x68, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x6c,
0x61, 0x75, 0x6e, 0x63, 0x68, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x62, 0x72, 0x6f,
0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0e, 0x62, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x53, 0x63, 0x6f,
0x70, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65,
0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76,
0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x20,
0x0a, 0x0c, 0x74, 0x6f, 0x70, 0x31, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x70, 0x31, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64,
0x12, 0x26, 0x0a, 0x0f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74,
0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d,
0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f,
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x6f, 0x6f, 0x6d, 0x5f,
0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
0x72, 0x6f, 0x6f, 0x6d, 0x53, 0x68, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x72,
0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18,
0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x76,
0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f, 0x61,
0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65,
0x74, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xe6, 0x02, 0x0a, 0x12, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f,
0x63, 0x6b, 0x65, 0x74, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09,
0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76,
0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12,
0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20,
0x01, 0x28, 0x05, 0x52, 0x09, 0x6e, 0x65, 0x78, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x24,
0x0a, 0x0e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73,
0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64,
0x41, 0x74, 0x4d, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x74,
0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65, 0x74,
0x41, 0x74, 0x4d, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x70, 0x31, 0x5f, 0x75, 0x73, 0x65,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x70, 0x31,
0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65,
0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x27,
0x0a, 0x10, 0x69, 0x6e, 0x5f, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x6e, 0x52, 0x6f, 0x6f, 0x6d,
0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, 0x12, 0x45, 0x0a, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72,
0x64, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70,
0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x2e,
0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74,
0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x56,
0x61, 0x6c, 0x75, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x5f,
0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
0x52, 0x10, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74,
0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65,
0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76,
0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1d,
0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01,
0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x17, 0x0a,
0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x73,
0x70, 0x65, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x69, 0x6e,
0x53, 0x70, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79,
0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x72, 0x79, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49,
0x64, 0x12, 0x24, 0x0a, 0x0e, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x63,
0x6f, 0x64, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x67, 0x69, 0x66, 0x74, 0x54,
0x79, 0x70, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x63, 0x70, 0x5f, 0x72, 0x65,
0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0e, 0x63, 0x70, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70,
0x65, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0f,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x67, 0x69, 0x66, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22,
0x0a, 0x0d, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18,
0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x67, 0x69, 0x66, 0x74, 0x49, 0x63, 0x6f, 0x6e, 0x55,
0x72, 0x6c, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x61, 0x6e, 0x69, 0x6d, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10,
0x67, 0x69, 0x66, 0x74, 0x41, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c,
0x12, 0x2a, 0x0a, 0x11, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x67, 0x69, 0x66, 0x74, 0x5f,
0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x74, 0x61, 0x72,
0x67, 0x65, 0x74, 0x47, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2a, 0x0a, 0x11,
0x67, 0x69, 0x66, 0x74, 0x5f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65,
0x73, 0x18, 0x13, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x67, 0x69, 0x66, 0x74, 0x45, 0x66, 0x66,
0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x69, 0x73, 0x5f, 0x72,
0x6f, 0x62, 0x6f, 0x74, 0x5f, 0x67, 0x69, 0x66, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52,
0x0b, 0x69, 0x73, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x47, 0x69, 0x66, 0x74, 0x12, 0x30, 0x0a, 0x14,
0x73, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x5f, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x5f,
0x67, 0x69, 0x66, 0x74, 0x18, 0x15, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x73, 0x79, 0x6e, 0x74,
0x68, 0x65, 0x74, 0x69, 0x63, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x12, 0x2f,
0x0a, 0x14, 0x72, 0x65, 0x61, 0x6c, 0x5f, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x72, 0x6f, 0x62, 0x6f,
0x74, 0x5f, 0x67, 0x69, 0x66, 0x74, 0x18, 0x16, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x72, 0x65,
0x61, 0x6c, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x47, 0x69, 0x66, 0x74, 0x12,
0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x17,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65,
0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x76, 0x61, 0x74, 0x61,
0x72, 0x18, 0x18, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x41,
0x76, 0x61, 0x74, 0x61, 0x72, 0x12, 0x33, 0x0a, 0x16, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f,
0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x19, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x44, 0x69, 0x73,
0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x40, 0x0a, 0x1d, 0x73, 0x65,
0x6e, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x5f, 0x64, 0x69, 0x73, 0x70,
0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x1a, 0x20, 0x01, 0x28,
0x09, 0x52, 0x19, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44,
0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x11,
0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d,
0x65, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65,
0x72, 0x4e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x63,
0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x1c, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x41, 0x76, 0x61, 0x74,
0x61, 0x72, 0x12, 0x37, 0x0a, 0x18, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x64,
0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x1d,
0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x44, 0x69,
0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x44, 0x0a, 0x1f, 0x72,
0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x5f, 0x64,
0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x1e,
0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x50, 0x72,
0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x22, 0xf7, 0x04, 0x0a, 0x17, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x4c,
0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x6e, 0x12, 0x17, 0x0a,
0x07, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x64, 0x72, 0x61, 0x77, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d,
0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73,
0x65, 0x6e, 0x64, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74,
0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01,
0x28, 0x09, 0x52, 0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69,
0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09,
0x67, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f,
0x6c, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c,
0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72,
0x5f, 0x70, 0x70, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6d, 0x75, 0x6c, 0x74,
0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x50, 0x70, 0x6d, 0x12, 0x2a, 0x0a, 0x11, 0x62, 0x61, 0x73,
0x65, 0x5f, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x09,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64,
0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x3f, 0x0a, 0x1c, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x61, 0x74,
0x6d, 0x6f, 0x73, 0x70, 0x68, 0x65, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f,
0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x19, 0x72, 0x6f, 0x6f,
0x6d, 0x41, 0x74, 0x6d, 0x6f, 0x73, 0x70, 0x68, 0x65, 0x72, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72,
0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69,
0x74, 0x79, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x69, 0x64, 0x79, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73,
0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79,
0x53, 0x75, 0x62, 0x73, 0x69, 0x64, 0x79, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x16,
0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64,
0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x65, 0x66,
0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69,
0x6e, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74,
0x5f, 0x6d, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74,
0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c,
0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28,
0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e,
0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x72, 0x6f, 0x62, 0x6f, 0x74, 0x18, 0x0f,
0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x12, 0x1c, 0x0a,
0x09, 0x73, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08,
0x52, 0x09, 0x73, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x22, 0x4a, 0x0a, 0x0f, 0x52,
0x6f, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x14,
0x0a, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x64,
0x65, 0x6c, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f,
0x68, 0x65, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72,
0x65, 0x6e, 0x74, 0x48, 0x65, 0x61, 0x74, 0x22, 0x5f, 0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x6d, 0x52,
0x61, 0x6e, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65,
0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x03, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66,
0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67,
0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x94, 0x02, 0x0a, 0x15, 0x52, 0x6f, 0x6f,
0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47, 0x72, 0x61,
0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x72, 0x6f, 0x6c,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52,
0x6f, 0x6c, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02,
0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e,
0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x49, 0x74, 0x65, 0x6d,
0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x67,
0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x21,
0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d,
0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20,
0x01, 0x28, 0x09, 0x52, 0x07, 0x69, 0x63, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x19, 0x0a, 0x08,
0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x67, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22,
0xeb, 0x03, 0x0a, 0x15, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x46, 0x75,
0x65, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63,
0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f,
0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18,
0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a,
0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
0x52, 0x09, 0x61, 0x64, 0x64, 0x65, 0x64, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x30, 0x0a, 0x14, 0x65,
0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x66,
0x75, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x65, 0x66, 0x66, 0x65, 0x63,
0x74, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x65, 0x64, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x23, 0x0a,
0x0d, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x05,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x46, 0x75,
0x65, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x75,
0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e,
0x74, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x75, 0x65, 0x6c, 0x5f, 0x74, 0x68,
0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x66,
0x75, 0x65, 0x6c, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x16, 0x0a, 0x06,
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74,
0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x74,
0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65, 0x74,
0x41, 0x74, 0x4d, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75,
0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65,
0x6e, 0x64, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x69,
0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69, 0x66,
0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x75,
0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64,
0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49,
0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67,
0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69,
0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x82, 0x04,
0x0a, 0x11, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x67, 0x6e, 0x69,
0x74, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64,
0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52,
0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e,
0x74, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75,
0x72, 0x72, 0x65, 0x6e, 0x74, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x75, 0x65,
0x6c, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28,
0x03, 0x52, 0x0d, 0x66, 0x75, 0x65, 0x6c, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64,
0x12, 0x22, 0x0a, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d,
0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x64,
0x41, 0x74, 0x4d, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x5f, 0x61,
0x74, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x6c, 0x61, 0x75, 0x6e,
0x63, 0x68, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x62, 0x72, 0x6f, 0x61, 0x64, 0x63,
0x61, 0x73, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0e, 0x62, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12,
0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f,
0x6e, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69,
0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74,
0x6f, 0x70, 0x31, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28,
0x03, 0x52, 0x0a, 0x74, 0x6f, 0x70, 0x31, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x26, 0x0a,
0x0f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64,
0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x55,
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61,
0x6e, 0x64, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x73, 0x68, 0x6f,
0x72, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x6f, 0x6f,
0x6d, 0x53, 0x68, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x72, 0x6f, 0x63, 0x6b,
0x65, 0x74, 0x5f, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0d, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0e, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f, 0x76, 0x65, 0x72, 0x55,
0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d,
0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65, 0x74, 0x41, 0x74,
0x4d, 0x73, 0x22, 0xe6, 0x02, 0x0a, 0x12, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65,
0x74, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63,
0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f,
0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18,
0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a,
0x6e, 0x65, 0x78, 0x74, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05,
0x52, 0x09, 0x6e, 0x65, 0x78, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x24, 0x0a, 0x0e, 0x6c,
0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x04, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0c, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x41, 0x74, 0x4d,
0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73,
0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65, 0x74, 0x41, 0x74, 0x4d,
0x73, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x70, 0x31, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x70, 0x31, 0x55, 0x73, 0x65,
0x72, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x75,
0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x67,
0x6e, 0x69, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x10, 0x69,
0x6e, 0x5f, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18,
0x08, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x6e, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65,
0x72, 0x49, 0x64, 0x73, 0x12, 0x45, 0x0a, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x18,
0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x65, 0x76,
0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6f,
0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47, 0x72, 0x61,
0x6e, 0x74, 0x52, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x93, 0x01, 0x0a, 0x17,
0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64,
0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22, 0x93,
0x01, 0x0a, 0x17, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77,
0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f,
0x63, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72,
0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c,
0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x45, 0x0a,
0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b,
0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f,
0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74,
0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x07, 0x72, 0x65, 0x77,
0x61, 0x72, 0x64, 0x73, 0x42, 0x33, 0x5a, 0x31, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f,
0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x76,
0x65, 0x6e, 0x74, 0x73, 0x2f, 0x72, 0x6f, 0x6f, 0x6d, 0x2f, 0x76, 0x31, 0x3b, 0x72, 0x6f, 0x6f,
0x6d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
0x47, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65,
0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b,
0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20,
0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x45, 0x0a, 0x07, 0x72, 0x65,
0x77, 0x61, 0x72, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e,
0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77,
0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64,
0x73, 0x42, 0x33, 0x5a, 0x31, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c,
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74,
0x73, 0x2f, 0x72, 0x6f, 0x6f, 0x6d, 0x2f, 0x76, 0x31, 0x3b, 0x72, 0x6f, 0x6f, 0x6d, 0x65, 0x76,
0x65, 0x6e, 0x74, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

@ -65,6 +65,8 @@ message RoomUserJoined {
RoomEntryVehicleSnapshot entry_vehicle = 4;
int64 country_id = 5;
int64 region_id = 6;
// is_robot
bool is_robot = 7;
}
// RoomUserLeft
@ -172,6 +174,16 @@ message RoomGiftSent {
bool synthetic_lucky_gift = 21;
// real_room_robot_gift Databi
bool real_room_robot_gift = 22;
// sender_name IM
string sender_name = 23;
string sender_avatar = 24;
string sender_display_user_id = 25;
string sender_pretty_display_user_id = 26;
// receiver_nickname Flutter
string receiver_nickname = 27;
string receiver_avatar = 28;
string receiver_display_user_id = 29;
string receiver_pretty_display_user_id = 30;
}
// RoomRobotLuckyGiftDrawn

File diff suppressed because it is too large Load Diff

View File

@ -643,6 +643,8 @@ message JoinRoomRequest {
RoomEntryVehicleSnapshot entry_vehicle = 4;
int64 actor_country_id = 5;
int64 actor_region_id = 6;
// actor_is_robot room-service gateway false
bool actor_is_robot = 7;
}
// JoinRoomResponse
@ -1010,6 +1012,16 @@ message SendGiftTargetHostScope {
int64 target_agency_owner_user_id = 4;
}
// SendGiftDisplayProfile gateway
// room-service IM Room Cell
message SendGiftDisplayProfile {
int64 user_id = 1;
string username = 2;
string avatar = 3;
string display_user_id = 4;
string pretty_display_user_id = 5;
}
// SendGiftRequest
message SendGiftRequest {
RequestMeta meta = 1;
@ -1037,6 +1049,10 @@ message SendGiftRequest {
string source = 14;
// target_host_scopes target
repeated SendGiftTargetHostScope target_host_scopes = 15;
// sender_display_profile gateway user-service IM
SendGiftDisplayProfile sender_display_profile = 16;
// target_display_profiles IM target_user_id
repeated SendGiftDisplayProfile target_display_profiles = 17;
}
// SendGiftResponse

View File

@ -1142,6 +1142,274 @@ func (x *CheckLoginRiskIPWhitelistResponse) GetWhitelisted() bool {
return false
}
// RegisterRiskConfig 是注册链路读取的风控配置。
type RegisterRiskConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
MaxAccountsPerDevice int32 `protobuf:"varint,2,opt,name=max_accounts_per_device,json=maxAccountsPerDevice,proto3" json:"max_accounts_per_device,omitempty"`
UpdatedAtMs int64 `protobuf:"varint,3,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
UpdatedByAdminId int64 `protobuf:"varint,4,opt,name=updated_by_admin_id,json=updatedByAdminId,proto3" json:"updated_by_admin_id,omitempty"`
}
func (x *RegisterRiskConfig) Reset() {
*x = RegisterRiskConfig{}
mi := &file_proto_user_v1_auth_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RegisterRiskConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RegisterRiskConfig) ProtoMessage() {}
func (x *RegisterRiskConfig) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RegisterRiskConfig.ProtoReflect.Descriptor instead.
func (*RegisterRiskConfig) Descriptor() ([]byte, []int) {
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{15}
}
func (x *RegisterRiskConfig) GetAppCode() string {
if x != nil {
return x.AppCode
}
return ""
}
func (x *RegisterRiskConfig) GetMaxAccountsPerDevice() int32 {
if x != nil {
return x.MaxAccountsPerDevice
}
return 0
}
func (x *RegisterRiskConfig) GetUpdatedAtMs() int64 {
if x != nil {
return x.UpdatedAtMs
}
return 0
}
func (x *RegisterRiskConfig) GetUpdatedByAdminId() int64 {
if x != nil {
return x.UpdatedByAdminId
}
return 0
}
// GetRegisterRiskConfigRequest 查询当前 App 的注册风控配置。
type GetRegisterRiskConfigRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
}
func (x *GetRegisterRiskConfigRequest) Reset() {
*x = GetRegisterRiskConfigRequest{}
mi := &file_proto_user_v1_auth_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetRegisterRiskConfigRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetRegisterRiskConfigRequest) ProtoMessage() {}
func (x *GetRegisterRiskConfigRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetRegisterRiskConfigRequest.ProtoReflect.Descriptor instead.
func (*GetRegisterRiskConfigRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{16}
}
func (x *GetRegisterRiskConfigRequest) GetMeta() *RequestMeta {
if x != nil {
return x.Meta
}
return nil
}
type GetRegisterRiskConfigResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Config *RegisterRiskConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
}
func (x *GetRegisterRiskConfigResponse) Reset() {
*x = GetRegisterRiskConfigResponse{}
mi := &file_proto_user_v1_auth_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetRegisterRiskConfigResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetRegisterRiskConfigResponse) ProtoMessage() {}
func (x *GetRegisterRiskConfigResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetRegisterRiskConfigResponse.ProtoReflect.Descriptor instead.
func (*GetRegisterRiskConfigResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{17}
}
func (x *GetRegisterRiskConfigResponse) GetConfig() *RegisterRiskConfig {
if x != nil {
return x.Config
}
return nil
}
// UpdateRegisterRiskConfigRequest 由后台保存当前 App 的注册风控配置。
type UpdateRegisterRiskConfigRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
MaxAccountsPerDevice int32 `protobuf:"varint,2,opt,name=max_accounts_per_device,json=maxAccountsPerDevice,proto3" json:"max_accounts_per_device,omitempty"`
OperatorAdminId int64 `protobuf:"varint,3,opt,name=operator_admin_id,json=operatorAdminId,proto3" json:"operator_admin_id,omitempty"`
}
func (x *UpdateRegisterRiskConfigRequest) Reset() {
*x = UpdateRegisterRiskConfigRequest{}
mi := &file_proto_user_v1_auth_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateRegisterRiskConfigRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateRegisterRiskConfigRequest) ProtoMessage() {}
func (x *UpdateRegisterRiskConfigRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateRegisterRiskConfigRequest.ProtoReflect.Descriptor instead.
func (*UpdateRegisterRiskConfigRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{18}
}
func (x *UpdateRegisterRiskConfigRequest) GetMeta() *RequestMeta {
if x != nil {
return x.Meta
}
return nil
}
func (x *UpdateRegisterRiskConfigRequest) GetMaxAccountsPerDevice() int32 {
if x != nil {
return x.MaxAccountsPerDevice
}
return 0
}
func (x *UpdateRegisterRiskConfigRequest) GetOperatorAdminId() int64 {
if x != nil {
return x.OperatorAdminId
}
return 0
}
type UpdateRegisterRiskConfigResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Config *RegisterRiskConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
}
func (x *UpdateRegisterRiskConfigResponse) Reset() {
*x = UpdateRegisterRiskConfigResponse{}
mi := &file_proto_user_v1_auth_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateRegisterRiskConfigResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateRegisterRiskConfigResponse) ProtoMessage() {}
func (x *UpdateRegisterRiskConfigResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateRegisterRiskConfigResponse.ProtoReflect.Descriptor instead.
func (*UpdateRegisterRiskConfigResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{19}
}
func (x *UpdateRegisterRiskConfigResponse) GetConfig() *RegisterRiskConfig {
if x != nil {
return x.Config
}
return nil
}
// AppHeartbeatRequest 刷新当前登录会话的 App 在线心跳。
type AppHeartbeatRequest struct {
state protoimpl.MessageState
@ -1155,7 +1423,7 @@ type AppHeartbeatRequest struct {
func (x *AppHeartbeatRequest) Reset() {
*x = AppHeartbeatRequest{}
mi := &file_proto_user_v1_auth_proto_msgTypes[15]
mi := &file_proto_user_v1_auth_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1167,7 +1435,7 @@ func (x *AppHeartbeatRequest) String() string {
func (*AppHeartbeatRequest) ProtoMessage() {}
func (x *AppHeartbeatRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[15]
mi := &file_proto_user_v1_auth_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1180,7 +1448,7 @@ func (x *AppHeartbeatRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use AppHeartbeatRequest.ProtoReflect.Descriptor instead.
func (*AppHeartbeatRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{15}
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{20}
}
func (x *AppHeartbeatRequest) GetMeta() *RequestMeta {
@ -1218,7 +1486,7 @@ type AppHeartbeatResponse struct {
func (x *AppHeartbeatResponse) Reset() {
*x = AppHeartbeatResponse{}
mi := &file_proto_user_v1_auth_proto_msgTypes[16]
mi := &file_proto_user_v1_auth_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1230,7 +1498,7 @@ func (x *AppHeartbeatResponse) String() string {
func (*AppHeartbeatResponse) ProtoMessage() {}
func (x *AppHeartbeatResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_auth_proto_msgTypes[16]
mi := &file_proto_user_v1_auth_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1243,7 +1511,7 @@ func (x *AppHeartbeatResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use AppHeartbeatResponse.ProtoReflect.Descriptor instead.
func (*AppHeartbeatResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{16}
return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{21}
}
func (x *AppHeartbeatResponse) GetAccepted() bool {
@ -1447,83 +1715,138 @@ var file_proto_user_v1_auth_proto_rawDesc = []byte{
0x49, 0x50, 0x57, 0x68, 0x69, 0x74, 0x65, 0x6c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x77, 0x68, 0x69, 0x74, 0x65, 0x6c, 0x69, 0x73, 0x74,
0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x77, 0x68, 0x69, 0x74, 0x65, 0x6c,
0x69, 0x73, 0x74, 0x65, 0x64, 0x22, 0x7d, 0x0a, 0x13, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72,
0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04,
0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61,
0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75,
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69,
0x6f, 0x6e, 0x49, 0x64, 0x22, 0xb0, 0x01, 0x0a, 0x14, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72,
0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a,
0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x68, 0x65, 0x61,
0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0d, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x41, 0x74, 0x4d,
0x73, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65,
0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65,
0x72, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x2e, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75,
0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xdc, 0x06, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70,
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e,
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75,
0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0f, 0x4c, 0x6f,
0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, 0x74, 0x79, 0x12, 0x25, 0x2e,
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f,
0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, 0x74, 0x79, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65,
0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x54, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x12, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72,
0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x12, 0x51, 0x75, 0x69, 0x63, 0x6b,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x2e,
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75,
0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e,
0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b,
0x65, 0x6e, 0x12, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75,
0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f,
0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x4c,
0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73,
0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72,
0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x69, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69,
0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70,
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c,
0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c,
0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a,
0x19, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x69, 0x73, 0x6b, 0x49,
0x50, 0x57, 0x68, 0x69, 0x74, 0x65, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x2f, 0x2e, 0x68, 0x79, 0x61,
0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b,
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x69, 0x73, 0x6b, 0x49, 0x50, 0x57, 0x68, 0x69, 0x74, 0x65,
0x6c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63,
0x6b, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x69, 0x73, 0x6b, 0x49, 0x50, 0x57, 0x68, 0x69, 0x74,
0x65, 0x6c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a,
0x0c, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x22, 0x2e,
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70,
0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76,
0x31, 0x2e, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e,
0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x69, 0x73, 0x74, 0x65, 0x64, 0x22, 0xb9, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74,
0x65, 0x72, 0x52, 0x69, 0x73, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08,
0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x35, 0x0a, 0x17, 0x6d, 0x61, 0x78, 0x5f, 0x61,
0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x64, 0x65, 0x76, 0x69,
0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x14, 0x6d, 0x61, 0x78, 0x41, 0x63, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x73, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x22,
0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18,
0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74,
0x4d, 0x73, 0x12, 0x2d, 0x0a, 0x13, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79,
0x5f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
0x10, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x49,
0x64, 0x22, 0x4e, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72,
0x52, 0x69, 0x73, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74,
0x61, 0x22, 0x5a, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72,
0x52, 0x69, 0x73, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x39, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x69, 0x73, 0x6b, 0x43,
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xb4, 0x01,
0x0a, 0x1f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72,
0x52, 0x69, 0x73, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74,
0x61, 0x12, 0x35, 0x0a, 0x17, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x73, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x05, 0x52, 0x14, 0x6d, 0x61, 0x78, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x50,
0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x6f, 0x70, 0x65, 0x72,
0x61, 0x74, 0x6f, 0x72, 0x5f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x41, 0x64, 0x6d,
0x69, 0x6e, 0x49, 0x64, 0x22, 0x5d, 0x0a, 0x20, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65,
0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x69, 0x73, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66,
0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70,
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65,
0x72, 0x52, 0x69, 0x73, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x22, 0x7d, 0x0a, 0x13, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62,
0x65, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65,
0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70,
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65,
0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x49, 0x64, 0x22, 0xb0, 0x01, 0x0a, 0x14, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62,
0x65, 0x61, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61,
0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61,
0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x68, 0x65, 0x61, 0x72, 0x74,
0x62, 0x65, 0x61, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0d, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x12,
0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d,
0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54,
0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x2e, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65,
0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x05,
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xcd, 0x08, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75,
0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73,
0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x69,
0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, 0x74, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69,
0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x54, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x21,
0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53,
0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76,
0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x12, 0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72,
0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x69, 0x63,
0x6b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73,
0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x69, 0x63, 0x6b, 0x43, 0x72, 0x65, 0x61, 0x74,
0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x57, 0x0a, 0x0c, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
0x12, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65,
0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65,
0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x4c, 0x6f, 0x67,
0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72,
0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76,
0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x69, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42,
0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75,
0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67,
0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63,
0x6b, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x19, 0x43,
0x68, 0x65, 0x63, 0x6b, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x69, 0x73, 0x6b, 0x49, 0x50, 0x57,
0x68, 0x69, 0x74, 0x65, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70,
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x6f,
0x67, 0x69, 0x6e, 0x52, 0x69, 0x73, 0x6b, 0x49, 0x50, 0x57, 0x68, 0x69, 0x74, 0x65, 0x6c, 0x69,
0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c,
0x6f, 0x67, 0x69, 0x6e, 0x52, 0x69, 0x73, 0x6b, 0x49, 0x50, 0x57, 0x68, 0x69, 0x74, 0x65, 0x6c,
0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x47,
0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x69, 0x73, 0x6b, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x12, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65,
0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72,
0x52, 0x69, 0x73, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76,
0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x69, 0x73,
0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x7b, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65,
0x72, 0x52, 0x69, 0x73, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2e, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61,
0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x69, 0x73, 0x6b, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61,
0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x69, 0x73, 0x6b, 0x43, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c,
0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x22, 0x2e, 0x68,
0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70,
0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x2e, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c,
0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x75,
0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@ -1538,7 +1861,7 @@ func file_proto_user_v1_auth_proto_rawDescGZIP() []byte {
return file_proto_user_v1_auth_proto_rawDescData
}
var file_proto_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
var file_proto_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
var file_proto_user_v1_auth_proto_goTypes = []any{
(*LoginPasswordRequest)(nil), // 0: hyapp.user.v1.LoginPasswordRequest
(*LoginThirdPartyRequest)(nil), // 1: hyapp.user.v1.LoginThirdPartyRequest
@ -1555,48 +1878,61 @@ var file_proto_user_v1_auth_proto_goTypes = []any{
(*RecordLoginBlockedResponse)(nil), // 12: hyapp.user.v1.RecordLoginBlockedResponse
(*CheckLoginRiskIPWhitelistRequest)(nil), // 13: hyapp.user.v1.CheckLoginRiskIPWhitelistRequest
(*CheckLoginRiskIPWhitelistResponse)(nil), // 14: hyapp.user.v1.CheckLoginRiskIPWhitelistResponse
(*AppHeartbeatRequest)(nil), // 15: hyapp.user.v1.AppHeartbeatRequest
(*AppHeartbeatResponse)(nil), // 16: hyapp.user.v1.AppHeartbeatResponse
(*RequestMeta)(nil), // 17: hyapp.user.v1.RequestMeta
(*AuthToken)(nil), // 18: hyapp.user.v1.AuthToken
(*RegisterRiskConfig)(nil), // 15: hyapp.user.v1.RegisterRiskConfig
(*GetRegisterRiskConfigRequest)(nil), // 16: hyapp.user.v1.GetRegisterRiskConfigRequest
(*GetRegisterRiskConfigResponse)(nil), // 17: hyapp.user.v1.GetRegisterRiskConfigResponse
(*UpdateRegisterRiskConfigRequest)(nil), // 18: hyapp.user.v1.UpdateRegisterRiskConfigRequest
(*UpdateRegisterRiskConfigResponse)(nil), // 19: hyapp.user.v1.UpdateRegisterRiskConfigResponse
(*AppHeartbeatRequest)(nil), // 20: hyapp.user.v1.AppHeartbeatRequest
(*AppHeartbeatResponse)(nil), // 21: hyapp.user.v1.AppHeartbeatResponse
(*RequestMeta)(nil), // 22: hyapp.user.v1.RequestMeta
(*AuthToken)(nil), // 23: hyapp.user.v1.AuthToken
}
var file_proto_user_v1_auth_proto_depIdxs = []int32{
17, // 0: hyapp.user.v1.LoginPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
17, // 1: hyapp.user.v1.LoginThirdPartyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
18, // 2: hyapp.user.v1.AuthResponse.token:type_name -> hyapp.user.v1.AuthToken
17, // 3: hyapp.user.v1.SetPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
17, // 4: hyapp.user.v1.QuickCreateAccountRequest.meta:type_name -> hyapp.user.v1.RequestMeta
18, // 5: hyapp.user.v1.QuickCreateAccountResponse.token:type_name -> hyapp.user.v1.AuthToken
17, // 6: hyapp.user.v1.RefreshTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta
18, // 7: hyapp.user.v1.RefreshTokenResponse.token:type_name -> hyapp.user.v1.AuthToken
17, // 8: hyapp.user.v1.LogoutRequest.meta:type_name -> hyapp.user.v1.RequestMeta
17, // 9: hyapp.user.v1.RecordLoginBlockedRequest.meta:type_name -> hyapp.user.v1.RequestMeta
17, // 10: hyapp.user.v1.CheckLoginRiskIPWhitelistRequest.meta:type_name -> hyapp.user.v1.RequestMeta
17, // 11: hyapp.user.v1.AppHeartbeatRequest.meta:type_name -> hyapp.user.v1.RequestMeta
18, // 12: hyapp.user.v1.AppHeartbeatResponse.token:type_name -> hyapp.user.v1.AuthToken
0, // 13: hyapp.user.v1.AuthService.LoginPassword:input_type -> hyapp.user.v1.LoginPasswordRequest
1, // 14: hyapp.user.v1.AuthService.LoginThirdParty:input_type -> hyapp.user.v1.LoginThirdPartyRequest
3, // 15: hyapp.user.v1.AuthService.SetPassword:input_type -> hyapp.user.v1.SetPasswordRequest
5, // 16: hyapp.user.v1.AuthService.QuickCreateAccount:input_type -> hyapp.user.v1.QuickCreateAccountRequest
7, // 17: hyapp.user.v1.AuthService.RefreshToken:input_type -> hyapp.user.v1.RefreshTokenRequest
9, // 18: hyapp.user.v1.AuthService.Logout:input_type -> hyapp.user.v1.LogoutRequest
11, // 19: hyapp.user.v1.AuthService.RecordLoginBlocked:input_type -> hyapp.user.v1.RecordLoginBlockedRequest
13, // 20: hyapp.user.v1.AuthService.CheckLoginRiskIPWhitelist:input_type -> hyapp.user.v1.CheckLoginRiskIPWhitelistRequest
15, // 21: hyapp.user.v1.AuthService.AppHeartbeat:input_type -> hyapp.user.v1.AppHeartbeatRequest
2, // 22: hyapp.user.v1.AuthService.LoginPassword:output_type -> hyapp.user.v1.AuthResponse
2, // 23: hyapp.user.v1.AuthService.LoginThirdParty:output_type -> hyapp.user.v1.AuthResponse
4, // 24: hyapp.user.v1.AuthService.SetPassword:output_type -> hyapp.user.v1.SetPasswordResponse
6, // 25: hyapp.user.v1.AuthService.QuickCreateAccount:output_type -> hyapp.user.v1.QuickCreateAccountResponse
8, // 26: hyapp.user.v1.AuthService.RefreshToken:output_type -> hyapp.user.v1.RefreshTokenResponse
10, // 27: hyapp.user.v1.AuthService.Logout:output_type -> hyapp.user.v1.LogoutResponse
12, // 28: hyapp.user.v1.AuthService.RecordLoginBlocked:output_type -> hyapp.user.v1.RecordLoginBlockedResponse
14, // 29: hyapp.user.v1.AuthService.CheckLoginRiskIPWhitelist:output_type -> hyapp.user.v1.CheckLoginRiskIPWhitelistResponse
16, // 30: hyapp.user.v1.AuthService.AppHeartbeat:output_type -> hyapp.user.v1.AppHeartbeatResponse
22, // [22:31] is the sub-list for method output_type
13, // [13:22] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
22, // 0: hyapp.user.v1.LoginPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
22, // 1: hyapp.user.v1.LoginThirdPartyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
23, // 2: hyapp.user.v1.AuthResponse.token:type_name -> hyapp.user.v1.AuthToken
22, // 3: hyapp.user.v1.SetPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta
22, // 4: hyapp.user.v1.QuickCreateAccountRequest.meta:type_name -> hyapp.user.v1.RequestMeta
23, // 5: hyapp.user.v1.QuickCreateAccountResponse.token:type_name -> hyapp.user.v1.AuthToken
22, // 6: hyapp.user.v1.RefreshTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta
23, // 7: hyapp.user.v1.RefreshTokenResponse.token:type_name -> hyapp.user.v1.AuthToken
22, // 8: hyapp.user.v1.LogoutRequest.meta:type_name -> hyapp.user.v1.RequestMeta
22, // 9: hyapp.user.v1.RecordLoginBlockedRequest.meta:type_name -> hyapp.user.v1.RequestMeta
22, // 10: hyapp.user.v1.CheckLoginRiskIPWhitelistRequest.meta:type_name -> hyapp.user.v1.RequestMeta
22, // 11: hyapp.user.v1.GetRegisterRiskConfigRequest.meta:type_name -> hyapp.user.v1.RequestMeta
15, // 12: hyapp.user.v1.GetRegisterRiskConfigResponse.config:type_name -> hyapp.user.v1.RegisterRiskConfig
22, // 13: hyapp.user.v1.UpdateRegisterRiskConfigRequest.meta:type_name -> hyapp.user.v1.RequestMeta
15, // 14: hyapp.user.v1.UpdateRegisterRiskConfigResponse.config:type_name -> hyapp.user.v1.RegisterRiskConfig
22, // 15: hyapp.user.v1.AppHeartbeatRequest.meta:type_name -> hyapp.user.v1.RequestMeta
23, // 16: hyapp.user.v1.AppHeartbeatResponse.token:type_name -> hyapp.user.v1.AuthToken
0, // 17: hyapp.user.v1.AuthService.LoginPassword:input_type -> hyapp.user.v1.LoginPasswordRequest
1, // 18: hyapp.user.v1.AuthService.LoginThirdParty:input_type -> hyapp.user.v1.LoginThirdPartyRequest
3, // 19: hyapp.user.v1.AuthService.SetPassword:input_type -> hyapp.user.v1.SetPasswordRequest
5, // 20: hyapp.user.v1.AuthService.QuickCreateAccount:input_type -> hyapp.user.v1.QuickCreateAccountRequest
7, // 21: hyapp.user.v1.AuthService.RefreshToken:input_type -> hyapp.user.v1.RefreshTokenRequest
9, // 22: hyapp.user.v1.AuthService.Logout:input_type -> hyapp.user.v1.LogoutRequest
11, // 23: hyapp.user.v1.AuthService.RecordLoginBlocked:input_type -> hyapp.user.v1.RecordLoginBlockedRequest
13, // 24: hyapp.user.v1.AuthService.CheckLoginRiskIPWhitelist:input_type -> hyapp.user.v1.CheckLoginRiskIPWhitelistRequest
16, // 25: hyapp.user.v1.AuthService.GetRegisterRiskConfig:input_type -> hyapp.user.v1.GetRegisterRiskConfigRequest
18, // 26: hyapp.user.v1.AuthService.UpdateRegisterRiskConfig:input_type -> hyapp.user.v1.UpdateRegisterRiskConfigRequest
20, // 27: hyapp.user.v1.AuthService.AppHeartbeat:input_type -> hyapp.user.v1.AppHeartbeatRequest
2, // 28: hyapp.user.v1.AuthService.LoginPassword:output_type -> hyapp.user.v1.AuthResponse
2, // 29: hyapp.user.v1.AuthService.LoginThirdParty:output_type -> hyapp.user.v1.AuthResponse
4, // 30: hyapp.user.v1.AuthService.SetPassword:output_type -> hyapp.user.v1.SetPasswordResponse
6, // 31: hyapp.user.v1.AuthService.QuickCreateAccount:output_type -> hyapp.user.v1.QuickCreateAccountResponse
8, // 32: hyapp.user.v1.AuthService.RefreshToken:output_type -> hyapp.user.v1.RefreshTokenResponse
10, // 33: hyapp.user.v1.AuthService.Logout:output_type -> hyapp.user.v1.LogoutResponse
12, // 34: hyapp.user.v1.AuthService.RecordLoginBlocked:output_type -> hyapp.user.v1.RecordLoginBlockedResponse
14, // 35: hyapp.user.v1.AuthService.CheckLoginRiskIPWhitelist:output_type -> hyapp.user.v1.CheckLoginRiskIPWhitelistResponse
17, // 36: hyapp.user.v1.AuthService.GetRegisterRiskConfig:output_type -> hyapp.user.v1.GetRegisterRiskConfigResponse
19, // 37: hyapp.user.v1.AuthService.UpdateRegisterRiskConfig:output_type -> hyapp.user.v1.UpdateRegisterRiskConfigResponse
21, // 38: hyapp.user.v1.AuthService.AppHeartbeat:output_type -> hyapp.user.v1.AppHeartbeatResponse
28, // [28:39] is the sub-list for method output_type
17, // [17:28] is the sub-list for method input_type
17, // [17:17] is the sub-list for extension type_name
17, // [17:17] is the sub-list for extension extendee
0, // [0:17] is the sub-list for field type_name
}
func init() { file_proto_user_v1_auth_proto_init() }
@ -1611,7 +1947,7 @@ func file_proto_user_v1_auth_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_user_v1_auth_proto_rawDesc,
NumEnums: 0,
NumMessages: 17,
NumMessages: 22,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -135,6 +135,34 @@ message CheckLoginRiskIPWhitelistResponse {
bool whitelisted = 1;
}
// RegisterRiskConfig
message RegisterRiskConfig {
string app_code = 1;
int32 max_accounts_per_device = 2;
int64 updated_at_ms = 3;
int64 updated_by_admin_id = 4;
}
// GetRegisterRiskConfigRequest App
message GetRegisterRiskConfigRequest {
RequestMeta meta = 1;
}
message GetRegisterRiskConfigResponse {
RegisterRiskConfig config = 1;
}
// UpdateRegisterRiskConfigRequest App
message UpdateRegisterRiskConfigRequest {
RequestMeta meta = 1;
int32 max_accounts_per_device = 2;
int64 operator_admin_id = 3;
}
message UpdateRegisterRiskConfigResponse {
RegisterRiskConfig config = 1;
}
// AppHeartbeatRequest App 线
message AppHeartbeatRequest {
RequestMeta meta = 1;
@ -160,5 +188,7 @@ service AuthService {
rpc Logout(LogoutRequest) returns (LogoutResponse);
rpc RecordLoginBlocked(RecordLoginBlockedRequest) returns (RecordLoginBlockedResponse);
rpc CheckLoginRiskIPWhitelist(CheckLoginRiskIPWhitelistRequest) returns (CheckLoginRiskIPWhitelistResponse);
rpc GetRegisterRiskConfig(GetRegisterRiskConfigRequest) returns (GetRegisterRiskConfigResponse);
rpc UpdateRegisterRiskConfig(UpdateRegisterRiskConfigRequest) returns (UpdateRegisterRiskConfigResponse);
rpc AppHeartbeat(AppHeartbeatRequest) returns (AppHeartbeatResponse);
}

View File

@ -27,6 +27,8 @@ const (
AuthService_Logout_FullMethodName = "/hyapp.user.v1.AuthService/Logout"
AuthService_RecordLoginBlocked_FullMethodName = "/hyapp.user.v1.AuthService/RecordLoginBlocked"
AuthService_CheckLoginRiskIPWhitelist_FullMethodName = "/hyapp.user.v1.AuthService/CheckLoginRiskIPWhitelist"
AuthService_GetRegisterRiskConfig_FullMethodName = "/hyapp.user.v1.AuthService/GetRegisterRiskConfig"
AuthService_UpdateRegisterRiskConfig_FullMethodName = "/hyapp.user.v1.AuthService/UpdateRegisterRiskConfig"
AuthService_AppHeartbeat_FullMethodName = "/hyapp.user.v1.AuthService/AppHeartbeat"
)
@ -44,6 +46,8 @@ type AuthServiceClient interface {
Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error)
RecordLoginBlocked(ctx context.Context, in *RecordLoginBlockedRequest, opts ...grpc.CallOption) (*RecordLoginBlockedResponse, error)
CheckLoginRiskIPWhitelist(ctx context.Context, in *CheckLoginRiskIPWhitelistRequest, opts ...grpc.CallOption) (*CheckLoginRiskIPWhitelistResponse, error)
GetRegisterRiskConfig(ctx context.Context, in *GetRegisterRiskConfigRequest, opts ...grpc.CallOption) (*GetRegisterRiskConfigResponse, error)
UpdateRegisterRiskConfig(ctx context.Context, in *UpdateRegisterRiskConfigRequest, opts ...grpc.CallOption) (*UpdateRegisterRiskConfigResponse, error)
AppHeartbeat(ctx context.Context, in *AppHeartbeatRequest, opts ...grpc.CallOption) (*AppHeartbeatResponse, error)
}
@ -135,6 +139,26 @@ func (c *authServiceClient) CheckLoginRiskIPWhitelist(ctx context.Context, in *C
return out, nil
}
func (c *authServiceClient) GetRegisterRiskConfig(ctx context.Context, in *GetRegisterRiskConfigRequest, opts ...grpc.CallOption) (*GetRegisterRiskConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetRegisterRiskConfigResponse)
err := c.cc.Invoke(ctx, AuthService_GetRegisterRiskConfig_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) UpdateRegisterRiskConfig(ctx context.Context, in *UpdateRegisterRiskConfigRequest, opts ...grpc.CallOption) (*UpdateRegisterRiskConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateRegisterRiskConfigResponse)
err := c.cc.Invoke(ctx, AuthService_UpdateRegisterRiskConfig_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) AppHeartbeat(ctx context.Context, in *AppHeartbeatRequest, opts ...grpc.CallOption) (*AppHeartbeatResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AppHeartbeatResponse)
@ -159,6 +183,8 @@ type AuthServiceServer interface {
Logout(context.Context, *LogoutRequest) (*LogoutResponse, error)
RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error)
CheckLoginRiskIPWhitelist(context.Context, *CheckLoginRiskIPWhitelistRequest) (*CheckLoginRiskIPWhitelistResponse, error)
GetRegisterRiskConfig(context.Context, *GetRegisterRiskConfigRequest) (*GetRegisterRiskConfigResponse, error)
UpdateRegisterRiskConfig(context.Context, *UpdateRegisterRiskConfigRequest) (*UpdateRegisterRiskConfigResponse, error)
AppHeartbeat(context.Context, *AppHeartbeatRequest) (*AppHeartbeatResponse, error)
mustEmbedUnimplementedAuthServiceServer()
}
@ -194,6 +220,12 @@ func (UnimplementedAuthServiceServer) RecordLoginBlocked(context.Context, *Recor
func (UnimplementedAuthServiceServer) CheckLoginRiskIPWhitelist(context.Context, *CheckLoginRiskIPWhitelistRequest) (*CheckLoginRiskIPWhitelistResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckLoginRiskIPWhitelist not implemented")
}
func (UnimplementedAuthServiceServer) GetRegisterRiskConfig(context.Context, *GetRegisterRiskConfigRequest) (*GetRegisterRiskConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetRegisterRiskConfig not implemented")
}
func (UnimplementedAuthServiceServer) UpdateRegisterRiskConfig(context.Context, *UpdateRegisterRiskConfigRequest) (*UpdateRegisterRiskConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateRegisterRiskConfig not implemented")
}
func (UnimplementedAuthServiceServer) AppHeartbeat(context.Context, *AppHeartbeatRequest) (*AppHeartbeatResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AppHeartbeat not implemented")
}
@ -362,6 +394,42 @@ func _AuthService_CheckLoginRiskIPWhitelist_Handler(srv interface{}, ctx context
return interceptor(ctx, in, info, handler)
}
func _AuthService_GetRegisterRiskConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetRegisterRiskConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).GetRegisterRiskConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_GetRegisterRiskConfig_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).GetRegisterRiskConfig(ctx, req.(*GetRegisterRiskConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_UpdateRegisterRiskConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateRegisterRiskConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).UpdateRegisterRiskConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_UpdateRegisterRiskConfig_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).UpdateRegisterRiskConfig(ctx, req.(*UpdateRegisterRiskConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_AppHeartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AppHeartbeatRequest)
if err := dec(in); err != nil {
@ -419,6 +487,14 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{
MethodName: "CheckLoginRiskIPWhitelist",
Handler: _AuthService_CheckLoginRiskIPWhitelist_Handler,
},
{
MethodName: "GetRegisterRiskConfig",
Handler: _AuthService_GetRegisterRiskConfig_Handler,
},
{
MethodName: "UpdateRegisterRiskConfig",
Handler: _AuthService_UpdateRegisterRiskConfig_Handler,
},
{
MethodName: "AppHeartbeat",
Handler: _AuthService_AppHeartbeat_Handler,

File diff suppressed because it is too large Load Diff

View File

@ -277,6 +277,16 @@ message ListRoleInvitationsResponse {
repeated RoleInvitation invitations = 1;
}
message GetRoleInvitationRequest {
RequestMeta meta = 1;
int64 actor_user_id = 2;
int64 invitation_id = 3;
}
message GetRoleInvitationResponse {
RoleInvitation invitation = 1;
}
message ProcessRoleInvitationRequest {
RequestMeta meta = 1;
string command_id = 2;
@ -513,6 +523,7 @@ service UserHostService {
rpc ListBDLeaderAgencies(ListBDLeaderAgenciesRequest) returns (ListBDLeaderAgenciesResponse);
rpc ListBDAgencies(ListBDAgenciesRequest) returns (ListBDAgenciesResponse);
rpc ListRoleInvitations(ListRoleInvitationsRequest) returns (ListRoleInvitationsResponse);
rpc GetRoleInvitation(GetRoleInvitationRequest) returns (GetRoleInvitationResponse);
rpc ProcessRoleInvitation(ProcessRoleInvitationRequest) returns (ProcessRoleInvitationResponse);
rpc GetHostProfile(GetHostProfileRequest) returns (GetHostProfileResponse);
rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse);

View File

@ -30,6 +30,7 @@ const (
UserHostService_ListBDLeaderAgencies_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDLeaderAgencies"
UserHostService_ListBDAgencies_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDAgencies"
UserHostService_ListRoleInvitations_FullMethodName = "/hyapp.user.v1.UserHostService/ListRoleInvitations"
UserHostService_GetRoleInvitation_FullMethodName = "/hyapp.user.v1.UserHostService/GetRoleInvitation"
UserHostService_ProcessRoleInvitation_FullMethodName = "/hyapp.user.v1.UserHostService/ProcessRoleInvitation"
UserHostService_GetHostProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetHostProfile"
UserHostService_GetBDProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetBDProfile"
@ -59,6 +60,7 @@ type UserHostServiceClient interface {
ListBDLeaderAgencies(ctx context.Context, in *ListBDLeaderAgenciesRequest, opts ...grpc.CallOption) (*ListBDLeaderAgenciesResponse, error)
ListBDAgencies(ctx context.Context, in *ListBDAgenciesRequest, opts ...grpc.CallOption) (*ListBDAgenciesResponse, error)
ListRoleInvitations(ctx context.Context, in *ListRoleInvitationsRequest, opts ...grpc.CallOption) (*ListRoleInvitationsResponse, error)
GetRoleInvitation(ctx context.Context, in *GetRoleInvitationRequest, opts ...grpc.CallOption) (*GetRoleInvitationResponse, error)
ProcessRoleInvitation(ctx context.Context, in *ProcessRoleInvitationRequest, opts ...grpc.CallOption) (*ProcessRoleInvitationResponse, error)
GetHostProfile(ctx context.Context, in *GetHostProfileRequest, opts ...grpc.CallOption) (*GetHostProfileResponse, error)
GetBDProfile(ctx context.Context, in *GetBDProfileRequest, opts ...grpc.CallOption) (*GetBDProfileResponse, error)
@ -189,6 +191,16 @@ func (c *userHostServiceClient) ListRoleInvitations(ctx context.Context, in *Lis
return out, nil
}
func (c *userHostServiceClient) GetRoleInvitation(ctx context.Context, in *GetRoleInvitationRequest, opts ...grpc.CallOption) (*GetRoleInvitationResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetRoleInvitationResponse)
err := c.cc.Invoke(ctx, UserHostService_GetRoleInvitation_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userHostServiceClient) ProcessRoleInvitation(ctx context.Context, in *ProcessRoleInvitationRequest, opts ...grpc.CallOption) (*ProcessRoleInvitationResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ProcessRoleInvitationResponse)
@ -306,6 +318,7 @@ type UserHostServiceServer interface {
ListBDLeaderAgencies(context.Context, *ListBDLeaderAgenciesRequest) (*ListBDLeaderAgenciesResponse, error)
ListBDAgencies(context.Context, *ListBDAgenciesRequest) (*ListBDAgenciesResponse, error)
ListRoleInvitations(context.Context, *ListRoleInvitationsRequest) (*ListRoleInvitationsResponse, error)
GetRoleInvitation(context.Context, *GetRoleInvitationRequest) (*GetRoleInvitationResponse, error)
ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error)
GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, error)
GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error)
@ -359,6 +372,9 @@ func (UnimplementedUserHostServiceServer) ListBDAgencies(context.Context, *ListB
func (UnimplementedUserHostServiceServer) ListRoleInvitations(context.Context, *ListRoleInvitationsRequest) (*ListRoleInvitationsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRoleInvitations not implemented")
}
func (UnimplementedUserHostServiceServer) GetRoleInvitation(context.Context, *GetRoleInvitationRequest) (*GetRoleInvitationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetRoleInvitation not implemented")
}
func (UnimplementedUserHostServiceServer) ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ProcessRoleInvitation not implemented")
}
@ -608,6 +624,24 @@ func _UserHostService_ListRoleInvitations_Handler(srv interface{}, ctx context.C
return interceptor(ctx, in, info, handler)
}
func _UserHostService_GetRoleInvitation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetRoleInvitationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserHostServiceServer).GetRoleInvitation(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserHostService_GetRoleInvitation_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserHostServiceServer).GetRoleInvitation(ctx, req.(*GetRoleInvitationRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserHostService_ProcessRoleInvitation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ProcessRoleInvitationRequest)
if err := dec(in); err != nil {
@ -839,6 +873,10 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListRoleInvitations",
Handler: _UserHostService_ListRoleInvitations_Handler,
},
{
MethodName: "GetRoleInvitation",
Handler: _UserHostService_GetRoleInvitation_Handler,
},
{
MethodName: "ProcessRoleInvitation",
Handler: _UserHostService_ProcessRoleInvitation_Handler,

File diff suppressed because it is too large Load Diff

View File

@ -212,7 +212,7 @@ message AdminCreditAssetResponse {
AssetBalance balance = 2;
}
// AdminCreditCoinSellerStockRequest active
// AdminCreditCoinSellerStockRequest
message AdminCreditCoinSellerStockRequest {
string command_id = 1;
int64 seller_user_id = 2;
@ -494,6 +494,9 @@ message ResourceGrant {
repeated ResourceGrantItem items = 11;
int64 created_at_ms = 12;
int64 updated_at_ms = 13;
int64 revoked_at_ms = 14;
int64 revoked_by_user_id = 15;
string revoke_reason = 16;
}
message ResourceGroupItemInput {
@ -793,6 +796,14 @@ message GrantResourceGroupRequest {
string grant_source = 7;
}
message RevokeResourceGrantRequest {
string request_id = 1;
string app_code = 2;
string grant_id = 3;
string reason = 4;
int64 operator_user_id = 5;
}
message ResourceGrantResponse {
ResourceGrant grant = 1;
}
@ -1305,6 +1316,44 @@ message CreateH5RechargeOrderRequest {
string payer_email = 17;
}
message CreateTemporaryRechargeOrderRequest {
string request_id = 1;
string app_code = 2;
string command_id = 3;
int64 coin_amount = 4;
int64 usd_minor_amount = 5;
string provider_code = 6;
int64 payment_method_id = 7;
string return_url = 8;
string notify_url = 9;
string client_ip = 10;
string language = 11;
string payer_name = 12;
string payer_account = 13;
string payer_email = 14;
}
message GetTemporaryRechargeOrderRequest {
string request_id = 1;
string app_code = 2;
string order_id = 3;
}
message ListTemporaryRechargeOrdersRequest {
string request_id = 1;
string app_code = 2;
string status = 3;
string provider_code = 4;
string keyword = 5;
int32 page = 6;
int32 page_size = 7;
}
message ListTemporaryRechargeOrdersResponse {
repeated ExternalRechargeOrder orders = 1;
int64 total = 2;
}
message SubmitH5RechargeTxRequest {
string request_id = 1;
string app_code = 2;
@ -1989,6 +2038,7 @@ service WalletService {
rpc UpsertGiftTypeConfig(UpsertGiftTypeConfigRequest) returns (GiftTypeConfigResponse);
rpc GrantResource(GrantResourceRequest) returns (ResourceGrantResponse);
rpc GrantResourceGroup(GrantResourceGroupRequest) returns (ResourceGrantResponse);
rpc RevokeResourceGrant(RevokeResourceGrantRequest) returns (ResourceGrantResponse);
rpc ListUserResources(ListUserResourcesRequest) returns (ListUserResourcesResponse);
rpc EquipUserResource(EquipUserResourceRequest) returns (EquipUserResourceResponse);
rpc UnequipUserResource(UnequipUserResourceRequest) returns (UnequipUserResourceResponse);
@ -2014,6 +2064,9 @@ service WalletService {
rpc UpdateThirdPartyPaymentRate(UpdateThirdPartyPaymentRateRequest) returns (ThirdPartyPaymentMethodResponse);
rpc ListH5RechargeOptions(H5RechargeOptionsRequest) returns (H5RechargeOptionsResponse);
rpc CreateH5RechargeOrder(CreateH5RechargeOrderRequest) returns (H5RechargeOrderResponse);
rpc CreateTemporaryRechargeOrder(CreateTemporaryRechargeOrderRequest) returns (H5RechargeOrderResponse);
rpc GetTemporaryRechargeOrder(GetTemporaryRechargeOrderRequest) returns (H5RechargeOrderResponse);
rpc ListTemporaryRechargeOrders(ListTemporaryRechargeOrdersRequest) returns (ListTemporaryRechargeOrdersResponse);
rpc SubmitH5RechargeTx(SubmitH5RechargeTxRequest) returns (H5RechargeOrderResponse);
rpc GetH5RechargeOrder(GetH5RechargeOrderRequest) returns (H5RechargeOrderResponse);
rpc HandleMifapayNotify(HandleMifapayNotifyRequest) returns (HandleMifapayNotifyResponse);

View File

@ -232,6 +232,7 @@ const (
WalletService_UpsertGiftTypeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertGiftTypeConfig"
WalletService_GrantResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResource"
WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup"
WalletService_RevokeResourceGrant_FullMethodName = "/hyapp.wallet.v1.WalletService/RevokeResourceGrant"
WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources"
WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource"
WalletService_UnequipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UnequipUserResource"
@ -257,6 +258,9 @@ const (
WalletService_UpdateThirdPartyPaymentRate_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateThirdPartyPaymentRate"
WalletService_ListH5RechargeOptions_FullMethodName = "/hyapp.wallet.v1.WalletService/ListH5RechargeOptions"
WalletService_CreateH5RechargeOrder_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateH5RechargeOrder"
WalletService_CreateTemporaryRechargeOrder_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateTemporaryRechargeOrder"
WalletService_GetTemporaryRechargeOrder_FullMethodName = "/hyapp.wallet.v1.WalletService/GetTemporaryRechargeOrder"
WalletService_ListTemporaryRechargeOrders_FullMethodName = "/hyapp.wallet.v1.WalletService/ListTemporaryRechargeOrders"
WalletService_SubmitH5RechargeTx_FullMethodName = "/hyapp.wallet.v1.WalletService/SubmitH5RechargeTx"
WalletService_GetH5RechargeOrder_FullMethodName = "/hyapp.wallet.v1.WalletService/GetH5RechargeOrder"
WalletService_HandleMifapayNotify_FullMethodName = "/hyapp.wallet.v1.WalletService/HandleMifapayNotify"
@ -325,6 +329,7 @@ type WalletServiceClient interface {
UpsertGiftTypeConfig(ctx context.Context, in *UpsertGiftTypeConfigRequest, opts ...grpc.CallOption) (*GiftTypeConfigResponse, error)
GrantResource(ctx context.Context, in *GrantResourceRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
GrantResourceGroup(ctx context.Context, in *GrantResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
RevokeResourceGrant(ctx context.Context, in *RevokeResourceGrantRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error)
EquipUserResource(ctx context.Context, in *EquipUserResourceRequest, opts ...grpc.CallOption) (*EquipUserResourceResponse, error)
UnequipUserResource(ctx context.Context, in *UnequipUserResourceRequest, opts ...grpc.CallOption) (*UnequipUserResourceResponse, error)
@ -350,6 +355,9 @@ type WalletServiceClient interface {
UpdateThirdPartyPaymentRate(ctx context.Context, in *UpdateThirdPartyPaymentRateRequest, opts ...grpc.CallOption) (*ThirdPartyPaymentMethodResponse, error)
ListH5RechargeOptions(ctx context.Context, in *H5RechargeOptionsRequest, opts ...grpc.CallOption) (*H5RechargeOptionsResponse, error)
CreateH5RechargeOrder(ctx context.Context, in *CreateH5RechargeOrderRequest, opts ...grpc.CallOption) (*H5RechargeOrderResponse, error)
CreateTemporaryRechargeOrder(ctx context.Context, in *CreateTemporaryRechargeOrderRequest, opts ...grpc.CallOption) (*H5RechargeOrderResponse, error)
GetTemporaryRechargeOrder(ctx context.Context, in *GetTemporaryRechargeOrderRequest, opts ...grpc.CallOption) (*H5RechargeOrderResponse, error)
ListTemporaryRechargeOrders(ctx context.Context, in *ListTemporaryRechargeOrdersRequest, opts ...grpc.CallOption) (*ListTemporaryRechargeOrdersResponse, error)
SubmitH5RechargeTx(ctx context.Context, in *SubmitH5RechargeTxRequest, opts ...grpc.CallOption) (*H5RechargeOrderResponse, error)
GetH5RechargeOrder(ctx context.Context, in *GetH5RechargeOrderRequest, opts ...grpc.CallOption) (*H5RechargeOrderResponse, error)
HandleMifapayNotify(ctx context.Context, in *HandleMifapayNotifyRequest, opts ...grpc.CallOption) (*HandleMifapayNotifyResponse, error)
@ -699,6 +707,16 @@ func (c *walletServiceClient) GrantResourceGroup(ctx context.Context, in *GrantR
return out, nil
}
func (c *walletServiceClient) RevokeResourceGrant(ctx context.Context, in *RevokeResourceGrantRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResourceGrantResponse)
err := c.cc.Invoke(ctx, WalletService_RevokeResourceGrant_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListUserResourcesResponse)
@ -949,6 +967,36 @@ func (c *walletServiceClient) CreateH5RechargeOrder(ctx context.Context, in *Cre
return out, nil
}
func (c *walletServiceClient) CreateTemporaryRechargeOrder(ctx context.Context, in *CreateTemporaryRechargeOrderRequest, opts ...grpc.CallOption) (*H5RechargeOrderResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(H5RechargeOrderResponse)
err := c.cc.Invoke(ctx, WalletService_CreateTemporaryRechargeOrder_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) GetTemporaryRechargeOrder(ctx context.Context, in *GetTemporaryRechargeOrderRequest, opts ...grpc.CallOption) (*H5RechargeOrderResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(H5RechargeOrderResponse)
err := c.cc.Invoke(ctx, WalletService_GetTemporaryRechargeOrder_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) ListTemporaryRechargeOrders(ctx context.Context, in *ListTemporaryRechargeOrdersRequest, opts ...grpc.CallOption) (*ListTemporaryRechargeOrdersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListTemporaryRechargeOrdersResponse)
err := c.cc.Invoke(ctx, WalletService_ListTemporaryRechargeOrders_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) SubmitH5RechargeTx(ctx context.Context, in *SubmitH5RechargeTxRequest, opts ...grpc.CallOption) (*H5RechargeOrderResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(H5RechargeOrderResponse)
@ -1276,6 +1324,7 @@ type WalletServiceServer interface {
UpsertGiftTypeConfig(context.Context, *UpsertGiftTypeConfigRequest) (*GiftTypeConfigResponse, error)
GrantResource(context.Context, *GrantResourceRequest) (*ResourceGrantResponse, error)
GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error)
RevokeResourceGrant(context.Context, *RevokeResourceGrantRequest) (*ResourceGrantResponse, error)
ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error)
EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error)
UnequipUserResource(context.Context, *UnequipUserResourceRequest) (*UnequipUserResourceResponse, error)
@ -1301,6 +1350,9 @@ type WalletServiceServer interface {
UpdateThirdPartyPaymentRate(context.Context, *UpdateThirdPartyPaymentRateRequest) (*ThirdPartyPaymentMethodResponse, error)
ListH5RechargeOptions(context.Context, *H5RechargeOptionsRequest) (*H5RechargeOptionsResponse, error)
CreateH5RechargeOrder(context.Context, *CreateH5RechargeOrderRequest) (*H5RechargeOrderResponse, error)
CreateTemporaryRechargeOrder(context.Context, *CreateTemporaryRechargeOrderRequest) (*H5RechargeOrderResponse, error)
GetTemporaryRechargeOrder(context.Context, *GetTemporaryRechargeOrderRequest) (*H5RechargeOrderResponse, error)
ListTemporaryRechargeOrders(context.Context, *ListTemporaryRechargeOrdersRequest) (*ListTemporaryRechargeOrdersResponse, error)
SubmitH5RechargeTx(context.Context, *SubmitH5RechargeTxRequest) (*H5RechargeOrderResponse, error)
GetH5RechargeOrder(context.Context, *GetH5RechargeOrderRequest) (*H5RechargeOrderResponse, error)
HandleMifapayNotify(context.Context, *HandleMifapayNotifyRequest) (*HandleMifapayNotifyResponse, error)
@ -1433,6 +1485,9 @@ func (UnimplementedWalletServiceServer) GrantResource(context.Context, *GrantRes
func (UnimplementedWalletServiceServer) GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GrantResourceGroup not implemented")
}
func (UnimplementedWalletServiceServer) RevokeResourceGrant(context.Context, *RevokeResourceGrantRequest) (*ResourceGrantResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RevokeResourceGrant not implemented")
}
func (UnimplementedWalletServiceServer) ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListUserResources not implemented")
}
@ -1508,6 +1563,15 @@ func (UnimplementedWalletServiceServer) ListH5RechargeOptions(context.Context, *
func (UnimplementedWalletServiceServer) CreateH5RechargeOrder(context.Context, *CreateH5RechargeOrderRequest) (*H5RechargeOrderResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateH5RechargeOrder not implemented")
}
func (UnimplementedWalletServiceServer) CreateTemporaryRechargeOrder(context.Context, *CreateTemporaryRechargeOrderRequest) (*H5RechargeOrderResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateTemporaryRechargeOrder not implemented")
}
func (UnimplementedWalletServiceServer) GetTemporaryRechargeOrder(context.Context, *GetTemporaryRechargeOrderRequest) (*H5RechargeOrderResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetTemporaryRechargeOrder not implemented")
}
func (UnimplementedWalletServiceServer) ListTemporaryRechargeOrders(context.Context, *ListTemporaryRechargeOrdersRequest) (*ListTemporaryRechargeOrdersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListTemporaryRechargeOrders not implemented")
}
func (UnimplementedWalletServiceServer) SubmitH5RechargeTx(context.Context, *SubmitH5RechargeTxRequest) (*H5RechargeOrderResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SubmitH5RechargeTx not implemented")
}
@ -2174,6 +2238,24 @@ func _WalletService_GrantResourceGroup_Handler(srv interface{}, ctx context.Cont
return interceptor(ctx, in, info, handler)
}
func _WalletService_RevokeResourceGrant_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RevokeResourceGrantRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).RevokeResourceGrant(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_RevokeResourceGrant_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).RevokeResourceGrant(ctx, req.(*RevokeResourceGrantRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_ListUserResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUserResourcesRequest)
if err := dec(in); err != nil {
@ -2624,6 +2706,60 @@ func _WalletService_CreateH5RechargeOrder_Handler(srv interface{}, ctx context.C
return interceptor(ctx, in, info, handler)
}
func _WalletService_CreateTemporaryRechargeOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateTemporaryRechargeOrderRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).CreateTemporaryRechargeOrder(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_CreateTemporaryRechargeOrder_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).CreateTemporaryRechargeOrder(ctx, req.(*CreateTemporaryRechargeOrderRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_GetTemporaryRechargeOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetTemporaryRechargeOrderRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).GetTemporaryRechargeOrder(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_GetTemporaryRechargeOrder_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).GetTemporaryRechargeOrder(ctx, req.(*GetTemporaryRechargeOrderRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_ListTemporaryRechargeOrders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListTemporaryRechargeOrdersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).ListTemporaryRechargeOrders(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_ListTemporaryRechargeOrders_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).ListTemporaryRechargeOrders(ctx, req.(*ListTemporaryRechargeOrdersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_SubmitH5RechargeTx_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SubmitH5RechargeTxRequest)
if err := dec(in); err != nil {
@ -3277,6 +3413,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GrantResourceGroup",
Handler: _WalletService_GrantResourceGroup_Handler,
},
{
MethodName: "RevokeResourceGrant",
Handler: _WalletService_RevokeResourceGrant_Handler,
},
{
MethodName: "ListUserResources",
Handler: _WalletService_ListUserResources_Handler,
@ -3377,6 +3517,18 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
MethodName: "CreateH5RechargeOrder",
Handler: _WalletService_CreateH5RechargeOrder_Handler,
},
{
MethodName: "CreateTemporaryRechargeOrder",
Handler: _WalletService_CreateTemporaryRechargeOrder_Handler,
},
{
MethodName: "GetTemporaryRechargeOrder",
Handler: _WalletService_GetTemporaryRechargeOrder_Handler,
},
{
MethodName: "ListTemporaryRechargeOrders",
Handler: _WalletService_ListTemporaryRechargeOrders_Handler,
},
{
MethodName: "SubmitH5RechargeTx",
Handler: _WalletService_SubmitH5RechargeTx_Handler,

View File

@ -0,0 +1,477 @@
# 通用确认消息 Flutter 对接
本文定义 Flutter 对接 `im_confirm` 私聊确认消息所需的 IM 解析、HTTP 接口、状态刷新和按钮置灰规则。业务类型首版是 `host / agency / bd` 身份邀请Flutter 只做通用展示,不写业务分支。
## 通用约定
### Base URL
```text
https://{api_host}
```
本地开发:
```text
http://127.0.0.1:13000
```
### Headers
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Authorization: Bearer {access_token}` | 是 | 登录后的 App token |
| `Content-Type: application/json` | POST 必填 | 请求体 JSON |
| `X-App-Code` | 否 | 多 App 包场景可传,通常由 gateway 解析 |
### 响应 Envelope
成功:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {}
}
```
失败:
```json
{
"code": "CONFLICT",
"message": "conflict",
"request_id": "req_xxx"
}
```
## IM 消息格式
腾讯 IM C2C 自定义消息:
| TIM 字段 | 值 |
| --- | --- |
| `Desc` | `im_confirm` |
| `Ext` | `im_confirm` |
| `Data` | JSON 字符串 |
`Data` 示例:
```json
{
"type": "im_confirm",
"version": 1,
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"msg": "Alice has invited you to join ABC guild",
"status": "pending",
"actions": [
{
"action": "reject",
"label": "Reject",
"style": "secondary"
},
{
"action": "accept",
"label": "Accept",
"style": "primary"
}
],
"created_at_ms": 1710000000000,
"expire_at_ms": 1710604800000
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `type` | string | 固定 `im_confirm` |
| `version` | int | 当前为 `1` |
| `confirm_id` | string | 确认消息 ID后续接口只传这个 |
| `business_type` | string | 业务类型,只用于日志和埋点 |
| `business_subtype` | string | 业务子类型,只用于展示或埋点 |
| `msg` | string | 气泡文案 |
| `status` | string | 初始状态,一般为 `pending` |
| `actions` | array | 按钮配置 |
| `created_at_ms` | int64 | 创建时间 |
| `expire_at_ms` | int64 | 过期时间,可能为 0 |
Flutter 不要把 `business_type``business_subtype` 当成接口参数。确认接口只需要 `confirm_id`
## 状态含义
| 状态 | Flutter 展示 |
| --- | --- |
| `pending` | 展示可点击按钮 |
| `processing` | 按钮置灰,显示处理中或本地 loading |
| `accepted` | 按钮置灰,接受态 |
| `rejected` | 按钮置灰,拒绝态 |
| `cancelled` | 按钮置灰,已取消 |
| `expired` | 按钮置灰,已过期 |
| `failed` | 按钮置灰,可提示稍后重试或联系客服 |
按钮是否可点只看最新状态是否为 `pending`
## 接口 1接受
地址:
```text
POST /api/v1/message/action-confirms/{confirm_id}/accept
```
参数:
```json
{
"command_id": "cfm_123_accept_001"
}
```
返回值 `data`
```json
{
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "accepted",
"processed_action": "accept",
"processed_at_ms": 1710000001000
}
```
相关 IM用户点击 `actions.action=accept` 的按钮时调用。
## 接口 2拒绝
地址:
```text
POST /api/v1/message/action-confirms/{confirm_id}/reject
```
参数:
```json
{
"command_id": "cfm_123_reject_001",
"reason": ""
}
```
返回值 `data`
```json
{
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "rejected",
"processed_action": "reject",
"processed_at_ms": 1710000001000
}
```
相关 IM用户点击 `actions.action=reject` 的按钮时调用。
## 接口 3批量刷新状态
地址:
```text
POST /api/v1/message/action-confirms/status
```
参数:
```json
{
"confirm_ids": ["cfm_123", "cfm_456"]
}
```
返回值 `data`
```json
{
"items": [
{
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "accepted",
"processed_action": "accept",
"processed_at_ms": 1710000001000
}
]
}
```
使用场景:
- 打开会话时,收集当前会话里所有 `im_confirm.confirm_id` 后批量刷新。
- App 从后台回到前台时,刷新当前可见页面的确认消息状态。
- 接口返回的状态覆盖 IM 初始 `status`
## 接口 4按会话刷新状态
地址:
```text
GET /api/v1/message/action-confirms?peer_user_id=10001&page_size=50
```
参数:
| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `peer_user_id` | 是 | 当前会话对方用户 ID |
| `page_size` | 否 | 默认 50最大 100 |
| `page_token` | 否 | 下一页游标 |
返回值 `data`
```json
{
"items": [
{
"confirm_id": "cfm_123",
"sender_user_id": "10001",
"receiver_user_id": "10002",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "pending",
"processed_action": "",
"processed_at_ms": 0,
"created_at_ms": 1710000000000
}
],
"next_page_token": ""
}
```
使用场景:
- 当前 IM SDK 本地消息无法快速遍历 `confirm_id` 时使用。
- 当前用户由 token 决定Flutter 只传对方 `peer_user_id`
## Flutter 数据模型
```dart
class ImConfirmMessage {
final String confirmId;
final String businessType;
final String businessSubtype;
final String msg;
final String status;
final List<ConfirmAction> actions;
final int createdAtMs;
final int expireAtMs;
ImConfirmMessage({
required this.confirmId,
required this.businessType,
required this.businessSubtype,
required this.msg,
required this.status,
required this.actions,
required this.createdAtMs,
required this.expireAtMs,
});
bool get canOperate => status == 'pending';
factory ImConfirmMessage.fromJson(Map<String, dynamic> json) {
return ImConfirmMessage(
confirmId: (json['confirm_id'] ?? '').toString(),
businessType: (json['business_type'] ?? '').toString(),
businessSubtype: (json['business_subtype'] ?? '').toString(),
msg: (json['msg'] ?? '').toString(),
status: (json['status'] ?? 'pending').toString(),
actions: ((json['actions'] as List?) ?? [])
.map((item) => ConfirmAction.fromJson(Map<String, dynamic>.from(item as Map)))
.toList(),
createdAtMs: (json['created_at_ms'] as num?)?.toInt() ?? 0,
expireAtMs: (json['expire_at_ms'] as num?)?.toInt() ?? 0,
);
}
}
class ConfirmAction {
final String action;
final String label;
final String style;
ConfirmAction({
required this.action,
required this.label,
required this.style,
});
factory ConfirmAction.fromJson(Map<String, dynamic> json) {
return ConfirmAction(
action: (json['action'] ?? '').toString(),
label: (json['label'] ?? '').toString(),
style: (json['style'] ?? '').toString(),
);
}
}
class ConfirmStatus {
final String confirmId;
final String status;
final String processedAction;
final int processedAtMs;
ConfirmStatus({
required this.confirmId,
required this.status,
required this.processedAction,
required this.processedAtMs,
});
bool get canOperate => status == 'pending';
factory ConfirmStatus.fromJson(Map<String, dynamic> json) {
return ConfirmStatus(
confirmId: (json['confirm_id'] ?? '').toString(),
status: (json['status'] ?? '').toString(),
processedAction: (json['processed_action'] ?? '').toString(),
processedAtMs: (json['processed_at_ms'] as num?)?.toInt() ?? 0,
);
}
}
```
## IM 解析
```dart
ImConfirmMessage? parseImConfirmCustomData(String data) {
final dynamic decoded = jsonDecode(data);
if (decoded is! Map) return null;
final map = Map<String, dynamic>.from(decoded);
if (map['type'] != 'im_confirm') return null;
final confirmId = (map['confirm_id'] ?? '').toString();
if (confirmId.isEmpty) return null;
return ImConfirmMessage.fromJson(map);
}
```
解析规则:
- 只处理 `type == im_confirm`
- `confirm_id` 为空时按普通不支持消息处理。
- `msg` 直接展示,不在 Flutter 端拼接业务文案。
- `actions` 为空时只展示文案,不展示按钮。
## API 调用
```dart
String newConfirmCommandId(String confirmId, String action) {
return '${confirmId}_${action}_${DateTime.now().millisecondsSinceEpoch}';
}
Future<ConfirmStatus> acceptConfirm(String confirmId) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/message/action-confirms/$confirmId/accept'),
headers: authJsonHeaders(),
body: jsonEncode({
'command_id': newConfirmCommandId(confirmId, 'accept'),
}),
);
return parseConfirmStatusEnvelope(response);
}
Future<ConfirmStatus> rejectConfirm(String confirmId, {String reason = ''}) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/message/action-confirms/$confirmId/reject'),
headers: authJsonHeaders(),
body: jsonEncode({
'command_id': newConfirmCommandId(confirmId, 'reject'),
'reason': reason,
}),
);
return parseConfirmStatusEnvelope(response);
}
```
`command_id` 每次点击生成一个新值。网络重试同一次请求时可以复用同一个 `command_id`
## 状态刷新
```dart
Future<Map<String, ConfirmStatus>> batchRefreshConfirmStatus(List<String> confirmIds) async {
final ids = confirmIds.where((id) => id.isNotEmpty).toSet().toList();
if (ids.isEmpty) return {};
final response = await http.post(
Uri.parse('$baseUrl/api/v1/message/action-confirms/status'),
headers: authJsonHeaders(),
body: jsonEncode({'confirm_ids': ids}),
);
final data = parseEnvelopeData(response);
final items = (data['items'] as List? ?? []);
return {
for (final item in items)
ConfirmStatus.fromJson(Map<String, dynamic>.from(item as Map)).confirmId:
ConfirmStatus.fromJson(Map<String, dynamic>.from(item as Map)),
};
}
```
刷新时机:
- 进入 IM 会话页。
- App resume。
- 收到新的 `im_confirm`
- 接受或拒绝接口返回 `CONFLICT`
## UI 规则
- 气泡文案展示 `msg`
- `status == pending` 时展示 `actions` 里的按钮。
- `status != pending` 时按钮置灰,不能再次点击。
- 本地发起请求后,当前按钮进入 loading同一条消息两个按钮都置灰。
- 接口成功后,用返回状态覆盖本地状态。
- 接口失败但不是鉴权错误时,调用状态接口刷新一次。
按钮样式建议:
| style | 展示 |
| --- | --- |
| `primary` | 主按钮,例如绿色 Accept |
| `secondary` | 次按钮,例如描边 Reject |
Flutter 不要根据 `business_subtype` 写死按钮文案,按钮文案以 `actions.label` 为准。
## 错误处理
| code | Flutter 处理 |
| --- | --- |
| `UNAUTHORIZED` | 重新登录 |
| `PERMISSION_DENIED` | 置灰并提示无权限 |
| `NOT_FOUND` | 置灰,消息已失效 |
| `CONFLICT` | 调状态接口刷新,按最新状态置灰 |
| `UPSTREAM_ERROR` | 解除 loading允许稍后重试 |
| `INTERNAL_ERROR` | 解除 loading提示稍后重试 |
重复点击、跨设备已处理、旧消息重新打开,都以状态接口返回为准。
## 对接检查
- 收到 `im_confirm` 后能渲染文案和两个按钮。
- 点击 Accept 后调用接受接口,成功后按钮置灰。
- 点击 Reject 后调用拒绝接口,成功后按钮置灰。
- 同一条消息重复点击不会重复请求;如果请求已经发出,两个按钮都置灰。
- App 重新进入会话后,通过状态接口刷新旧消息状态。
- 另一个设备处理后,本设备 resume 能刷新为终态。
- `CONFLICT` 时不弹业务成功,只刷新状态后展示最终状态。

View File

@ -0,0 +1,422 @@
# 通用确认消息方案
本文定义 App 私聊中可操作确认消息的后端方案。首个业务场景是 `host / agency / bd` 身份邀请,后续其他业务只新增 `business_type` 处理器,不改变 Flutter 的通用消息渲染和确认接口。
## 目标
- 腾讯云 IM 私聊里支持通用确认消息App 可以展示文案和 `Accept / Reject` 等操作按钮。
- App 点击按钮后只提交 `confirm_id` 和动作,不提交业务参数。
- 后端根据 `confirm_id` 查出业务类型、业务主键、发送人、接收人和当前状态,再调用对应 owner service 执行业务。
- 处理成功后确认消息进入终态App 再次渲染同一条 IM 时按钮置灰。
- 当前用户只能处理发给自己的确认消息,不能替别人确认,也不能枚举别人的消息状态。
## 非目标
- 不用 `confirm` 表替代业务事实表。`role_invitations.status` 仍然是身份邀请的权威状态。
- 不让客户端传 `business_type``business_id``sender_user_id``receiver_user_id` 来决定业务行为。
- 不把确认消息做成房间状态、连接态或腾讯云 IM 会话状态的 owner。
- 本方案不包含 `bd_leader` 邀请,当前只覆盖 `host / agency / bd`
## 服务边界
| 模块 | 职责 |
| --- | --- |
| `gateway-service` | 提供 `/api/v1` HTTP 入口,透传当前登录用户和 `app_code`,不直接写确认表 |
| `message action module` | 确认消息 owner首版落在 `activity-service/internal/service/message`;负责确认表、状态机、幂等和业务分发 |
| `user-service` | `host / agency / bd` 业务 owner负责 `role_invitations``ProcessRoleInvitation` |
| `notice-service` | 只负责把确认消息投递到腾讯云 IM C2C不执行确认业务 |
| 腾讯云 IM | 私聊消息、离线、漫游和客户端长连接 |
首版物理落点建议:
- 确认表放在 `hyapp_activity`,由 `activity-service` 的 message 模块管理。
- `gateway-service` 新增 action confirm HTTP handler内部调用 activity-service gRPC。
- `notice-service` 消费 message action outbox发送腾讯 IM C2C 自定义消息。
## 总流程
```mermaid
sequenceDiagram
participant A as Sender App
participant Gateway as gateway-service
participant User as user-service
participant UserDB as hyapp_user
participant Message as activity-service message module
participant MsgDB as hyapp_activity
participant Notice as notice-service
participant TIM as Tencent IM
participant B as Receiver App
A->>Gateway: 创建 host/agency/bd 邀请
Gateway->>User: InviteHost / InviteAgency / InviteBD
User->>UserDB: tx 写 role_invitations + user_outbox
User-->>Gateway: invitation
Gateway-->>A: invitation
Message->>UserDB: consume RoleInvitationCreated
Message->>MsgDB: 创建 message_action_confirms + message_action_outbox
Notice->>MsgDB: consume ConfirmMessageCreated
Notice->>TIM: C2C TIMCustomElem(type=im_confirm)
TIM-->>B: IM 确认消息
B->>Gateway: POST /api/v1/message/action-confirms/{confirm_id}/accept
Gateway->>Message: AcceptActionConfirm
Message->>MsgDB: pending -> processing
Message->>User: ProcessRoleInvitation(action=accept)
User->>UserDB: tx 推进 role_invitations 终态
Message->>MsgDB: processing -> accepted
Gateway-->>B: confirm status accepted
```
## 数据表
### message_action_confirms
确认消息的当前状态表。
```sql
CREATE TABLE message_action_confirms (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
confirm_id VARCHAR(96) NOT NULL,
business_type VARCHAR(64) NOT NULL,
business_subtype VARCHAR(64) NOT NULL DEFAULT '',
business_id VARCHAR(96) NOT NULL,
sender_user_id BIGINT NOT NULL,
receiver_user_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
message_text VARCHAR(1024) NOT NULL,
actions_json JSON NOT NULL,
payload_json JSON NULL,
source_name VARCHAR(64) NOT NULL,
source_event_id VARCHAR(128) NOT NULL,
processing_action VARCHAR(32) NOT NULL DEFAULT '',
processing_command_id VARCHAR(96) NOT NULL DEFAULT '',
locked_by VARCHAR(128) NOT NULL DEFAULT '',
lock_until_ms BIGINT NOT NULL DEFAULT 0,
processed_action VARCHAR(32) NOT NULL DEFAULT '',
processed_command_id VARCHAR(96) NOT NULL DEFAULT '',
processed_by_user_id BIGINT NULL,
processed_at_ms BIGINT NULL,
expire_at_ms BIGINT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, confirm_id),
UNIQUE KEY uk_confirm_source_event (app_code, source_name, source_event_id),
KEY idx_confirm_receiver_status (app_code, receiver_user_id, status, created_at_ms),
KEY idx_confirm_peer (app_code, receiver_user_id, sender_user_id, created_at_ms),
KEY idx_confirm_business (app_code, business_type, business_id)
);
```
字段规则:
- `confirm_id` 由服务端生成,例如 `cfm_xxx`
- `business_type` 首版支持 `role_invitation`
- `business_subtype` 首版支持 `host``agency``bd`
- `business_id``role_invitation` 来说就是 `role_invitations.invitation_id`
- `sender_user_id` 是邀请发起人,`receiver_user_id` 是目标用户。
- `message_text` 是 IM 展示快照,业务判断不能依赖它。
- `actions_json` 首版为 `["accept","reject"]`
- `payload_json` 只放展示扩展,例如邀请人昵称、公会名、头像等。
- `source_name + source_event_id` 用于从 owner outbox 重试时幂等创建确认消息。
### message_action_outbox
确认消息投递事件表,由 `notice-service` 消费后发腾讯 IM。
```sql
CREATE TABLE message_action_outbox (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
event_id VARCHAR(128) NOT NULL,
event_type VARCHAR(64) NOT NULL,
confirm_id VARCHAR(96) NOT NULL,
receiver_user_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
payload_json JSON NOT NULL,
created_at_ms BIGINT NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
last_error VARCHAR(512) NOT NULL DEFAULT '',
PRIMARY KEY (app_code, event_id),
KEY idx_message_action_outbox_status (app_code, status, created_at_ms)
);
```
首版事件:
- `ConfirmMessageCreated`:发送一条新的 `im_confirm`
- 后续如果需要跨设备实时置灰,可新增 `ConfirmMessageProcessed`,投递状态变更 IM。首版可以通过状态接口刷新。
## 状态机
| 状态 | 说明 |
| --- | --- |
| `pending` | 待处理App 展示可点击按钮 |
| `processing` | 后端正在执行App 按钮置灰并显示处理中 |
| `accepted` | 已接受,终态 |
| `rejected` | 已拒绝,终态 |
| `cancelled` | 业务方取消,终态 |
| `expired` | 超时未处理,终态 |
| `failed` | 后端处理失败且需要人工排查,终态 |
状态推进规则:
- 只有 `receiver_user_id == 当前登录用户` 才能接受或拒绝。
- `pending` 才能进入 `processing`
- `processing` 通过 `locked_by + lock_until_ms` 防止重复点击并发执行。
- owner service 成功后,`processing` 进入 `accepted``rejected`
- 如果服务在 owner service 成功后、confirm 状态更新前崩溃,下一次同 `confirm_id` 请求必须用 `business_type + business_id` 查询 owner 当前事实,修复 confirm 状态。
## 业务类型
### role_invitation
首版处理 `host / agency / bd`
| subtype | business_id | accept 动作 | reject 动作 |
| --- | --- | --- | --- |
| `host` | `role_invitations.invitation_id` | 调 user-service `ProcessRoleInvitation(action=accept)` | 调 user-service `ProcessRoleInvitation(action=reject)` |
| `agency` | `role_invitations.invitation_id` | 调 user-service `ProcessRoleInvitation(action=accept)` | 调 user-service `ProcessRoleInvitation(action=reject)` |
| `bd` | `role_invitations.invitation_id` | 调 user-service `ProcessRoleInvitation(action=accept)` | 调 user-service `ProcessRoleInvitation(action=reject)` |
`role_invitation` 的最终状态以 user-service 为准。confirm 模块只保存按钮状态和业务路由。
## HTTP 接口
所有接口都走 `/api/v1` envelope。
### 接受确认消息
地址:
```text
POST /api/v1/message/action-confirms/{confirm_id}/accept
```
参数:
```json
{
"command_id": "cfm_123_accept_001"
}
```
返回值:
```json
{
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "accepted",
"processed_action": "accept",
"processed_at_ms": 1710000001000
}
```
相关 IM收到 `type=im_confirm``confirm_id=cfm_123` 的消息后,点击 `accept` 调此接口。
### 拒绝确认消息
地址:
```text
POST /api/v1/message/action-confirms/{confirm_id}/reject
```
参数:
```json
{
"command_id": "cfm_123_reject_001",
"reason": ""
}
```
返回值:
```json
{
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "rejected",
"processed_action": "reject",
"processed_at_ms": 1710000001000
}
```
相关 IM收到 `type=im_confirm``confirm_id=cfm_123` 的消息后,点击 `reject` 调此接口。
### 批量查询确认状态
地址:
```text
POST /api/v1/message/action-confirms/status
```
参数:
```json
{
"confirm_ids": ["cfm_123", "cfm_456"]
}
```
返回值:
```json
{
"items": [
{
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "accepted",
"processed_action": "accept",
"processed_at_ms": 1710000001000
}
]
}
```
相关 IMApp 打开会话或回到前台时,收集当前页面内的 `confirm_id` 后调用该接口刷新按钮状态。
### 按会话查询确认状态
地址:
```text
GET /api/v1/message/action-confirms?peer_user_id=10001&page_size=50
```
参数:
| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `peer_user_id` | 是 | 对方用户 ID当前用户从 token 取,不能由客户端传 |
| `page_size` | 否 | 默认 50最大 100 |
| `page_token` | 否 | 分页游标 |
返回值:
```json
{
"items": [
{
"confirm_id": "cfm_123",
"sender_user_id": "10001",
"receiver_user_id": "10002",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "pending",
"processed_action": "",
"processed_at_ms": 0,
"created_at_ms": 1710000000000
}
],
"next_page_token": ""
}
```
相关 IMApp 如果无法从本地消息列表快速收集 `confirm_id`,可以按当前会话刷新最近确认消息状态。
## IM 负载
腾讯 IM 使用 C2C `TIMCustomElem`
```json
{
"type": "im_confirm",
"version": 1,
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"msg": "Alice has invited you to join ABC guild",
"status": "pending",
"actions": [
{
"action": "reject",
"label": "Reject",
"style": "secondary"
},
{
"action": "accept",
"label": "Accept",
"style": "primary"
}
],
"created_at_ms": 1710000000000,
"expire_at_ms": 1710604800000
}
```
腾讯 IM 字段建议:
| TIM 字段 | 值 |
| --- | --- |
| `Desc` | `im_confirm` |
| `Ext` | `im_confirm` |
| `Data` | 上面的 JSON 字符串 |
App 只依赖 `type``confirm_id``msg``status``actions``business_type` 只用于日志和埋点,不用于 App 决定业务接口。
## 创建消息规则
`host / agency / bd` 邀请创建成功后user-service 写 `RoleInvitationCreated` outbox。payload 必须包含:
```json
{
"event_type": "RoleInvitationCreated",
"invitation_id": "987",
"invitation_type": "host",
"status": "pending",
"inviter_user_id": "10001",
"target_user_id": "10002",
"agency_name": "ABC guild",
"created_at_ms": 1710000000000
}
```
message action worker 根据该 payload 创建:
- `business_type = role_invitation`
- `business_subtype = invitation_type`
- `business_id = invitation_id`
- `sender_user_id = inviter_user_id`
- `receiver_user_id = target_user_id`
- `actions_json = ["accept","reject"]`
## 错误码
| HTTP | code | 场景 |
| --- | --- | --- |
| 400 | `INVALID_ARGUMENT` | `confirm_id``command_id` 或 action 参数非法 |
| 401 | `UNAUTHORIZED` | 未登录或 token 无效 |
| 403 | `PERMISSION_DENIED` | 当前用户不是接收人 |
| 404 | `NOT_FOUND` | `confirm_id` 不存在 |
| 409 | `CONFLICT` | 已处理、已过期、处理中或业务状态不允许操作 |
| 502 | `UPSTREAM_ERROR` | owner service 不可用 |
## 开发顺序
1. 在 activity-service message 模块新增 `message_action_confirms``message_action_outbox`
2. 新增 `ActionConfirmService` gRPC`AcceptActionConfirm``RejectActionConfirm``BatchGetActionConfirmStatus``ListConversationActionConfirms`
3. gateway 新增 HTTP 接口并调用 action confirm gRPC。
4. user-service 邀请创建事务写 `RoleInvitationCreated` outbox。
5. activity-service 增加 user outbox consumer创建 confirm row 和 message action outbox。
6. notice-service 增加 message action outbox consumer发送 `im_confirm`
7. 补 `role_invitation` business handler接受/拒绝时调用 user-service `ProcessRoleInvitation`
8. 补状态修复逻辑confirm 仍 pending/processing 但 user-service 邀请已经终态时,以 user-service 状态回写 confirm。
## 验证
- 创建 Host 邀请后,能查到 `role_invitations.pending``message_action_confirms.pending` 和一条待投递 outbox。
- notice-service 投递后App 收到 `type=im_confirm`
- 接收人接受后,`role_invitations.status=accepted`confirm 状态为 `accepted`
- 接收人拒绝后,`role_invitations.status=rejected`confirm 状态为 `rejected`
- 非接收人调用接受/拒绝接口返回 `PERMISSION_DENIED`
- 重复点击同一个动作返回同一个终态,不重复创建业务事实。
- 已接受后再拒绝返回 `CONFLICT`,状态接口返回 `accepted`

63
pkg/messagemq/messages.go Normal file
View File

@ -0,0 +1,63 @@
// Package messagemq defines RocketMQ payloads owned by activity-service message modules.
package messagemq
import (
"encoding/json"
"errors"
"strings"
)
const (
MessageTypeActionOutboxEvent = "message_action_outbox_event"
TagMessageActionOutboxEvent = "message_action_outbox_event"
)
// ActionOutboxMessage is the MQ representation of one committed message_action_outbox fact.
type ActionOutboxMessage struct {
MessageType string `json:"message_type"`
AppCode string `json:"app_code"`
EventID string `json:"event_id"`
EventType string `json:"event_type"`
ConfirmID string `json:"confirm_id"`
PayloadJSON string `json:"payload_json"`
OccurredAtMS int64 `json:"occurred_at_ms"`
}
// EncodeActionOutboxMessage serializes a confirm-message fact for notice-service delivery.
func EncodeActionOutboxMessage(message ActionOutboxMessage) ([]byte, error) {
message.MessageType = MessageTypeActionOutboxEvent
if strings.TrimSpace(message.AppCode) == "" ||
strings.TrimSpace(message.EventID) == "" ||
strings.TrimSpace(message.EventType) == "" ||
strings.TrimSpace(message.ConfirmID) == "" ||
message.OccurredAtMS <= 0 {
return nil, errors.New("message action outbox message is incomplete")
}
if strings.TrimSpace(message.PayloadJSON) == "" {
message.PayloadJSON = "{}"
}
return json.Marshal(message)
}
// DecodeActionOutboxMessage validates one confirm-message MQ body.
func DecodeActionOutboxMessage(body []byte) (ActionOutboxMessage, error) {
var message ActionOutboxMessage
if err := json.Unmarshal(body, &message); err != nil {
return ActionOutboxMessage{}, err
}
if message.MessageType != MessageTypeActionOutboxEvent {
return ActionOutboxMessage{}, errors.New("unexpected message action outbox message_type")
}
if strings.TrimSpace(message.AppCode) == "" ||
strings.TrimSpace(message.EventID) == "" ||
strings.TrimSpace(message.EventType) == "" ||
strings.TrimSpace(message.ConfirmID) == "" ||
message.OccurredAtMS <= 0 {
return ActionOutboxMessage{}, errors.New("message action outbox message is incomplete")
}
if strings.TrimSpace(message.PayloadJSON) == "" {
message.PayloadJSON = "{}"
}
return message, nil
}

View File

@ -62,12 +62,12 @@ var catalog = map[Code]Spec{
RegionNotFound: spec(codes.NotFound, httpStatusNotFound, RegionNotFound, "not found"),
RegionCountryConflict: spec(codes.FailedPrecondition, httpStatusConflict, RegionCountryConflict, "conflict"),
RegionDisabled: spec(codes.FailedPrecondition, httpStatusConflict, RegionDisabled, "conflict"),
InvalidInviteCode: spec(codes.InvalidArgument, httpStatusBadRequest, InvalidInviteCode, "invalid argument"),
InvalidInviteCode: spec(codes.InvalidArgument, httpStatusBadRequest, InvalidInviteCode, "invalid invite code"),
DeviceAlreadyRegistered: spec(
codes.FailedPrecondition,
httpStatusConflict,
DeviceAlreadyRegistered,
"conflict",
"device already registered",
),
InsufficientBalance: spec(codes.FailedPrecondition, httpStatusConflict, InsufficientBalance, "insufficient balance"),

View File

@ -60,7 +60,7 @@ func TestCatalogMappings(t *testing.T) {
grpcCode: codes.InvalidArgument,
httpStatus: httpStatusBadRequest,
publicCode: string(InvalidInviteCode),
publicMessage: "invalid argument",
publicMessage: "invalid invite code",
},
{
name: "unavailable is exposed as upstream error",
@ -86,6 +86,14 @@ func TestCatalogMappings(t *testing.T) {
publicCode: string(CountryChangeCooldown),
publicMessage: "country change is cooling down",
},
{
name: "registered device conflict exposes concrete login reason",
code: DeviceAlreadyRegistered,
grpcCode: codes.FailedPrecondition,
httpStatus: httpStatusConflict,
publicCode: string(DeviceAlreadyRegistered),
publicMessage: "device already registered",
},
}
for _, test := range tests {

View File

@ -25,6 +25,7 @@ TOPICS=(
"hyapp_robot_room_outbox"
"hyapp_room_rocket_launch"
"hyapp_user_outbox"
"hyapp_message_action_outbox"
"hyapp_game_outbox"
)

View File

@ -9,11 +9,13 @@ CREATE TABLE IF NOT EXISTS manager_profiles (
contact_info VARCHAR(128) NOT NULL DEFAULT '' COMMENT '联系信息',
can_grant_avatar_frame TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心赠送头像框',
can_grant_vehicle TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心赠送座驾',
can_grant_badge TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心赠送徽章',
can_update_user_level TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心升级用户等级',
can_add_bd_leader TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 BD Leader',
can_add_admin TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 Admin',
can_add_superadmin TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心添加 Superadmin',
can_block_user TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心封禁用户',
can_transfer_user_country TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否允许在经理中心转移用户国家',
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台管理员 ID',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
@ -48,9 +50,18 @@ 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 = 'manager_profiles' AND COLUMN_NAME = 'can_grant_badge') = 0,
'ALTER TABLE manager_profiles ADD COLUMN can_grant_badge TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''是否允许在经理中心赠送徽章'' AFTER can_grant_vehicle',
'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 = 'manager_profiles' AND COLUMN_NAME = 'can_update_user_level') = 0,
'ALTER TABLE manager_profiles ADD COLUMN can_update_user_level TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''是否允许在经理中心升级用户等级'' AFTER can_grant_vehicle',
'ALTER TABLE manager_profiles ADD COLUMN can_update_user_level TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''是否允许在经理中心升级用户等级'' AFTER can_grant_badge',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
@ -93,9 +104,18 @@ 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 = 'manager_profiles' AND COLUMN_NAME = 'can_transfer_user_country') = 0,
'ALTER TABLE manager_profiles ADD COLUMN can_transfer_user_country TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''是否允许在经理中心转移用户国家'' AFTER can_block_user',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
INSERT INTO manager_profiles (
app_code, user_id, status, contact_info, can_grant_avatar_frame, can_grant_vehicle,
can_update_user_level, can_add_bd_leader, can_add_admin, can_add_superadmin, can_block_user,
app_code, user_id, status, contact_info, can_grant_avatar_frame, can_grant_vehicle, can_grant_badge,
can_update_user_level, can_add_bd_leader, can_add_admin, can_add_superadmin, can_block_user, can_transfer_user_country,
created_by_admin_id, created_at_ms, updated_at_ms
)
SELECT
@ -110,6 +130,8 @@ SELECT
1,
1,
1,
1,
1,
0,
MIN(bl.created_at_ms),
MAX(bl.updated_at_ms)

View File

@ -0,0 +1,861 @@
#!/usr/bin/env python3
"""Refresh robot avatar frames and VIP badges.
The script is intentionally dry-run by default. With --yes it:
1. Builds the robot user set from users.source=game_robot plus robot-service pools.
2. Picks avatar frames priced at or below 2,000,000 COIN, excluding known reserved frames.
3. Picks one VIP strip badge and one VIP tile badge.
4. Ensures wallet entitlements exist, replaces equipped avatar/badge rows, and rebuilds activity badge display slots.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import shlex
import subprocess
import time
import uuid
from pathlib import Path
from typing import Any
import pymysql
import yaml
APP_CODE_DEFAULT = "lalu"
ROBOT_SOURCE_DEFAULT = "game_robot"
GRANT_SOURCE = "game_robot_init"
RESOURCE_OUTBOX_ASSET = "RESOURCE"
OUTBOX_STATUS_PENDING = "pending"
ENTITLEMENT_STATUS_ACTIVE = "active"
GRANT_STATUS_DONE = "succeeded"
RESULT_ENTITLEMENT = "entitlement"
ROBOT_RESOURCE_DURATION_DAYS = 9999
ROBOT_RESOURCE_DURATION_MS = ROBOT_RESOURCE_DURATION_DAYS * 24 * 60 * 60 * 1000
ROBOT_AVATAR_FRAME_MAX_COIN_PRICE = 2_000_000
EXCLUDED_AVATAR_FRAME_IDS = {165, 175}
BADGE_DISPLAY_SLOTS = ("profile_strip", "profile_tile", "honor_wall")
def normalize_app_code(value: str) -> str:
return (value or "").strip().lower() or APP_CODE_DEFAULT
def stable_hash(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def prefixed_id(prefix: str, value: str) -> str:
return prefix + stable_hash(value)
def current_ms() -> int:
return int(time.time() * 1000)
def read_config_text(location: str) -> str:
location = location.strip()
if looks_like_ssh_location(location):
host, remote_path = location.split(":", 1)
return subprocess.check_output(
["ssh", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", host, "cat " + shlex.quote(remote_path)],
text=True,
)
return Path(location).expanduser().read_text(encoding="utf-8")
def looks_like_ssh_location(location: str) -> bool:
if location.startswith("/") or location.startswith("./") or location.startswith("../") or "://" in location:
return False
head, sep, tail = location.partition(":")
return bool(sep and tail and ("@" in head or "." in head or head.replace("-", "").isalnum()))
def load_yaml(location: str) -> dict[str, Any]:
payload = yaml.safe_load(read_config_text(location)) or {}
if not isinstance(payload, dict):
raise RuntimeError(f"config must be a YAML object: {location}")
return payload
def parse_go_mysql_dsn(dsn: str) -> dict[str, Any]:
prefix, rest = dsn.split("@tcp(", 1)
user, password = prefix.split(":", 1) if ":" in prefix else (prefix, "")
addr, rest = rest.split(")/", 1)
host, port = addr.rsplit(":", 1) if ":" in addr else (addr, "3306")
database = rest.split("?", 1)[0]
return {
"host": host,
"port": int(port),
"user": user,
"password": password,
"database": database,
"charset": "utf8mb4",
"autocommit": False,
"connect_timeout": 8,
"read_timeout": 120,
"write_timeout": 120,
"cursorclass": pymysql.cursors.DictCursor,
}
def connect_from_config(path: str, dsn_key: str = "mysql_dsn"):
cfg = load_yaml(path)
dsn = str(cfg.get(dsn_key) or "").strip()
if not dsn:
raise RuntimeError(f"{dsn_key} is required in {path}")
return pymysql.connect(**parse_go_mysql_dsn(dsn))
def table_exists(conn, table_name: str) -> bool:
with conn.cursor() as cursor:
cursor.execute(
"SELECT COUNT(*) AS count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s",
(table_name,),
)
return int(cursor.fetchone()["count"]) > 0
def fetch_user_source_robots(conn, app_code: str, source: str) -> dict[int, dict[str, Any]]:
with conn.cursor() as cursor:
cursor.execute(
"""
SELECT user_id, status, COALESCE(source, '') AS source
FROM users
WHERE app_code = %s AND source = %s
ORDER BY user_id ASC
""",
(app_code, source),
)
return {
int(row["user_id"]): {"user_status": row["status"], "sources": {f"users.source:{row['source']}"}}
for row in cursor.fetchall()
}
def fetch_robot_pool_users(conn, app_code: str) -> dict[int, set[str]]:
result: dict[int, set[str]] = {}
for table, label_column in (("robot_game_robots", "game_id"), ("robot_room_robots", "room_scene")):
if not table_exists(conn, table):
continue
with conn.cursor() as cursor:
cursor.execute(
f"""
SELECT user_id, status, {label_column} AS label
FROM {table}
WHERE app_code = %s
ORDER BY user_id ASC
""",
(app_code,),
)
for row in cursor.fetchall():
user_id = int(row["user_id"])
result.setdefault(user_id, set()).add(f"{table}:{row['label']}:{row['status']}")
return result
def merge_robot_sources(
user_robots: dict[int, dict[str, Any]],
pool_robots: dict[int, set[str]],
) -> dict[int, dict[str, Any]]:
merged = dict(user_robots)
for user_id, sources in pool_robots.items():
item = merged.setdefault(user_id, {"user_status": "", "sources": set()})
item["sources"].update(sources)
return merged
def filter_existing_users(conn, app_code: str, robot_map: dict[int, dict[str, Any]]) -> dict[int, dict[str, Any]]:
user_ids = sorted(robot_map)
if not user_ids:
return {}
existing: dict[int, str] = {}
for chunk in chunks(user_ids, 500):
placeholders = ",".join(["%s"] * len(chunk))
with conn.cursor() as cursor:
cursor.execute(
f"SELECT user_id, status FROM users WHERE app_code = %s AND user_id IN ({placeholders})",
[app_code, *chunk],
)
for row in cursor.fetchall():
existing[int(row["user_id"])] = str(row["status"])
filtered: dict[int, dict[str, Any]] = {}
for user_id in user_ids:
if user_id not in existing:
continue
item = robot_map[user_id]
if not item.get("user_status"):
item["user_status"] = existing[user_id]
filtered[user_id] = item
return filtered
def fetch_resources(conn, app_code: str) -> list[dict[str, Any]]:
with conn.cursor() as cursor:
cursor.execute(
"""
SELECT app_code, resource_id, resource_code, resource_type, name, status,
grantable, manager_grant_enabled, grant_strategy, wallet_asset_type,
wallet_asset_amount, price_type, coin_price, gift_point_amount,
COALESCE(CAST(usage_scope_json AS CHAR), '[]') AS usage_scope_json,
asset_url, preview_url, animation_url,
COALESCE(CAST(metadata_json AS CHAR), '{}') AS metadata_json,
sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
FROM resources
WHERE app_code = %s AND status = 'active'
ORDER BY sort_order ASC, resource_id ASC
""",
(app_code,),
)
return list(cursor.fetchall())
def parse_json_value(raw: str, fallback: Any) -> Any:
try:
return json.loads(raw or "")
except Exception:
return fallback
def badge_form(resource: dict[str, Any]) -> str:
payload = parse_json_value(str(resource.get("metadata_json") or "{}"), {})
if not isinstance(payload, dict):
return ""
return str(payload.get("badge_form") or "").strip().lower()
def is_vip_badge(resource: dict[str, Any]) -> bool:
code = str(resource.get("resource_code") or "").strip().lower()
name = str(resource.get("name") or "").strip().lower()
return str(resource.get("resource_type") or "") == "badge" and (code.startswith("vip") or name.startswith("vip"))
def build_catalog(resources: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
avatar_frames: list[dict[str, Any]] = []
long_badges: list[dict[str, Any]] = []
short_badges: list[dict[str, Any]] = []
for resource in resources:
resource_type = str(resource.get("resource_type") or "").strip()
resource_id = int(resource.get("resource_id") or 0)
if resource_type == "avatar_frame":
if resource_id in EXCLUDED_AVATAR_FRAME_IDS:
continue
if int(resource.get("coin_price") or 0) > ROBOT_AVATAR_FRAME_MAX_COIN_PRICE:
continue
avatar_frames.append(resource)
continue
if not is_vip_badge(resource):
continue
form = badge_form(resource)
if form in ("strip", "long"):
long_badges.append(resource)
elif form in ("tile", "short"):
short_badges.append(resource)
return {
"avatar_frame": avatar_frames,
"long_badge": long_badges,
"short_badge": short_badges,
}
def assert_catalog(catalog: dict[str, list[dict[str, Any]]]) -> None:
missing = [key for key, value in catalog.items() if not value]
if missing:
raise RuntimeError(f"robot appearance resource pool is incomplete: {', '.join(missing)}")
def choose_resource(items: list[dict[str, Any]], app_code: str, user_id: int, slot: str) -> dict[str, Any]:
index = int(stable_hash(f"{app_code}|{user_id}|{slot}")[:16], 16) % len(items)
return items[index]
def resource_snapshot(resource: dict[str, Any]) -> dict[str, Any]:
scopes = parse_json_value(str(resource.get("usage_scope_json") or "[]"), [])
if not isinstance(scopes, list):
scopes = []
return {
"AppCode": str(resource.get("app_code") or ""),
"ResourceID": int(resource.get("resource_id") or 0),
"ResourceCode": str(resource.get("resource_code") or ""),
"ResourceType": str(resource.get("resource_type") or ""),
"Name": str(resource.get("name") or ""),
"Status": str(resource.get("status") or ""),
"Grantable": bool(resource.get("grantable")),
"GrantStrategy": str(resource.get("grant_strategy") or ""),
"WalletAssetType": str(resource.get("wallet_asset_type") or ""),
"WalletAssetAmount": int(resource.get("wallet_asset_amount") or 0),
"PriceType": str(resource.get("price_type") or ""),
"CoinPrice": int(resource.get("coin_price") or 0),
"GiftPointAmount": int(resource.get("gift_point_amount") or 0),
"UsageScopes": [str(item) for item in scopes],
"AssetURL": str(resource.get("asset_url") or ""),
"PreviewURL": str(resource.get("preview_url") or ""),
"AnimationURL": str(resource.get("animation_url") or ""),
"MetadataJSON": str(resource.get("metadata_json") or "{}"),
"SortOrder": int(resource.get("sort_order") or 0),
"CreatedByUserID": int(resource.get("created_by_user_id") or 0),
"UpdatedByUserID": int(resource.get("updated_by_user_id") or 0),
"CreatedAtMS": int(resource.get("created_at_ms") or 0),
"UpdatedAtMS": int(resource.get("updated_at_ms") or 0),
"ManagerGrantEnabled": bool(resource.get("manager_grant_enabled")),
}
def grant_request_hash(app_code: str, user_id: int, resource_id: int, reason: str, operator_user_id: int) -> str:
return stable_hash(
f"resource_grant|{normalize_app_code(app_code)}|{user_id}|{resource_id}|1|{ROBOT_RESOURCE_DURATION_MS}|{reason}|{operator_user_id}|{GRANT_SOURCE}"
)
def resource_outbox_event_id(event_type: str, command_id: str, user_id: int, resource_id: int) -> str:
return "wev_" + stable_hash(f"{event_type}|{command_id}|{user_id}|{resource_id}")
def insert_wallet_outbox(
cursor,
*,
app_code: str,
event_type: str,
command_id: str,
user_id: int,
resource_id: int,
payload: dict[str, Any],
now_ms: int,
) -> None:
cursor.execute(
"""
INSERT IGNORE INTO wallet_outbox (
app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type,
available_delta, frozen_delta, payload, status, retry_count, created_at_ms, updated_at_ms
) VALUES (%s, %s, %s, '', %s, %s, %s, 0, 0, %s, %s, 0, %s, %s)
""",
(
app_code,
resource_outbox_event_id(event_type, command_id, user_id, resource_id),
event_type,
command_id,
user_id,
RESOURCE_OUTBOX_ASSET,
json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
OUTBOX_STATUS_PENDING,
now_ms,
now_ms,
),
)
def active_entitlement(cursor, app_code: str, user_id: int, resource_id: int, now_ms: int) -> str:
cursor.execute(
"""
SELECT entitlement_id
FROM user_resource_entitlements
WHERE app_code = %s AND user_id = %s AND resource_id = %s
AND status = 'active'
AND effective_at_ms <= %s
AND (expires_at_ms = 0 OR expires_at_ms > %s)
AND remaining_quantity > 0
ORDER BY expires_at_ms DESC, created_at_ms DESC
LIMIT 1
FOR UPDATE
""",
(app_code, user_id, resource_id, now_ms, now_ms),
)
row = cursor.fetchone()
return str(row["entitlement_id"]) if row else ""
def ensure_entitlement(
cursor,
*,
app_code: str,
user_id: int,
resource: dict[str, Any],
slot: str,
operator_user_id: int,
reason: str,
run_id: str,
now_ms: int,
) -> tuple[str, bool, str]:
resource_id = int(resource["resource_id"])
existing = active_entitlement(cursor, app_code, user_id, resource_id, now_ms)
if existing:
return existing, False, ""
command_id = f"robot_appearance_refresh:{run_id}:{user_id}:{slot}:{resource_id}"
grant_id = prefixed_id("rgr_", f"{normalize_app_code(app_code)}|{command_id}")
entitlement_id = prefixed_id("ent_", f"{normalize_app_code(app_code)}|{grant_id}|{resource_id}|{now_ms}")
snapshot = resource_snapshot(resource)
snapshot_json = json.dumps(snapshot, ensure_ascii=False, separators=(",", ":"))
expires_at_ms = now_ms + ROBOT_RESOURCE_DURATION_MS
cursor.execute(
"""
INSERT INTO resource_grants (
app_code, grant_id, command_id, target_user_id, grant_source, grant_subject_type,
grant_subject_id, status, request_hash, group_snapshot_json, reason,
operator_user_id, created_at_ms, updated_at_ms
) VALUES (%s, %s, %s, %s, %s, 'resource', %s, %s, %s, NULL, %s, %s, %s, %s)
""",
(
app_code,
grant_id,
command_id,
user_id,
GRANT_SOURCE,
str(resource_id),
GRANT_STATUS_DONE,
grant_request_hash(app_code, user_id, resource_id, reason, operator_user_id),
reason,
operator_user_id,
now_ms,
now_ms,
),
)
cursor.execute(
"""
INSERT INTO user_resource_entitlements (
app_code, entitlement_id, user_id, resource_id, status, quantity, remaining_quantity,
effective_at_ms, expires_at_ms, source_grant_id, created_at_ms, updated_at_ms
) VALUES (%s, %s, %s, %s, %s, 1, 1, %s, %s, %s, %s, %s)
""",
(app_code, entitlement_id, user_id, resource_id, ENTITLEMENT_STATUS_ACTIVE, now_ms, expires_at_ms, grant_id, now_ms, now_ms),
)
cursor.execute(
"""
INSERT INTO resource_grant_items (
app_code, grant_id, resource_id, resource_snapshot_json, quantity, duration_ms,
result_type, wallet_transaction_id, entitlement_id, created_at_ms
) VALUES (%s, %s, %s, %s, 1, %s, %s, '', %s, %s)
""",
(app_code, grant_id, resource_id, snapshot_json, ROBOT_RESOURCE_DURATION_MS, RESULT_ENTITLEMENT, entitlement_id, now_ms),
)
insert_wallet_outbox(
cursor,
app_code=app_code,
event_type="ResourceGranted",
command_id=command_id,
user_id=user_id,
resource_id=resource_id,
payload={"grant_id": grant_id, "resource": snapshot},
now_ms=now_ms,
)
return entitlement_id, True, grant_id
def entitlement_snapshot(
*,
app_code: str,
user_id: int,
entitlement_id: str,
resource: dict[str, Any],
grant_id: str,
now_ms: int,
) -> dict[str, Any]:
return {
"AppCode": app_code,
"EntitlementID": entitlement_id,
"UserID": user_id,
"ResourceID": int(resource["resource_id"]),
"Resource": resource_snapshot(resource),
"Status": ENTITLEMENT_STATUS_ACTIVE,
"Quantity": 1,
"RemainingQuantity": 1,
"EffectiveAtMS": now_ms,
"ExpiresAtMS": now_ms + ROBOT_RESOURCE_DURATION_MS,
"SourceGrantID": grant_id,
"CreatedAtMS": now_ms,
"UpdatedAtMS": now_ms,
"Equipped": True,
}
def replace_wallet_equipment(
conn,
*,
app_code: str,
user_id: int,
selected: dict[str, dict[str, Any]],
operator_user_id: int,
reason: str,
run_id: str,
) -> dict[str, Any]:
now_ms = current_ms()
created_grants = 0
equipped_rows = 0
cursor = conn.cursor()
try:
cursor.execute("START TRANSACTION")
cursor.execute(
"""
DELETE FROM user_resource_equipment
WHERE app_code = %s AND user_id = %s AND resource_type IN ('avatar_frame', 'badge')
""",
(app_code, user_id),
)
deleted_equipment = cursor.rowcount
for slot, resource in selected.items():
resource_type = str(resource["resource_type"])
resource_id = int(resource["resource_id"])
entitlement_id, created, grant_id = ensure_entitlement(
cursor,
app_code=app_code,
user_id=user_id,
resource=resource,
slot=slot,
operator_user_id=operator_user_id,
reason=reason,
run_id=run_id,
now_ms=now_ms,
)
if created:
created_grants += 1
cursor.execute(
"""
INSERT INTO user_resource_equipment (
app_code, user_id, resource_type, entitlement_id, resource_id, updated_at_ms
) VALUES (%s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE resource_id = VALUES(resource_id), updated_at_ms = VALUES(updated_at_ms)
""",
(app_code, user_id, resource_type, entitlement_id, resource_id, now_ms),
)
equipped_rows += 1
event_command_id = f"robot_appearance_equip:{run_id}:{user_id}:{slot}:{resource_id}"
insert_wallet_outbox(
cursor,
app_code=app_code,
event_type="UserResourceChanged",
command_id=event_command_id,
user_id=user_id,
resource_id=resource_id,
payload={
"action": "equip",
"app_code": app_code,
"user_id": user_id,
"resource_id": resource_id,
"resource_type": resource_type,
"entitlement": entitlement_snapshot(
app_code=app_code,
user_id=user_id,
entitlement_id=entitlement_id,
resource=resource,
grant_id=grant_id,
now_ms=now_ms,
),
"updated_at_ms": now_ms,
"event_command": event_command_id,
"source": "robot_appearance_refresh",
},
now_ms=now_ms,
)
conn.commit()
return {"created_grants": created_grants, "equipped_rows": equipped_rows, "deleted_equipment": deleted_equipment}
except Exception:
conn.rollback()
raise
finally:
cursor.close()
def replace_activity_badge_slots(
conn,
*,
app_code: str,
user_id: int,
selected: dict[str, dict[str, Any]],
wallet_conn,
) -> dict[str, int]:
now_ms = current_ms()
long_badge = selected["long_badge"]
short_badge = selected["short_badge"]
long_entitlement = latest_equipped_entitlement(wallet_conn, app_code, user_id, int(long_badge["resource_id"]))
short_entitlement = latest_equipped_entitlement(wallet_conn, app_code, user_id, int(short_badge["resource_id"]))
cursor = conn.cursor()
try:
cursor.execute("START TRANSACTION")
cursor.execute(
f"""
DELETE FROM user_badge_display_slots
WHERE app_code = %s AND user_id = %s AND slot IN ({','.join(['%s'] * len(BADGE_DISPLAY_SLOTS))})
""",
(app_code, user_id, *BADGE_DISPLAY_SLOTS),
)
deleted = cursor.rowcount
inserts = [
("profile_strip", 1, "strip", int(long_badge["resource_id"]), long_entitlement, "robot_appearance_refresh:long"),
("profile_tile", 1, "tile", int(short_badge["resource_id"]), short_entitlement, "robot_appearance_refresh:tile"),
("honor_wall", 1, "tile", int(short_badge["resource_id"]), short_entitlement, "robot_appearance_refresh:honor"),
]
for slot, position, form, resource_id, entitlement_id, source_id in inserts:
cursor.execute(
"""
INSERT INTO user_badge_display_slots (
app_code, user_id, slot, position, badge_form, resource_id,
entitlement_id, source_type, source_id, pin_mode, updated_at_ms
) VALUES (%s, %s, %s, %s, %s, %s, %s, 'system_grant', %s, 'auto', %s)
ON DUPLICATE KEY UPDATE
badge_form = VALUES(badge_form),
resource_id = VALUES(resource_id),
entitlement_id = VALUES(entitlement_id),
source_type = VALUES(source_type),
source_id = VALUES(source_id),
pin_mode = VALUES(pin_mode),
updated_at_ms = VALUES(updated_at_ms)
""",
(app_code, user_id, slot, position, form, resource_id, entitlement_id, source_id, now_ms),
)
conn.commit()
return {"deleted_slots": deleted, "inserted_slots": len(inserts)}
except Exception:
conn.rollback()
raise
finally:
cursor.close()
def latest_equipped_entitlement(conn, app_code: str, user_id: int, resource_id: int) -> str:
with conn.cursor() as cursor:
cursor.execute(
"""
SELECT entitlement_id
FROM user_resource_equipment
WHERE app_code = %s AND user_id = %s AND resource_id = %s
ORDER BY updated_at_ms DESC
LIMIT 1
""",
(app_code, user_id, resource_id),
)
row = cursor.fetchone()
return str(row["entitlement_id"]) if row else ""
def equipment_summary(conn, app_code: str, user_ids: list[int], valid_frame_ids: set[int], valid_badge_ids: set[int]) -> dict[str, int]:
if not user_ids:
return {}
summary = {
"users_with_valid_avatar_frame": 0,
"invalid_avatar_frame_equipment_rows": 0,
"valid_badge_equipment_rows": 0,
"invalid_badge_equipment_rows": 0,
}
for chunk in chunks(user_ids, 500):
user_placeholders = ",".join(["%s"] * len(chunk))
frame_placeholders = ",".join(["%s"] * len(valid_frame_ids)) or "NULL"
badge_placeholders = ",".join(["%s"] * len(valid_badge_ids)) or "NULL"
with conn.cursor() as cursor:
cursor.execute(
f"""
SELECT
COUNT(DISTINCT CASE WHEN eq.resource_type = 'avatar_frame' AND eq.resource_id IN ({frame_placeholders}) THEN eq.user_id END) AS users_with_valid_avatar_frame,
SUM(CASE WHEN eq.resource_type = 'avatar_frame' AND eq.resource_id NOT IN ({frame_placeholders}) THEN 1 ELSE 0 END) AS invalid_avatar_frame_equipment_rows,
SUM(CASE WHEN eq.resource_type = 'badge' AND eq.resource_id IN ({badge_placeholders}) THEN 1 ELSE 0 END) AS valid_badge_equipment_rows,
SUM(CASE WHEN eq.resource_type = 'badge' AND eq.resource_id NOT IN ({badge_placeholders}) THEN 1 ELSE 0 END) AS invalid_badge_equipment_rows
FROM user_resource_equipment eq
WHERE eq.app_code = %s AND eq.user_id IN ({user_placeholders})
AND eq.resource_type IN ('avatar_frame', 'badge')
""",
[*valid_frame_ids, *valid_frame_ids, *valid_badge_ids, *valid_badge_ids, app_code, *chunk],
)
row = cursor.fetchone()
for key in summary:
summary[key] += int(row.get(key) or 0)
return summary
def activity_slot_summary(conn, app_code: str, user_ids: list[int], valid_long_ids: set[int], valid_short_ids: set[int]) -> dict[str, int]:
if not user_ids:
return {}
valid_by_slot = {
"profile_strip": valid_long_ids,
"profile_tile": valid_short_ids,
"honor_wall": valid_short_ids,
}
summary = {f"{slot}_valid_users": 0 for slot in BADGE_DISPLAY_SLOTS}
summary.update({f"{slot}_invalid_rows": 0 for slot in BADGE_DISPLAY_SLOTS})
for slot, valid_ids in valid_by_slot.items():
for chunk in chunks(user_ids, 500):
user_placeholders = ",".join(["%s"] * len(chunk))
valid_placeholders = ",".join(["%s"] * len(valid_ids)) or "NULL"
with conn.cursor() as cursor:
cursor.execute(
f"""
SELECT
COUNT(DISTINCT CASE WHEN resource_id IN ({valid_placeholders}) THEN user_id END) AS valid_users,
SUM(CASE WHEN resource_id NOT IN ({valid_placeholders}) THEN 1 ELSE 0 END) AS invalid_rows
FROM user_badge_display_slots
WHERE app_code = %s AND user_id IN ({user_placeholders}) AND slot = %s
""",
[*valid_ids, *valid_ids, app_code, *chunk, slot],
)
row = cursor.fetchone()
summary[f"{slot}_valid_users"] += int(row.get("valid_users") or 0)
summary[f"{slot}_invalid_rows"] += int(row.get("invalid_rows") or 0)
return summary
def chunks(values: list[int], size: int):
for index in range(0, len(values), size):
yield values[index : index + size]
def sample_assignments(app_code: str, user_ids: list[int], catalog: dict[str, list[dict[str, Any]]], limit: int = 10) -> list[dict[str, Any]]:
samples = []
for user_id in user_ids[:limit]:
selected = select_for_user(app_code, user_id, catalog)
samples.append(
{
"user_id": user_id,
"avatar_frame": compact_resource(selected["avatar_frame"]),
"long_badge": compact_resource(selected["long_badge"]),
"short_badge": compact_resource(selected["short_badge"]),
}
)
return samples
def select_for_user(app_code: str, user_id: int, catalog: dict[str, list[dict[str, Any]]]) -> dict[str, dict[str, Any]]:
return {
"avatar_frame": choose_resource(catalog["avatar_frame"], app_code, user_id, "avatar_frame"),
"long_badge": choose_resource(catalog["long_badge"], app_code, user_id, "long_badge"),
"short_badge": choose_resource(catalog["short_badge"], app_code, user_id, "short_badge"),
}
def compact_resource(resource: dict[str, Any]) -> dict[str, Any]:
return {
"resource_id": int(resource["resource_id"]),
"resource_code": resource["resource_code"],
"name": resource["name"],
"coin_price": int(resource.get("coin_price") or 0),
"badge_form": badge_form(resource),
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Refresh all robot avatar frames and VIP badges.")
parser.add_argument("--user-config", default="services/user-service/configs/config.yaml")
parser.add_argument("--wallet-config", default="services/wallet-service/configs/config.yaml")
parser.add_argument("--robot-config", default="services/robot-service/configs/config.yaml")
parser.add_argument("--activity-config", default="services/activity-service/configs/config.yaml")
parser.add_argument("--app-code", default=APP_CODE_DEFAULT)
parser.add_argument("--robot-source", default=ROBOT_SOURCE_DEFAULT)
parser.add_argument("--operator-user-id", type=int, default=0)
parser.add_argument("--reason", default="refresh robot avatar frames and vip badges")
parser.add_argument("--progress-every", type=int, default=50)
parser.add_argument("--skip-activity", action="store_true")
parser.add_argument("--yes", action="store_true", help="Actually write wallet/activity changes.")
return parser.parse_args()
def main() -> int:
args = parse_args()
app_code = normalize_app_code(args.app_code)
if args.yes and args.operator_user_id <= 0:
raise RuntimeError("--operator-user-id is required with --yes")
if args.yes and not args.reason.strip():
raise RuntimeError("--reason is required with --yes")
user_conn = connect_from_config(args.user_config)
wallet_conn = connect_from_config(args.wallet_config)
robot_conn = connect_from_config(args.robot_config) if args.robot_config.strip() else None
activity_conn = None if args.skip_activity else connect_from_config(args.activity_config)
try:
source_robots = fetch_user_source_robots(user_conn, app_code, args.robot_source.strip())
pool_robots = fetch_robot_pool_users(robot_conn, app_code) if robot_conn else {}
robots = filter_existing_users(user_conn, app_code, merge_robot_sources(source_robots, pool_robots))
user_ids = sorted(robots)
resources = fetch_resources(wallet_conn, app_code)
catalog = build_catalog(resources)
assert_catalog(catalog)
valid_frame_ids = {int(item["resource_id"]) for item in catalog["avatar_frame"]}
valid_long_ids = {int(item["resource_id"]) for item in catalog["long_badge"]}
valid_short_ids = {int(item["resource_id"]) for item in catalog["short_badge"]}
valid_badge_ids = valid_long_ids | valid_short_ids
before_wallet = equipment_summary(wallet_conn, app_code, user_ids, valid_frame_ids, valid_badge_ids)
before_activity = (
activity_slot_summary(activity_conn, app_code, user_ids, valid_long_ids, valid_short_ids) if activity_conn else {}
)
summary = {
"mode": "execute" if args.yes else "dry_run",
"app_code": app_code,
"robot_user_count": len(user_ids),
"source_user_count": len(source_robots),
"robot_pool_user_count": len(pool_robots),
"resource_pool": {key: [compact_resource(item) for item in value] for key, value in catalog.items()},
"before_wallet": before_wallet,
"before_activity": before_activity,
"sample_assignments": sample_assignments(app_code, user_ids, catalog),
}
print(json.dumps(summary, ensure_ascii=False, indent=2))
if not args.yes:
print("dry-run only; rerun with --yes --operator-user-id <id> to write changes")
return 0
run_id = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime()) + "_" + uuid.uuid4().hex[:12]
changed = {
"run_id": run_id,
"processed": 0,
"created_grants": 0,
"equipped_rows": 0,
"deleted_equipment": 0,
"deleted_activity_slots": 0,
"inserted_activity_slots": 0,
}
for user_id in user_ids:
selected = select_for_user(app_code, user_id, catalog)
wallet_result = replace_wallet_equipment(
wallet_conn,
app_code=app_code,
user_id=user_id,
selected=selected,
operator_user_id=args.operator_user_id,
reason=args.reason.strip(),
run_id=run_id,
)
changed["created_grants"] += wallet_result["created_grants"]
changed["equipped_rows"] += wallet_result["equipped_rows"]
changed["deleted_equipment"] += wallet_result["deleted_equipment"]
if activity_conn:
activity_result = replace_activity_badge_slots(
activity_conn,
app_code=app_code,
user_id=user_id,
selected=selected,
wallet_conn=wallet_conn,
)
changed["deleted_activity_slots"] += activity_result["deleted_slots"]
changed["inserted_activity_slots"] += activity_result["inserted_slots"]
changed["processed"] += 1
if args.progress_every > 0 and (changed["processed"] % args.progress_every == 0 or changed["processed"] == len(user_ids)):
print(json.dumps(changed, ensure_ascii=False))
after_wallet = equipment_summary(wallet_conn, app_code, user_ids, valid_frame_ids, valid_badge_ids)
after_activity = (
activity_slot_summary(activity_conn, app_code, user_ids, valid_long_ids, valid_short_ids) if activity_conn else {}
)
print(
json.dumps(
{
"ok": True,
**changed,
"after_wallet": after_wallet,
"after_activity": after_activity,
},
ensure_ascii=False,
indent=2,
)
)
return 0
finally:
if robot_conn:
robot_conn.close()
if activity_conn:
activity_conn.close()
wallet_conn.close()
user_conn.close()
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -35,6 +35,7 @@ import (
coinledgermodule "hyapp-admin-server/internal/modules/coinledger"
countryregionmodule "hyapp-admin-server/internal/modules/countryregion"
cprelationmodule "hyapp-admin-server/internal/modules/cprelation"
cpweeklyrankmodule "hyapp-admin-server/internal/modules/cpweeklyrank"
cumulativerechargerewardmodule "hyapp-admin-server/internal/modules/cumulativerechargereward"
dailytaskmodule "hyapp-admin-server/internal/modules/dailytask"
dashboardmodule "hyapp-admin-server/internal/modules/dashboard"
@ -59,6 +60,7 @@ import (
registrationrewardmodule "hyapp-admin-server/internal/modules/registrationreward"
reportmodule "hyapp-admin-server/internal/modules/report"
resourcemodule "hyapp-admin-server/internal/modules/resource"
riskconfigmodule "hyapp-admin-server/internal/modules/riskconfig"
roomadminmodule "hyapp-admin-server/internal/modules/roomadmin"
roomrocketmodule "hyapp-admin-server/internal/modules/roomrocket"
roomturnoverrewardmodule "hyapp-admin-server/internal/modules/roomturnoverreward"
@ -263,6 +265,7 @@ func main() {
CoinLedger: coinledgermodule.New(userDB, walletDB, sqlDB, walletclient.NewGRPC(walletConn), auditHandler),
CountryRegion: countryregionmodule.New(userclient.NewGRPC(userConn), userDB, cfg, auditHandler),
CPRelation: cprelationmodule.NewWithActivity(userDB, activityclient.NewGRPC(activityConn), auditHandler),
CPWeeklyRank: cpweeklyrankmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
CumulativeRecharge: cumulativerechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
DailyTask: dailytaskmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
Dashboard: dashboardmodule.New(store, cfg, userclient.NewGRPC(userConn)),
@ -293,6 +296,7 @@ func main() {
RegistrationReward: registrationrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
RegionBlock: regionblockmodule.New(userDB, auditHandler),
Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, cfg.WalletService.RequestTimeout, auditHandler),
RiskConfig: riskconfigmodule.New(userclient.NewGRPC(userConn), auditHandler),
RoomAdmin: roomadminmodule.New(userDB, store, roomClient, robotClient, auditHandler),
RoomRocket: roomrocketmodule.New(roomClient, auditHandler),
RoomTurnoverReward: roomturnoverrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),

View File

@ -288,6 +288,7 @@
| `resource-group:update` | `button` | 编辑资源组、替换组成员、启用、禁用 |
| `resource-grant:view` | `menu` | 资源赠送记录 |
| `resource-grant:create` | `button` | 给用户赠送资源或资源组 |
| `resource-grant:revoke` | `button` | 撤销成功的资源组赠送 |
| `gift:view` | `menu` | 礼物列表 |
| `gift:create` | `button` | 从资源库选择资源新增礼物 |
| `gift:update` | `button` | 编辑礼物、资源绑定和价格 |

View File

@ -3,6 +3,7 @@ module hyapp-admin-server
go 1.26.3
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/gin-gonic/gin v1.10.0
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.2.1
@ -10,7 +11,6 @@ require (
github.com/tencentyun/cos-go-sdk-v5 v0.7.66
golang.org/x/crypto v0.32.0
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.35.1
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.25.12
@ -57,4 +57,5 @@ require (
golang.org/x/net v0.29.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
)

View File

@ -1,5 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
@ -59,6 +61,7 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=

View File

@ -15,6 +15,8 @@ type Client interface {
BatchGetUsers(ctx context.Context, req BatchGetUsersRequest) (map[int64]*User, error)
AdminChangeUserCountry(ctx context.Context, req AdminChangeUserCountryRequest) (*ChangeUserCountryResult, error)
SetUserStatus(ctx context.Context, req SetUserStatusRequest) (*SetUserStatusResult, error)
GetRegisterRiskConfig(ctx context.Context, req GetRegisterRiskConfigRequest) (*RegisterRiskConfig, error)
UpdateRegisterRiskConfig(ctx context.Context, req UpdateRegisterRiskConfigRequest) (*RegisterRiskConfig, error)
ResolveDisplayUserID(ctx context.Context, req ResolveDisplayUserIDRequest) (*UserIdentity, error)
ListPrettyDisplayIDPools(ctx context.Context, req ListPrettyDisplayIDPoolsRequest) ([]*PrettyDisplayIDPool, int64, error)
CreatePrettyDisplayIDPool(ctx context.Context, req PrettyDisplayIDPoolRequest) (*PrettyDisplayIDPool, error)
@ -117,6 +119,25 @@ type SetUserStatusResult struct {
RoomEvictError string `json:"roomEvictError,omitempty"`
}
type GetRegisterRiskConfigRequest struct {
RequestID string
Caller string
}
type UpdateRegisterRiskConfigRequest struct {
RequestID string
Caller string
MaxAccountsPerDevice int32
OperatorAdminID int64
}
type RegisterRiskConfig struct {
AppCode string `json:"appCode"`
MaxAccountsPerDevice int32 `json:"maxAccountsPerDevice"`
UpdatedAtMs int64 `json:"updatedAtMs"`
UpdatedByAdminID int64 `json:"updatedByAdminId"`
}
type ResolveDisplayUserIDRequest struct {
RequestID string
Caller string
@ -240,6 +261,28 @@ func (c *GRPCClient) GetUser(ctx context.Context, req GetUserRequest) (*User, er
return fromProtoUser(resp.GetUser()), nil
}
func (c *GRPCClient) GetRegisterRiskConfig(ctx context.Context, req GetRegisterRiskConfigRequest) (*RegisterRiskConfig, error) {
resp, err := c.authClient.GetRegisterRiskConfig(ctx, &userv1.GetRegisterRiskConfigRequest{
Meta: requestMeta(ctx, req.RequestID, req.Caller),
})
if err != nil {
return nil, err
}
return fromProtoRegisterRiskConfig(resp.GetConfig()), nil
}
func (c *GRPCClient) UpdateRegisterRiskConfig(ctx context.Context, req UpdateRegisterRiskConfigRequest) (*RegisterRiskConfig, error) {
resp, err := c.authClient.UpdateRegisterRiskConfig(ctx, &userv1.UpdateRegisterRiskConfigRequest{
Meta: requestMeta(ctx, req.RequestID, req.Caller),
MaxAccountsPerDevice: req.MaxAccountsPerDevice,
OperatorAdminId: req.OperatorAdminID,
})
if err != nil {
return nil, err
}
return fromProtoRegisterRiskConfig(resp.GetConfig()), nil
}
// BatchGetUsers 复用 user-service 主数据 RPC让后台列表只做展示聚合不复制用户资料到游戏库。
func (c *GRPCClient) BatchGetUsers(ctx context.Context, req BatchGetUsersRequest) (map[int64]*User, error) {
resp, err := c.client.BatchGetUsers(ctx, &userv1.BatchGetUsersRequest{
@ -326,6 +369,18 @@ func (c *GRPCClient) ResolveDisplayUserID(ctx context.Context, req ResolveDispla
return fromProtoUserIdentity(resp.GetIdentity()), nil
}
func fromProtoRegisterRiskConfig(config *userv1.RegisterRiskConfig) *RegisterRiskConfig {
if config == nil {
return nil
}
return &RegisterRiskConfig{
AppCode: config.GetAppCode(),
MaxAccountsPerDevice: config.GetMaxAccountsPerDevice(),
UpdatedAtMs: config.GetUpdatedAtMs(),
UpdatedByAdminID: config.GetUpdatedByAdminId(),
}
}
func fromProtoUser(user *userv1.User) *User {
if user == nil {
return nil
@ -488,71 +543,71 @@ type SetAgencyJoinEnabledRequest struct {
}
type BDProfile struct {
UserID int64 `json:"userId,string"`
Role string `json:"role"`
RegionID int64 `json:"regionId"`
ParentLeaderUserID int64 `json:"parentLeaderUserId,string"`
PositionAlias string `json:"positionAlias"`
Status string `json:"status"`
CreatedByUserID int64 `json:"createdByUserId,string"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
DisplayUserID string `json:"displayUserId"`
Username string `json:"username"`
Avatar string `json:"avatar"`
RegionName string `json:"regionName"`
ParentLeaderDisplayID string `json:"parentLeaderDisplayUserId"`
ParentLeaderUsername string `json:"parentLeaderUsername"`
ParentLeaderAvatar string `json:"parentLeaderAvatar"`
CreatedByDisplayUserID string `json:"createdByDisplayUserId"`
CreatedByUsername string `json:"createdByUsername"`
CreatedByAvatar string `json:"createdByAvatar"`
SubBDCount int64 `json:"subBdCount"`
UserID int64 `json:"userId,string"`
Role string `json:"role"`
RegionID int64 `json:"regionId"`
ParentLeaderUserID int64 `json:"parentLeaderUserId,string"`
PositionAlias string `json:"positionAlias"`
Status string `json:"status"`
CreatedByUserID int64 `json:"createdByUserId,string"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
DisplayUserID string `json:"displayUserId"`
Username string `json:"username"`
Avatar string `json:"avatar"`
RegionName string `json:"regionName"`
ParentLeaderDisplayID string `json:"parentLeaderDisplayUserId"`
ParentLeaderUsername string `json:"parentLeaderUsername"`
ParentLeaderAvatar string `json:"parentLeaderAvatar"`
CreatedByDisplayUserID string `json:"createdByDisplayUserId"`
CreatedByUsername string `json:"createdByUsername"`
CreatedByAvatar string `json:"createdByAvatar"`
SubBDCount int64 `json:"subBdCount"`
SalaryWallet SalaryWallet `json:"salaryWallet"`
}
type Agency struct {
AgencyID int64 `json:"agencyId,string"`
OwnerUserID int64 `json:"ownerUserId,string"`
RegionID int64 `json:"regionId"`
ParentBDUserID int64 `json:"parentBdUserId,string"`
Name string `json:"name"`
Status string `json:"status"`
JoinEnabled bool `json:"joinEnabled"`
MaxHosts int32 `json:"maxHosts"`
CreatedByUserID int64 `json:"createdByUserId,string"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
OwnerDisplayUserID string `json:"ownerDisplayUserId"`
OwnerUsername string `json:"ownerUsername"`
OwnerAvatar string `json:"ownerAvatar"`
ParentBDDisplayUserID string `json:"parentBdDisplayUserId"`
ParentBDUsername string `json:"parentBdUsername"`
ParentBDAvatar string `json:"parentBdAvatar"`
RegionName string `json:"regionName"`
AgencyID int64 `json:"agencyId,string"`
OwnerUserID int64 `json:"ownerUserId,string"`
RegionID int64 `json:"regionId"`
ParentBDUserID int64 `json:"parentBdUserId,string"`
Name string `json:"name"`
Status string `json:"status"`
JoinEnabled bool `json:"joinEnabled"`
MaxHosts int32 `json:"maxHosts"`
CreatedByUserID int64 `json:"createdByUserId,string"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
OwnerDisplayUserID string `json:"ownerDisplayUserId"`
OwnerUsername string `json:"ownerUsername"`
OwnerAvatar string `json:"ownerAvatar"`
ParentBDDisplayUserID string `json:"parentBdDisplayUserId"`
ParentBDUsername string `json:"parentBdUsername"`
ParentBDAvatar string `json:"parentBdAvatar"`
RegionName string `json:"regionName"`
SalaryWallet SalaryWallet `json:"salaryWallet"`
}
type HostProfile struct {
UserID int64 `json:"userId,string"`
Status string `json:"status"`
RegionID int64 `json:"regionId"`
CurrentAgencyID int64 `json:"currentAgencyId,string"`
CurrentMembershipID int64 `json:"currentMembershipId,string"`
Source string `json:"source"`
FirstBecameHostAtMs int64 `json:"firstBecameHostAtMs"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
DisplayUserID string `json:"displayUserId"`
Username string `json:"username"`
Avatar string `json:"avatar"`
RegionName string `json:"regionName"`
CurrentAgencyName string `json:"currentAgencyName"`
Diamond int64 `json:"diamond"`
CurrentAgencyOwnerUserID int64 `json:"currentAgencyOwnerUserId,string"`
CurrentAgencyOwnerDisplayUserID string `json:"currentAgencyOwnerDisplayUserId"`
CurrentAgencyOwnerUsername string `json:"currentAgencyOwnerUsername"`
CurrentAgencyOwnerAvatar string `json:"currentAgencyOwnerAvatar"`
UserID int64 `json:"userId,string"`
Status string `json:"status"`
RegionID int64 `json:"regionId"`
CurrentAgencyID int64 `json:"currentAgencyId,string"`
CurrentMembershipID int64 `json:"currentMembershipId,string"`
Source string `json:"source"`
FirstBecameHostAtMs int64 `json:"firstBecameHostAtMs"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
DisplayUserID string `json:"displayUserId"`
Username string `json:"username"`
Avatar string `json:"avatar"`
RegionName string `json:"regionName"`
CurrentAgencyName string `json:"currentAgencyName"`
Diamond int64 `json:"diamond"`
CurrentAgencyOwnerUserID int64 `json:"currentAgencyOwnerUserId,string"`
CurrentAgencyOwnerDisplayUserID string `json:"currentAgencyOwnerDisplayUserId"`
CurrentAgencyOwnerUsername string `json:"currentAgencyOwnerUsername"`
CurrentAgencyOwnerAvatar string `json:"currentAgencyOwnerAvatar"`
SalaryWallet SalaryWallet `json:"salaryWallet"`
}

View File

@ -29,6 +29,7 @@ type Client interface {
UpsertGiftTypeConfig(ctx context.Context, req *walletv1.UpsertGiftTypeConfigRequest) (*walletv1.GiftTypeConfigResponse, error)
GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error)
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error)
RevokeResourceGrant(ctx context.Context, req *walletv1.RevokeResourceGrantRequest) (*walletv1.ResourceGrantResponse, error)
EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error)
ListResourceGrants(ctx context.Context, req *walletv1.ListResourceGrantsRequest) (*walletv1.ListResourceGrantsResponse, error)
ListResourceShopItems(ctx context.Context, req *walletv1.ListResourceShopItemsRequest) (*walletv1.ListResourceShopItemsResponse, error)
@ -39,6 +40,8 @@ type Client interface {
AdminCreditCoinSellerStock(ctx context.Context, req *walletv1.AdminCreditCoinSellerStockRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error)
ListRechargeBills(ctx context.Context, req *walletv1.ListRechargeBillsRequest) (*walletv1.ListRechargeBillsResponse, error)
ListThirdPartyPaymentChannels(ctx context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error)
GetTemporaryRechargeOrder(ctx context.Context, req *walletv1.GetTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error)
ListTemporaryRechargeOrders(ctx context.Context, req *walletv1.ListTemporaryRechargeOrdersRequest) (*walletv1.ListTemporaryRechargeOrdersResponse, error)
SetThirdPartyPaymentMethodStatus(ctx context.Context, req *walletv1.SetThirdPartyPaymentMethodStatusRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error)
UpdateThirdPartyPaymentRate(ctx context.Context, req *walletv1.UpdateThirdPartyPaymentRateRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error)
ListAdminRechargeProducts(ctx context.Context, req *walletv1.ListAdminRechargeProductsRequest) (*walletv1.ListAdminRechargeProductsResponse, error)
@ -139,6 +142,10 @@ func (c *GRPCClient) GrantResourceGroup(ctx context.Context, req *walletv1.Grant
return c.client.GrantResourceGroup(ctx, req)
}
func (c *GRPCClient) RevokeResourceGrant(ctx context.Context, req *walletv1.RevokeResourceGrantRequest) (*walletv1.ResourceGrantResponse, error) {
return c.client.RevokeResourceGrant(ctx, req)
}
func (c *GRPCClient) EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error) {
return c.client.EquipUserResource(ctx, req)
}
@ -179,6 +186,14 @@ func (c *GRPCClient) ListThirdPartyPaymentChannels(ctx context.Context, req *wal
return c.client.ListThirdPartyPaymentChannels(ctx, req)
}
func (c *GRPCClient) GetTemporaryRechargeOrder(ctx context.Context, req *walletv1.GetTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) {
return c.client.GetTemporaryRechargeOrder(ctx, req)
}
func (c *GRPCClient) ListTemporaryRechargeOrders(ctx context.Context, req *walletv1.ListTemporaryRechargeOrdersRequest) (*walletv1.ListTemporaryRechargeOrdersResponse, error) {
return c.client.ListTemporaryRechargeOrders(ctx, req)
}
func (c *GRPCClient) SetThirdPartyPaymentMethodStatus(ctx context.Context, req *walletv1.SetThirdPartyPaymentMethodStatusRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error) {
return c.client.SetThirdPartyPaymentMethodStatus(ctx, req)
}

View File

@ -33,24 +33,67 @@ type Service struct {
}
type AppUser struct {
Avatar string `json:"avatar"`
Coin int64 `json:"coin"`
Country string `json:"country"`
CountryDisplayName string `json:"countryDisplayName"`
CountryName string `json:"countryName"`
CreatedAtMs int64 `json:"createdAtMs"`
DefaultDisplayUserID string `json:"defaultDisplayUserId"`
DisplayUserID string `json:"displayUserId"`
Gender string `json:"gender"`
LastActiveAtMs int64 `json:"lastActiveAtMs"`
PrettyDisplayUserID string `json:"prettyDisplayUserId"`
PrettyID string `json:"prettyId"`
RegionID int64 `json:"regionId"`
RegionName string `json:"regionName"`
Status string `json:"status"`
UpdatedAtMs int64 `json:"updatedAtMs"`
UserID string `json:"userId"`
Username string `json:"username"`
Avatar string `json:"avatar"`
Balances []AppUserAssetBalance `json:"balances,omitempty"`
Coin int64 `json:"coin"`
Country string `json:"country"`
CountryDisplayName string `json:"countryDisplayName"`
CountryName string `json:"countryName"`
CreatedAtMs int64 `json:"createdAtMs"`
DefaultDisplayUserID string `json:"defaultDisplayUserId"`
Diamond int64 `json:"diamond"`
DisplayUserID string `json:"displayUserId"`
EquippedResources []AppUserResource `json:"equippedResources,omitempty"`
Gender string `json:"gender"`
LastActiveAtMs int64 `json:"lastActiveAtMs"`
PrettyDisplayUserID string `json:"prettyDisplayUserId"`
PrettyID string `json:"prettyId"`
RegionID int64 `json:"regionId"`
RegionName string `json:"regionName"`
Resources []AppUserResource `json:"resources,omitempty"`
Status string `json:"status"`
UpdatedAtMs int64 `json:"updatedAtMs"`
UserID string `json:"userId"`
Username string `json:"username"`
VIP AppUserVIP `json:"vip"`
}
type AppUserAssetBalance struct {
AssetType string `json:"assetType"`
AvailableAmount int64 `json:"availableAmount"`
FrozenAmount int64 `json:"frozenAmount"`
TotalAmount int64 `json:"totalAmount"`
Version int64 `json:"version"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
type AppUserVIP struct {
Level int32 `json:"level"`
Name string `json:"name"`
Active bool `json:"active"`
StartedAtMs int64 `json:"startedAtMs"`
ExpiresAtMs int64 `json:"expiresAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
type AppUserResource struct {
EntitlementID string `json:"entitlementId"`
ResourceID int64 `json:"resourceId"`
ResourceCode string `json:"resourceCode"`
ResourceType string `json:"resourceType"`
Name string `json:"name"`
Status string `json:"status"`
Quantity int64 `json:"quantity"`
RemainingQuantity int64 `json:"remainingQuantity"`
EffectiveAtMs int64 `json:"effectiveAtMs"`
ExpiresAtMs int64 `json:"expiresAtMs"`
SourceGrantID string `json:"sourceGrantId"`
AssetURL string `json:"assetUrl"`
PreviewURL string `json:"previewUrl"`
AnimationURL string `json:"animationUrl"`
Equipped bool `json:"equipped"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
type SetUserStatusResult struct {
@ -146,6 +189,9 @@ func (s *Service) ListUsers(ctx context.Context, query listQuery) ([]AppUser, in
if err := s.fillBalances(ctx, items, userIDs); err != nil {
return nil, 0, err
}
if err := s.fillVIPLevels(ctx, items, userIDs); err != nil {
return nil, 0, err
}
return items, total, nil
}
@ -206,7 +252,11 @@ func (s *Service) listUsersSortedByCoin(ctx context.Context, query listQuery, wh
return nil, err
}
sortAppUsersByCoin(items, query.SortDirection)
return paginateAppUsers(items, query.Page, query.PageSize), nil
pagedItems := paginateAppUsers(items, query.Page, query.PageSize)
if err := s.fillVIPLevels(ctx, pagedItems, appUserNumericIDs(pagedItems)); err != nil {
return nil, err
}
return pagedItems, nil
}
func scanAppUserRows(rows *sql.Rows, capacity int) ([]AppUser, []int64, error) {
@ -327,6 +377,12 @@ func (s *Service) GetUser(ctx context.Context, userID int64) (AppUser, error) {
if err := s.fillBalances(ctx, items, []int64{userID}); err != nil {
return AppUser{}, err
}
if err := s.fillVIPLevels(ctx, items, []int64{userID}); err != nil {
return AppUser{}, err
}
if err := s.fillUserResources(ctx, items, userID); err != nil {
return AppUser{}, err
}
return items[0], nil
}
@ -517,26 +573,31 @@ func (s *Service) fillBalances(ctx context.Context, items []AppUser, userIDs []i
}
// 用户列表金币排序会对完整筛选集补余额,按块查询可以避免 IN 参数过长导致后台列表失败。
chunk := userIDs[start:end]
placeholders := strings.TrimRight(strings.Repeat("?,", len(chunk)), ",")
args := make([]any, 0, len(chunk)+2)
args := make([]any, 0, len(chunk)+1)
args = append(args, appCode)
for _, id := range chunk {
args = append(args, id)
}
args = append(args, "COIN")
rows, err := s.walletDB.QueryContext(ctx, fmt.Sprintf(`
SELECT user_id, asset_type, available_amount
SELECT user_id, asset_type, available_amount, frozen_amount, version, updated_at_ms
FROM wallet_accounts
WHERE app_code = ? AND user_id IN (%s) AND asset_type = ?
`, placeholders), args...)
WHERE app_code = ? AND user_id IN (%s)
ORDER BY user_id ASC, asset_type ASC
`, placeholders(len(chunk))), args...)
if err != nil {
return err
}
for rows.Next() {
var userID int64
var assetType string
var amount int64
if err := rows.Scan(&userID, &assetType, &amount); err != nil {
var balance AppUserAssetBalance
if err := rows.Scan(
&userID,
&balance.AssetType,
&balance.AvailableAmount,
&balance.FrozenAmount,
&balance.Version,
&balance.UpdatedAtMs,
); err != nil {
_ = rows.Close()
return err
}
@ -544,9 +605,15 @@ func (s *Service) fillBalances(ctx context.Context, items []AppUser, userIDs []i
if !ok {
continue
}
switch strings.ToUpper(assetType) {
balance.AssetType = strings.ToUpper(strings.TrimSpace(balance.AssetType))
balance.TotalAmount = balance.AvailableAmount + balance.FrozenAmount
// 钱包资产明细作为详情页“全部资产”的余额来源;金币和钻石仍同步到顶层字段,兼容列表排序和旧页面读取。
items[i].Balances = append(items[i].Balances, balance)
switch balance.AssetType {
case "COIN":
items[i].Coin = amount
items[i].Coin = balance.AvailableAmount
case "DIAMOND":
items[i].Diamond = balance.AvailableAmount
}
}
if err := rows.Err(); err != nil {
@ -560,6 +627,138 @@ func (s *Service) fillBalances(ctx context.Context, items []AppUser, userIDs []i
return nil
}
func (s *Service) fillVIPLevels(ctx context.Context, items []AppUser, userIDs []int64) error {
if s.walletDB == nil || len(userIDs) == 0 {
return nil
}
index := make(map[int64]int, len(userIDs))
for i, id := range userIDs {
index[id] = i
}
appCode := appctx.FromContext(ctx)
nowMs := nowMillis()
const chunkSize = 500
for start := 0; start < len(userIDs); start += chunkSize {
end := start + chunkSize
if end > len(userIDs) {
end = len(userIDs)
}
chunk := userIDs[start:end]
args := make([]any, 0, len(chunk)+1)
args = append(args, appCode)
for _, id := range chunk {
args = append(args, id)
}
rows, err := s.walletDB.QueryContext(ctx, fmt.Sprintf(`
SELECT m.user_id, m.level, COALESCE(v.name, m.name), m.started_at_ms, m.expires_at_ms, m.updated_at_ms
FROM user_vip_memberships m
LEFT JOIN vip_levels v ON v.app_code = m.app_code AND v.level = m.level
WHERE m.app_code = ? AND m.user_id IN (%s)
`, placeholders(len(chunk))), args...)
if err != nil {
return err
}
for rows.Next() {
var userID int64
var vip AppUserVIP
if err := rows.Scan(&userID, &vip.Level, &vip.Name, &vip.StartedAtMs, &vip.ExpiresAtMs, &vip.UpdatedAtMs); err != nil {
_ = rows.Close()
return err
}
i, ok := index[userID]
if !ok {
continue
}
// 这里刻意沿用 wallet-service GetMyVip 的展示口径:只看 level 和 expires_at_ms 计算当前 VIP
// 过期会员仍保留历史行,但后台用户列表不展示为有效等级。
vip.Active = vip.Level > 0 && vip.ExpiresAtMs > nowMs
if !vip.Active {
vip.Level = 0
vip.Name = ""
}
items[i].VIP = vip
}
if err := rows.Err(); err != nil {
_ = rows.Close()
return err
}
if err := rows.Close(); err != nil {
return err
}
}
return nil
}
func (s *Service) fillUserResources(ctx context.Context, items []AppUser, userID int64) error {
if s.walletDB == nil || len(items) == 0 || userID <= 0 {
return nil
}
appCode := appctx.FromContext(ctx)
nowMs := nowMillis()
rows, err := s.walletDB.QueryContext(ctx, `
SELECT e.entitlement_id, e.resource_id, r.resource_code, r.resource_type, r.name, e.status,
e.quantity, e.remaining_quantity, e.effective_at_ms, e.expires_at_ms, e.source_grant_id,
COALESCE(r.asset_url, ''), COALESCE(r.preview_url, ''), COALESCE(r.animation_url, ''),
CASE WHEN eq.entitlement_id IS NULL THEN FALSE ELSE TRUE END AS equipped,
e.created_at_ms, e.updated_at_ms
FROM user_resource_entitlements e
JOIN resources r ON r.app_code = e.app_code AND r.resource_id = e.resource_id
LEFT JOIN user_resource_equipment eq
ON eq.app_code = e.app_code
AND eq.user_id = e.user_id
AND eq.resource_id = e.resource_id
AND eq.entitlement_id = e.entitlement_id
WHERE e.app_code = ? AND e.user_id = ?
AND e.status = 'active'
AND e.effective_at_ms <= ? AND (e.expires_at_ms = 0 OR e.expires_at_ms > ?)
AND e.remaining_quantity > 0
AND r.status = 'active'
ORDER BY equipped DESC, r.resource_type ASC, e.updated_at_ms DESC, e.created_at_ms DESC
`, appCode, userID, nowMs, nowMs)
if err != nil {
return err
}
defer rows.Close()
resources := make([]AppUserResource, 0)
equipped := make([]AppUserResource, 0)
for rows.Next() {
var resource AppUserResource
if err := rows.Scan(
&resource.EntitlementID,
&resource.ResourceID,
&resource.ResourceCode,
&resource.ResourceType,
&resource.Name,
&resource.Status,
&resource.Quantity,
&resource.RemainingQuantity,
&resource.EffectiveAtMs,
&resource.ExpiresAtMs,
&resource.SourceGrantID,
&resource.AssetURL,
&resource.PreviewURL,
&resource.AnimationURL,
&resource.Equipped,
&resource.CreatedAtMs,
&resource.UpdatedAtMs,
); err != nil {
return err
}
resources = append(resources, resource)
if resource.Equipped {
// 装备状态只从 user_resource_equipment 读取;前端可以直接展示当前佩戴,不需要再按类型猜测。
equipped = append(equipped, resource)
}
}
if err := rows.Err(); err != nil {
return err
}
items[0].Resources = resources
items[0].EquippedResources = equipped
return nil
}
func normalizeListQuery(query listQuery) listQuery {
if query.Page < 1 {
query.Page = 1
@ -737,6 +936,18 @@ func paginateAppUsers(items []AppUser, page int, pageSize int) []AppUser {
return items[start:end]
}
func appUserNumericIDs(items []AppUser) []int64 {
ids := make([]int64, 0, len(items))
for _, item := range items {
id, err := strconv.ParseInt(item.UserID, 10, 64)
if err != nil || id <= 0 {
continue
}
ids = append(ids, id)
}
return ids
}
func countRows(ctx context.Context, db *sql.DB, whereSQL string, args ...any) (int64, error) {
var total int64
err := db.QueryRowContext(ctx, "SELECT COUNT(*) "+whereSQL, args...).Scan(&total)

View File

@ -24,6 +24,7 @@ const (
coinSellerTransferBizType = "coin_seller_transfer"
coinSellerRechargeBizType = "coin_seller_recharge"
coinSellerStockPurchaseBizType = "coin_seller_stock_purchase"
coinSellerStockDeductionBizType = "coin_seller_stock_deduction"
coinSellerCoinCompensationBizType = "coin_seller_coin_compensation"
salaryTransferToCoinSellerBizType = "salary_transfer_to_coin_seller"
coinSellerLedgerTypeAdminStockCredit = "admin_stock_credit"
@ -782,6 +783,7 @@ func coinSellerLedgerBizTypes(ledgerType string) ([]string, error) {
// 空类型表示“全部币商流水”,但仍只允许当前产品定义的公开币商库存口径,不能把其他内部账带出来。
return []string{
coinSellerStockPurchaseBizType,
coinSellerStockDeductionBizType,
coinSellerRechargeBizType,
coinSellerCoinCompensationBizType,
coinSellerTransferBizType,
@ -790,7 +792,7 @@ func coinSellerLedgerBizTypes(ledgerType string) ([]string, error) {
}, nil
case coinSellerLedgerTypeAdminStockCredit:
// 进货操作是运营库存口径,底层包含后台 USDT 进货、H5 三方币商充值、金币补偿和后台扣除;资产条件仍限定在 COIN_SELLER_COIN。
return []string{coinSellerStockPurchaseBizType, coinSellerRechargeBizType, coinSellerCoinCompensationBizType, coinManualCreditBizType}, nil
return []string{coinSellerStockPurchaseBizType, coinSellerStockDeductionBizType, coinSellerRechargeBizType, coinSellerCoinCompensationBizType, coinManualCreditBizType}, nil
case coinSellerLedgerTypeSellerTransfer:
// 币商转用户只展示币商侧出账分录,收款用户资料在展示投影里补齐。
return []string{coinSellerTransferBizType}, nil
@ -804,7 +806,7 @@ func coinSellerLedgerBizTypes(ledgerType string) ([]string, error) {
func coinSellerLedgerTypeForBizType(bizType string) string {
switch bizType {
case coinSellerStockPurchaseBizType, coinSellerRechargeBizType, coinSellerCoinCompensationBizType:
case coinSellerStockPurchaseBizType, coinSellerStockDeductionBizType, coinSellerRechargeBizType, coinSellerCoinCompensationBizType:
return coinSellerLedgerTypeAdminStockCredit
case coinManualCreditBizType:
return coinSellerLedgerTypeAdminStockCredit
@ -821,6 +823,8 @@ func coinSellerLedgerLabel(item coinSellerLedgerDTO) string {
switch item.BizType {
case coinSellerStockPurchaseBizType:
return "USDT进货"
case coinSellerStockDeductionBizType:
return "USDT扣除"
case coinSellerRechargeBizType:
return "三方充值"
case coinSellerCoinCompensationBizType:

View File

@ -51,10 +51,10 @@ func TestCoinSellerLedgerWhereMapsAdminStockCredit(t *testing.T) {
if err != nil {
t.Fatalf("coin seller ledger where failed: %v", err)
}
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?) AND e.user_id IN (?,?)"; where != want {
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?,?) AND e.user_id IN (?,?)"; where != want {
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
}
if len(args) != 8 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerRechargeBizType || args[4] != coinSellerCoinCompensationBizType || args[5] != coinManualCreditBizType || args[6] != int64(3001) || args[7] != int64(3002) {
if len(args) != 9 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerStockDeductionBizType || args[4] != coinSellerRechargeBizType || args[5] != coinSellerCoinCompensationBizType || args[6] != coinManualCreditBizType || args[7] != int64(3001) || args[8] != int64(3002) {
t.Fatalf("args mismatch: %#v", args)
}
}
@ -64,10 +64,10 @@ func TestCoinSellerLedgerWhereEmptyTypeUsesAllPublicTypes(t *testing.T) {
if err != nil {
t.Fatalf("coin seller ledger where failed: %v", err)
}
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?,?,?)"; where != want {
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?,?,?,?)"; where != want {
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
}
if len(args) != 8 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerRechargeBizType || args[4] != coinSellerCoinCompensationBizType || args[5] != coinSellerTransferBizType || args[6] != salaryTransferToCoinSellerBizType || args[7] != coinManualCreditBizType {
if len(args) != 9 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerStockDeductionBizType || args[4] != coinSellerRechargeBizType || args[5] != coinSellerCoinCompensationBizType || args[6] != coinSellerTransferBizType || args[7] != salaryTransferToCoinSellerBizType || args[8] != coinManualCreditBizType {
t.Fatalf("args mismatch: %#v", args)
}
}
@ -117,6 +117,7 @@ func TestCoinSellerLedgerLabelUsesConcreteStockAndDebitType(t *testing.T) {
want string
}{
{name: "usdt purchase", item: coinSellerLedgerDTO{BizType: coinSellerStockPurchaseBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "USDT进货"},
{name: "usdt deduction", item: coinSellerLedgerDTO{BizType: coinSellerStockDeductionBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "USDT扣除"},
{name: "third party recharge", item: coinSellerLedgerDTO{BizType: coinSellerRechargeBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "三方充值"},
{name: "compensation", item: coinSellerLedgerDTO{BizType: coinSellerCoinCompensationBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "金币补偿"},
{name: "debit", item: coinSellerLedgerDTO{BizType: coinManualCreditBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit, AvailableDelta: -100}, want: "金币扣除"},

View File

@ -68,6 +68,11 @@ func (h *Handler) UpdateConfig(c *gin.Context) {
return
}
if req.WeeklyRank != nil {
// CP 周榜已经拆成独立活动入口;旧接口保留兼容字段,但写入必须同时具备周榜活动权限,避免只拿到关系配置权限的后台用户绕过新菜单。
if !middleware.HasPermission(c, "cp-weekly-rank:update") {
response.Forbidden(c, "没有操作权限")
return
}
req.WeeklyRank.UpdatedByAdminID = int64(middleware.CurrentUserID(c))
}
config, err := h.service.UpdateConfig(c.Request.Context(), appctx.FromContext(c.Request.Context()), req)

View File

@ -0,0 +1,192 @@
package cpweeklyrank
import (
"strconv"
"strings"
"time"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/activityclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
activityv1 "hyapp.local/api/proto/activity/v1"
"github.com/gin-gonic/gin"
)
type Handler struct {
activity activityclient.Client
audit shared.OperationLogger
}
func New(activity activityclient.Client, audit shared.OperationLogger) *Handler {
return &Handler{activity: activity, audit: audit}
}
type configRequest struct {
Enabled bool `json:"enabled"`
TopCount int32 `json:"top_count"`
Rewards []rewardDTO `json:"rewards"`
}
type configDTO struct {
AppCode string `json:"app_code"`
ActivityCode string `json:"activity_code"`
Enabled bool `json:"enabled"`
RelationType string `json:"relation_type"`
TopCount int32 `json:"top_count"`
Rewards []rewardDTO `json:"rewards"`
UpdatedByAdminID int64 `json:"updated_by_admin_id,string"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type rewardDTO struct {
RankNo int32 `json:"rank_no"`
ResourceGroupID int64 `json:"resource_group_id"`
}
type settlementDTO struct {
SettlementID string `json:"settlement_id"`
PeriodStartMS int64 `json:"period_start_ms"`
PeriodEndMS int64 `json:"period_end_ms"`
RankNo int32 `json:"rank_no"`
RelationshipID string `json:"relationship_id"`
UserID int64 `json:"user_id,string"`
Score int64 `json:"score"`
ResourceGroupID int64 `json:"resource_group_id"`
WalletCommandID string `json:"wallet_command_id"`
WalletGrantID string `json:"wallet_grant_id"`
Status string `json:"status"`
FailureReason string `json:"failure_reason"`
AttemptCount int32 `json:"attempt_count"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
func (h *Handler) GetConfig(c *gin.Context) {
resp, err := h.activity.GetCPWeeklyRankConfig(c.Request.Context(), &activityv1.GetCPWeeklyRankConfigRequest{Meta: h.meta(c)})
if err != nil {
response.ServerError(c, "获取CP排行活动配置失败")
return
}
response.OK(c, configFromProto(resp.GetConfig()))
}
func (h *Handler) UpdateConfig(c *gin.Context) {
var req configRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "CP排行活动配置参数不正确")
return
}
resp, err := h.activity.UpdateCPWeeklyRankConfig(c.Request.Context(), &activityv1.UpdateCPWeeklyRankConfigRequest{
Meta: h.meta(c),
Config: req.toProto(),
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := configFromProto(resp.GetConfig())
shared.OperationLogWithResourceID(c, h.audit, "update-cp-weekly-rank-config", "cp_weekly_rank_configs", item.AppCode, "success", "")
response.OK(c, item)
}
func (h *Handler) ListSettlements(c *gin.Context) {
periodStartMS := parseInt64Query(c, "period_start_ms")
periodEndMS := parseInt64Query(c, "period_end_ms")
resp, err := h.activity.ListCPWeeklyRankSettlements(c.Request.Context(), &activityv1.ListCPWeeklyRankSettlementsRequest{
Meta: h.meta(c),
PeriodStartMs: periodStartMS,
PeriodEndMs: periodEndMS,
})
if err != nil {
response.ServerError(c, "获取CP排行活动发奖记录失败")
return
}
items := make([]settlementDTO, 0, len(resp.GetSettlements()))
for _, settlement := range resp.GetSettlements() {
items = append(items, settlementFromProto(settlement))
}
response.OK(c, gin.H{"items": items, "total": len(items), "period_start_ms": periodStartMS, "period_end_ms": periodEndMS})
}
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
return &activityv1.RequestMeta{
RequestId: middleware.CurrentRequestID(c),
Caller: "admin-server",
AppCode: appctx.FromContext(c.Request.Context()),
SentAtMs: time.Now().UTC().UnixMilli(),
}
}
func (r configRequest) toProto() *activityv1.CPWeeklyRankConfig {
topCount := r.TopCount
if topCount <= 0 {
topCount = 3
}
rewards := make([]*activityv1.CPWeeklyRankReward, 0, len(r.Rewards))
for _, reward := range r.Rewards {
// activity-service 会按 top_count 归一化和校验admin-server 只做传输层适配,
// 保留原 rank/resource_group 组合,避免两个服务出现不同的配置裁决口径。
rewards = append(rewards, &activityv1.CPWeeklyRankReward{RankNo: reward.RankNo, ResourceGroupId: reward.ResourceGroupID})
}
return &activityv1.CPWeeklyRankConfig{
ActivityCode: "cp_weekly_rank",
Enabled: r.Enabled,
RelationType: "cp",
TopCount: topCount,
Rewards: rewards,
}
}
func configFromProto(config *activityv1.CPWeeklyRankConfig) configDTO {
if config == nil {
return configDTO{ActivityCode: "cp_weekly_rank", Enabled: true, RelationType: "cp", TopCount: 3, Rewards: []rewardDTO{}}
}
rewards := make([]rewardDTO, 0, len(config.GetRewards()))
for _, reward := range config.GetRewards() {
rewards = append(rewards, rewardDTO{RankNo: reward.GetRankNo(), ResourceGroupID: reward.GetResourceGroupId()})
}
return configDTO{
AppCode: config.GetAppCode(),
ActivityCode: config.GetActivityCode(),
Enabled: config.GetEnabled(),
RelationType: config.GetRelationType(),
TopCount: config.GetTopCount(),
Rewards: rewards,
UpdatedByAdminID: config.GetUpdatedByAdminId(),
CreatedAtMS: config.GetCreatedAtMs(),
UpdatedAtMS: config.GetUpdatedAtMs(),
}
}
func settlementFromProto(settlement *activityv1.CPWeeklyRankSettlement) settlementDTO {
if settlement == nil {
return settlementDTO{}
}
return settlementDTO{
SettlementID: settlement.GetSettlementId(),
PeriodStartMS: settlement.GetPeriodStartMs(),
PeriodEndMS: settlement.GetPeriodEndMs(),
RankNo: settlement.GetRankNo(),
RelationshipID: settlement.GetRelationshipId(),
UserID: settlement.GetUserId(),
Score: settlement.GetScore(),
ResourceGroupID: settlement.GetResourceGroupId(),
WalletCommandID: settlement.GetWalletCommandId(),
WalletGrantID: settlement.GetWalletGrantId(),
Status: settlement.GetStatus(),
FailureReason: settlement.GetFailureReason(),
AttemptCount: settlement.GetAttemptCount(),
CreatedAtMS: settlement.GetCreatedAtMs(),
UpdatedAtMS: settlement.GetUpdatedAtMs(),
}
}
func parseInt64Query(c *gin.Context, key string) int64 {
value, _ := strconv.ParseInt(strings.TrimSpace(c.Query(key)), 10, 64)
return value
}

View File

@ -0,0 +1,16 @@
package cpweeklyrank
import (
"hyapp-admin-server/internal/middleware"
"github.com/gin-gonic/gin"
)
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
if h == nil {
return
}
protected.GET("/admin/activity/cp-weekly-rank/config", middleware.RequirePermission("cp-weekly-rank:view"), h.GetConfig)
protected.PUT("/admin/activity/cp-weekly-rank/config", middleware.RequirePermission("cp-weekly-rank:update"), h.UpdateConfig)
protected.GET("/admin/activity/cp-weekly-rank/settlements", middleware.RequirePermission("cp-weekly-rank:view"), h.ListSettlements)
}

View File

@ -176,38 +176,12 @@ func (h *Handler) resolveGrantUserID(ctx context.Context, keyword string) (int64
if keyword == "" {
return 0, false, true
}
if h.userDB == nil {
return 0, false, true
}
// keyword 支持真实 user_id、短 ID 和昵称模糊匹配;排序优先精确 ID避免昵称碰撞查到错误用户。
appCode := appctx.FromContext(ctx)
nowMs := time.Now().UnixMilli()
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs)
orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs)
args := append([]any{appCode}, matchArgs...)
args = append(args, orderArgs...)
rows, err := h.userDB.QueryContext(ctx, `
SELECT u.user_id
FROM users u
WHERE u.app_code = ? AND `+matchSQL+`
ORDER BY
`+orderSQL+`,
u.user_id DESC
LIMIT 1`,
args...,
)
// 累计充值奖励按 activity-service 的内部 user_id 过滤keyword 的长 ID、短号、靓号、昵称优先级由 shared 统一维护。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil {
return 0, false, false
}
defer rows.Close()
if !rows.Next() {
return 0, false, rows.Err() == nil
}
var userID int64
if err := rows.Scan(&userID); err != nil {
return 0, false, false
}
return userID, true, rows.Err() == nil
return userID, matched, true
}
func (h *Handler) fillGrantUsers(ctx context.Context, grants []grantDTO) error {

View File

@ -31,6 +31,7 @@ func (h *Handler) DashboardOverview(c *gin.Context) {
func (h *Handler) StatisticsOverview(c *gin.Context) {
overview, err := h.service.StatisticsOverview(c.Request.Context(), StatisticsQuery{
AppCode: c.DefaultQuery("app_code", "lalu"),
StatTZ: c.Query("stat_tz"),
StartMS: parseInt64(c.Query("start_ms")),
EndMS: parseInt64(c.Query("end_ms")),
SeriesStartMS: parseInt64(c.Query("series_start_ms")),
@ -48,6 +49,7 @@ func (h *Handler) StatisticsOverview(c *gin.Context) {
func (h *Handler) SelfGameStatisticsOverview(c *gin.Context) {
overview, err := h.service.SelfGameStatisticsOverview(c.Request.Context(), SelfGameStatisticsQuery{
AppCode: c.DefaultQuery("app_code", "lalu"),
StatTZ: c.Query("stat_tz"),
RequestID: middleware.CurrentRequestID(c),
StartMS: parseInt64(c.Query("start_ms")),
EndMS: parseInt64(c.Query("end_ms")),

View File

@ -23,6 +23,7 @@ type DashboardService struct {
type StatisticsQuery struct {
AppCode string
StatTZ string
StartMS int64
EndMS int64
SeriesStartMS int64
@ -33,6 +34,7 @@ type StatisticsQuery struct {
type SelfGameStatisticsQuery struct {
AppCode string
StatTZ string
RequestID string
StartMS int64
EndMS int64
@ -57,6 +59,9 @@ func (s *DashboardService) Overview() (*repository.DashboardOverview, error) {
func (s *DashboardService) StatisticsOverview(ctx context.Context, query StatisticsQuery) (map[string]any, error) {
values := url.Values{}
values.Set("app_code", query.AppCode)
if statTZ := strings.TrimSpace(query.StatTZ); statTZ != "" {
values.Set("stat_tz", statTZ)
}
if query.StartMS > 0 {
values.Set("start_ms", strconv.FormatInt(query.StartMS, 10))
}
@ -81,6 +86,9 @@ func (s *DashboardService) StatisticsOverview(ctx context.Context, query Statist
func (s *DashboardService) SelfGameStatisticsOverview(ctx context.Context, query SelfGameStatisticsQuery) (map[string]any, error) {
values := url.Values{}
values.Set("app_code", query.AppCode)
if statTZ := strings.TrimSpace(query.StatTZ); statTZ != "" {
values.Set("stat_tz", statTZ)
}
if query.StartMS > 0 {
values.Set("start_ms", strconv.FormatInt(query.StartMS, 10))
}

View File

@ -181,37 +181,12 @@ func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string) (int64
if keyword == "" {
return 0, false, true
}
if h.userDB == nil {
return 0, false, true
}
appCode := appctx.FromContext(ctx)
nowMs := time.Now().UnixMilli()
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs)
orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs)
args := append([]any{appCode}, matchArgs...)
args = append(args, orderArgs...)
rows, err := h.userDB.QueryContext(ctx, `
SELECT u.user_id
FROM users u
WHERE u.app_code = ? AND `+matchSQL+`
ORDER BY
`+orderSQL+`,
u.user_id DESC
LIMIT 1`,
args...,
)
// 奖励领取记录由 activity-service 按内部 user_id 过滤;后台关键词先统一解析,避免各活动模块复制短号/靓号 SQL。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil {
return 0, false, false
}
defer rows.Close()
if !rows.Next() {
return 0, false, rows.Err() == nil
}
var userID int64
if err := rows.Scan(&userID); err != nil {
return 0, false, false
}
return userID, true, rows.Err() == nil
return userID, matched, true
}
func (h *Handler) fillClaimUsers(ctx context.Context, claims []claimDTO) error {

View File

@ -16,6 +16,7 @@ const (
robotResourceDurationMS = int64(robotResourceDurationDays) * int64(24*time.Hour/time.Millisecond)
robotMinDisplayLevel = 5
robotMaxDisplayLevel = 30
robotMaxAvatarFrameCoins = 2_000_000
)
var excludedRobotAvatarFrameResourceIDs = map[int64]struct{}{
@ -83,6 +84,11 @@ func (h *Handler) loadRobotAppearanceCatalog(ctx context.Context, requestID stri
}
catalog := robotAppearanceCatalog{avatarFrames: avatarFrames, vehicles: vehicles}
for _, badge := range badges {
if !isRobotVIPBadgeResource(badge) {
// 机器人批量账号只使用 VIP 体系徽章;普通活动、成就和运营徽章仍留给真实用户或明确活动链路,
// 避免机器人大量占用非 VIP 资源造成客户端误判身份来源。
continue
}
switch robotBadgeForm(badge.GetMetadataJson()) {
case "strip", "long":
catalog.longBadges = append(catalog.longBadges, badge)
@ -115,6 +121,11 @@ func (h *Handler) listRobotResources(ctx context.Context, requestID string, appC
// 真人发放、商城和其他后台场景不受这个创建机器人专用策略影响。
continue
}
if resourceType == "avatar_frame" && item.GetCoinPrice() > robotMaxAvatarFrameCoins {
// 头像框用金币价格做机器人随机池硬边界;价格高于 2000000 的资源通常是稀有或运营保留展示,
// 创建机器人时不应自动发放,线上批量修复脚本也复用同一条规则。
continue
}
resources = append(resources, item)
}
return resources, nil
@ -128,6 +139,15 @@ func isExcludedRobotAppearanceResource(resourceType string, resourceID int64) bo
return excluded
}
func isRobotVIPBadgeResource(resource *walletv1.Resource) bool {
if resource == nil || resource.GetResourceType() != "badge" {
return false
}
code := strings.ToLower(strings.TrimSpace(resource.GetResourceCode()))
name := strings.ToLower(strings.TrimSpace(resource.GetName()))
return strings.HasPrefix(code, "vip") || strings.HasPrefix(name, "vip")
}
func (h *Handler) grantAndEquipRobotResource(ctx context.Context, requestID string, appCode string, actorUserID int64, userID int64, slot string, resourceID int64, equip bool) error {
grant, err := h.wallet.GrantResource(ctx, &walletv1.GrantResourceRequest{
CommandId: fmt.Sprintf("game_robot_init:%s:%d:%s:%d", requestID, userID, slot, resourceID),

View File

@ -75,16 +75,19 @@ func (c *fakeRobotWalletClient) ListResources(_ context.Context, req *walletv1.L
switch req.GetResourceType() {
case "avatar_frame":
return &walletv1.ListResourcesResponse{Resources: []*walletv1.Resource{
{ResourceId: 165, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false},
{ResourceId: 175, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false},
{ResourceId: 101, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false},
{ResourceId: 165, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false, CoinPrice: 1_000_000},
{ResourceId: 175, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false, CoinPrice: 1_500_000},
{ResourceId: 101, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false, CoinPrice: robotMaxAvatarFrameCoins},
{ResourceId: 176, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false, CoinPrice: robotMaxAvatarFrameCoins + 1},
}}, nil
case "vehicle":
return &walletv1.ListResourcesResponse{Resources: []*walletv1.Resource{{ResourceId: 201, ResourceType: "vehicle", Status: "active", ManagerGrantEnabled: false}}}, nil
case "badge":
return &walletv1.ListResourcesResponse{Resources: []*walletv1.Resource{
{ResourceId: 301, ResourceType: "badge", Status: "active", ManagerGrantEnabled: false, MetadataJson: `{"badge_form":"strip"}`},
{ResourceId: 302, ResourceType: "badge", Status: "active", ManagerGrantEnabled: false, MetadataJson: `{"badge_form":"tile"}`},
{ResourceId: 301, ResourceCode: "event_host_strip", ResourceType: "badge", Name: "Host Badge", Status: "active", ManagerGrantEnabled: false, MetadataJson: `{"badge_form":"strip"}`},
{ResourceId: 302, ResourceCode: "activity_tile", ResourceType: "badge", Name: "Activity Badge", Status: "active", ManagerGrantEnabled: false, MetadataJson: `{"badge_form":"tile"}`},
{ResourceId: 303, ResourceCode: "VIP1_Long_Badge", ResourceType: "badge", Name: "VIP1_Long Badge", Status: "active", ManagerGrantEnabled: false, MetadataJson: `{"badge_form":"strip"}`},
{ResourceId: 304, ResourceCode: "vip1badge", ResourceType: "badge", Name: "VIP1badge", Status: "active", ManagerGrantEnabled: false, MetadataJson: `{"badge_form":"tile"}`},
}}, nil
default:
return &walletv1.ListResourcesResponse{}, nil
@ -178,9 +181,11 @@ func TestGenerateDiceRobotsUsesLikeiProfiles(t *testing.T) {
}
}
for _, resourceID := range append(wallets.grants, wallets.equips...) {
// 165 和 175 是资源库里的有效头像框,但创建机器人不能随机抽中,避免批量机器人展示这两款运营保留资源。
if resourceID == 165 || resourceID == 175 {
t.Fatalf("robot appearance must exclude avatar frame %d from random grants/equips", resourceID)
// 165/175 是资源库里的有效头像框但属于运营保留资源176 超过机器人头像框价格上限;
// 301/302 是非 VIP 徽章。创建机器人随机池必须同时排除这些资源,避免新账号继续带出旧规则。
switch resourceID {
case 165, 175, 176, 301, 302:
t.Fatalf("robot appearance must exclude resource %d from random grants/equips", resourceID)
}
}
}

View File

@ -340,9 +340,9 @@ func (h *Handler) DebitCoinSellerStock(c *gin.Context) {
response.BadRequest(c, err.Error())
return
}
writeHostOrgAuditLog(c, h.audit, "coin-seller-stock-debit", "wallet_entries", sellerUserID,
fmt.Sprintf("command_id=%s seller_user_id=%d coin_amount=%d transaction_id=%s reason=%q",
strings.TrimSpace(req.CommandID), sellerUserID, req.CoinAmount, result.GetTransactionId(), strings.TrimSpace(req.Reason)))
writeHostOrgAuditLog(c, h.audit, "coin-seller-stock-debit", "coin_seller_stock_records", sellerUserID,
fmt.Sprintf("command_id=%s seller_user_id=%d stock_type=%s coin_amount=%d paid_currency_code=%s paid_amount_micro=%d transaction_id=%s reason=%q",
strings.TrimSpace(req.CommandID), sellerUserID, result.GetStockType(), result.GetCoinAmount(), result.GetPaidCurrencyCode(), result.GetPaidAmountMicro(), result.GetTransactionId(), strings.TrimSpace(req.Reason)))
response.Created(c, coinSellerStockDebitFromProto(sellerUserID, req.CoinAmount, result))
}

View File

@ -53,11 +53,13 @@ type ManagerListItem struct {
Contact string `json:"contact"`
CanGrantAvatarFrame bool `json:"canGrantAvatarFrame"`
CanGrantVehicle bool `json:"canGrantVehicle"`
CanGrantBadge bool `json:"canGrantBadge"`
CanUpdateUserLevel bool `json:"canUpdateUserLevel"`
CanAddBDLeader bool `json:"canAddBdLeader"`
CanAddAdmin bool `json:"canAddAdmin"`
CanAddSuperadmin bool `json:"canAddSuperadmin"`
CanBlockUser bool `json:"canBlockUser"`
CanTransferCountry bool `json:"canTransferUserCountry"`
BDLeaderCount int64 `json:"bdLeaderCount"`
LastInvitedAtMs int64 `json:"lastInvitedAtMs"`
CreatedAtMs int64 `json:"createdAtMs"`
@ -600,24 +602,26 @@ func (r *Reader) CreateManagerProfile(ctx context.Context, userID int64, actorID
// 不新增第二条身份,也不触碰该经理已经邀请出的 BD Leader/Admin 归属统计。
if _, err := r.db.ExecContext(ctx, `
INSERT INTO manager_profiles (
app_code, user_id, status, contact_info, can_grant_avatar_frame, can_grant_vehicle,
can_update_user_level, can_add_bd_leader, can_add_admin, can_add_superadmin, can_block_user,
app_code, user_id, status, contact_info, can_grant_avatar_frame, can_grant_vehicle, can_grant_badge,
can_update_user_level, can_add_bd_leader, can_add_admin, can_add_superadmin, can_block_user, can_transfer_user_country,
created_by_admin_id, created_at_ms, updated_at_ms
) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
status = 'active',
contact_info = VALUES(contact_info),
can_grant_avatar_frame = VALUES(can_grant_avatar_frame),
can_grant_vehicle = VALUES(can_grant_vehicle),
can_grant_badge = VALUES(can_grant_badge),
can_update_user_level = VALUES(can_update_user_level),
can_add_bd_leader = VALUES(can_add_bd_leader),
can_add_admin = VALUES(can_add_admin),
can_add_superadmin = VALUES(can_add_superadmin),
can_block_user = VALUES(can_block_user),
can_transfer_user_country = VALUES(can_transfer_user_country),
updated_at_ms = VALUES(updated_at_ms)
`, appCode, userID, contact, permissions.CanGrantAvatarFrame, permissions.CanGrantVehicle,
permissions.CanUpdateUserLevel, permissions.CanAddBDLeader, permissions.CanAddAdmin,
permissions.CanAddSuperadmin, permissions.CanBlockUser, actorID, nowMs, nowMs); err != nil {
permissions.CanGrantBadge, permissions.CanUpdateUserLevel, permissions.CanAddBDLeader, permissions.CanAddAdmin,
permissions.CanAddSuperadmin, permissions.CanBlockUser, permissions.CanTransferCountry, actorID, nowMs, nowMs); err != nil {
return nil, err
}
items, _, err := r.ListManagers(ctx, listQuery{Keyword: strconv.FormatInt(userID, 10), Page: 1, PageSize: 1})
@ -716,15 +720,15 @@ func (r *Reader) ListManagers(ctx context.Context, query listQuery) ([]*ManagerL
SELECT mp.user_id, COALESCE(manager.current_display_user_id, ''), COALESCE(manager.username, ''),
COALESCE(manager.avatar, ''), `+userCountryRegionIDSQL("manager_region")+`, `+userCountryRegionNameSQL("manager_region")+`,
mp.status, COALESCE(mp.contact_info, ''),
mp.can_grant_avatar_frame, mp.can_grant_vehicle, mp.can_update_user_level,
mp.can_add_bd_leader, mp.can_add_admin, mp.can_add_superadmin, mp.can_block_user,
mp.can_grant_avatar_frame, mp.can_grant_vehicle, mp.can_grant_badge, mp.can_update_user_level,
mp.can_add_bd_leader, mp.can_add_admin, mp.can_add_superadmin, mp.can_block_user, mp.can_transfer_user_country,
COUNT(DISTINCT bl.user_id), COALESCE(MAX(bl.created_at_ms), 0),
mp.created_at_ms, mp.updated_at_ms
%s
GROUP BY mp.user_id, manager.current_display_user_id, manager.username, manager.avatar,
manager_region.region_id, manager_region.name, mp.status, mp.contact_info,
mp.can_grant_avatar_frame, mp.can_grant_vehicle, mp.can_update_user_level,
mp.can_add_bd_leader, mp.can_add_admin, mp.can_add_superadmin, mp.can_block_user,
mp.can_grant_avatar_frame, mp.can_grant_vehicle, mp.can_grant_badge, mp.can_update_user_level,
mp.can_add_bd_leader, mp.can_add_admin, mp.can_add_superadmin, mp.can_block_user, mp.can_transfer_user_country,
mp.created_at_ms, mp.updated_at_ms
ORDER BY mp.updated_at_ms DESC, mp.user_id DESC
LIMIT ? OFFSET ?
@ -748,11 +752,13 @@ func (r *Reader) ListManagers(ctx context.Context, query listQuery) ([]*ManagerL
&item.Contact,
&item.CanGrantAvatarFrame,
&item.CanGrantVehicle,
&item.CanGrantBadge,
&item.CanUpdateUserLevel,
&item.CanAddBDLeader,
&item.CanAddAdmin,
&item.CanAddSuperadmin,
&item.CanBlockUser,
&item.CanTransferCountry,
&item.BDLeaderCount,
&item.LastInvitedAtMs,
&item.CreatedAtMs,

View File

@ -65,54 +65,64 @@ type createManagerRequest struct {
TargetUserID flexibleInt64 `json:"targetUserId" binding:"required"`
CanGrantAvatarFrame *bool `json:"canGrantAvatarFrame"`
CanGrantVehicle *bool `json:"canGrantVehicle"`
CanGrantBadge *bool `json:"canGrantBadge"`
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
CanAddBDLeader *bool `json:"canAddBdLeader"`
CanAddAdmin *bool `json:"canAddAdmin"`
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
CanBlockUser *bool `json:"canBlockUser"`
CanTransferCountry *bool `json:"canTransferUserCountry"`
}
type updateManagerRequest struct {
Contact *string `json:"contact"`
CanGrantAvatarFrame *bool `json:"canGrantAvatarFrame"`
CanGrantVehicle *bool `json:"canGrantVehicle"`
CanGrantBadge *bool `json:"canGrantBadge"`
CanUpdateUserLevel *bool `json:"canUpdateUserLevel"`
CanAddBDLeader *bool `json:"canAddBdLeader"`
CanAddAdmin *bool `json:"canAddAdmin"`
CanAddSuperadmin *bool `json:"canAddSuperadmin"`
CanBlockUser *bool `json:"canBlockUser"`
CanTransferCountry *bool `json:"canTransferUserCountry"`
}
type managerPermissions struct {
CanGrantAvatarFrame bool
CanGrantVehicle bool
CanGrantBadge bool
CanUpdateUserLevel bool
CanAddBDLeader bool
CanAddAdmin bool
CanAddSuperadmin bool
CanBlockUser bool
CanTransferCountry bool
}
func (r createManagerRequest) permissionsWithDefault() managerPermissions {
return managerPermissions{
CanGrantAvatarFrame: boolDefaultTrue(r.CanGrantAvatarFrame),
CanGrantVehicle: boolDefaultTrue(r.CanGrantVehicle),
CanGrantBadge: boolDefaultTrue(r.CanGrantBadge),
CanUpdateUserLevel: boolDefaultTrue(r.CanUpdateUserLevel),
CanAddBDLeader: boolDefaultTrue(r.CanAddBDLeader),
CanAddAdmin: boolDefaultTrue(r.CanAddAdmin),
CanAddSuperadmin: boolDefaultTrue(r.CanAddSuperadmin),
CanBlockUser: boolDefaultTrue(r.CanBlockUser),
CanTransferCountry: boolDefaultTrue(r.CanTransferCountry),
}
}
func (r updateManagerRequest) appendPermissionAssignments(assignments *[]string, args *[]any) {
appendBoolAssignment(assignments, args, "can_grant_avatar_frame", r.CanGrantAvatarFrame)
appendBoolAssignment(assignments, args, "can_grant_vehicle", r.CanGrantVehicle)
appendBoolAssignment(assignments, args, "can_grant_badge", r.CanGrantBadge)
appendBoolAssignment(assignments, args, "can_update_user_level", r.CanUpdateUserLevel)
appendBoolAssignment(assignments, args, "can_add_bd_leader", r.CanAddBDLeader)
appendBoolAssignment(assignments, args, "can_add_admin", r.CanAddAdmin)
appendBoolAssignment(assignments, args, "can_add_superadmin", r.CanAddSuperadmin)
appendBoolAssignment(assignments, args, "can_block_user", r.CanBlockUser)
appendBoolAssignment(assignments, args, "can_transfer_user_country", r.CanTransferCountry)
}
func appendBoolAssignment(assignments *[]string, args *[]any, column string, value *bool) {
@ -171,10 +181,11 @@ type coinSellerStockCreditRequest struct {
}
type coinSellerStockDebitRequest struct {
CommandID string `json:"commandId" binding:"required"`
CoinAmount int64 `json:"coinAmount" binding:"required"`
EvidenceRef string `json:"evidenceRef"`
Reason string `json:"reason" binding:"required"`
CommandID string `json:"commandId" binding:"required"`
CoinAmount int64 `json:"coinAmount" binding:"required"`
RechargeAmount string `json:"rechargeAmount" binding:"required"`
EvidenceRef string `json:"evidenceRef"`
Reason string `json:"reason" binding:"required"`
}
type replaceCoinSellerSalaryRatesRequest struct {

View File

@ -15,11 +15,16 @@ type coinSellerStockCreditResponse struct {
}
type coinSellerStockDebitResponse struct {
TransactionID string `json:"transactionId"`
SellerUserID int64 `json:"sellerUserId,string"`
CoinAmount int64 `json:"coinAmount"`
AvailableDelta int64 `json:"availableDelta"`
BalanceAfter int64 `json:"balanceAfter"`
TransactionID string `json:"transactionId"`
SellerUserID int64 `json:"sellerUserId,string"`
StockType string `json:"stockType"`
CoinAmount int64 `json:"coinAmount"`
AvailableDelta int64 `json:"availableDelta"`
PaidCurrencyCode string `json:"paidCurrencyCode"`
PaidAmountMicro int64 `json:"paidAmountMicro"`
CountsAsSellerRecharge bool `json:"countsAsSellerRecharge"`
BalanceAfter int64 `json:"balanceAfter"`
CreatedAtMS int64 `json:"createdAtMs"`
}
func coinSellerStockCreditFromProto(item *walletv1.AdminCreditCoinSellerStockResponse) coinSellerStockCreditResponse {
@ -39,19 +44,20 @@ func coinSellerStockCreditFromProto(item *walletv1.AdminCreditCoinSellerStockRes
}
}
func coinSellerStockDebitFromProto(sellerUserID int64, coinAmount int64, item *walletv1.AdminCreditAssetResponse) coinSellerStockDebitResponse {
func coinSellerStockDebitFromProto(sellerUserID int64, coinAmount int64, item *walletv1.AdminCreditCoinSellerStockResponse) coinSellerStockDebitResponse {
if item == nil {
return coinSellerStockDebitResponse{SellerUserID: sellerUserID, CoinAmount: coinAmount, AvailableDelta: -coinAmount}
}
balanceAfter := int64(0)
if item.GetBalance() != nil {
balanceAfter = item.GetBalance().GetAvailableAmount()
}
return coinSellerStockDebitResponse{
TransactionID: item.GetTransactionId(),
SellerUserID: sellerUserID,
CoinAmount: coinAmount,
AvailableDelta: -coinAmount,
BalanceAfter: balanceAfter,
TransactionID: item.GetTransactionId(),
SellerUserID: sellerUserID,
StockType: item.GetStockType(),
CoinAmount: item.GetCoinAmount(),
AvailableDelta: item.GetCoinAmount(),
PaidCurrencyCode: item.GetPaidCurrencyCode(),
PaidAmountMicro: item.GetPaidAmountMicro(),
CountsAsSellerRecharge: item.GetCountsAsSellerRecharge(),
BalanceAfter: item.GetBalanceAfter(),
CreatedAtMS: item.GetCreatedAtMs(),
}
}

View File

@ -13,9 +13,9 @@ import (
)
const (
coinSellerStatusActive = "active"
coinSellerMerchantAssetType = "COIN_SELLER_COIN"
coinSellerStockTypePurchase = "usdt_purchase"
coinSellerStockTypeDeduction = "usdt_deduction"
coinSellerStockTypeCompensate = "coin_compensation"
paidCurrencyUSDT = "USDT"
sortByDiamond = "diamond"
@ -312,7 +312,7 @@ func (s *Service) CreditCoinSellerStock(ctx context.Context, actorID int64, sell
return nil, fmt.Errorf("stock type is invalid")
}
seller, err := s.activeCoinSellerUser(ctx, sellerUserID, requestID)
seller, err := s.coinSellerUser(ctx, sellerUserID, requestID)
if err != nil {
return nil, err
}
@ -334,7 +334,7 @@ func (s *Service) CreditCoinSellerStock(ctx context.Context, actorID int64, sell
})
}
func (s *Service) DebitCoinSellerStock(ctx context.Context, actorID int64, sellerUserID int64, requestID string, req coinSellerStockDebitRequest) (*walletv1.AdminCreditAssetResponse, error) {
func (s *Service) DebitCoinSellerStock(ctx context.Context, actorID int64, sellerUserID int64, requestID string, req coinSellerStockDebitRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) {
if s.walletClient == nil {
return nil, fmt.Errorf("wallet client is not configured")
}
@ -353,24 +353,33 @@ func (s *Service) DebitCoinSellerStock(ctx context.Context, actorID int64, selle
if len(reason) > 512 || len(commandID) > 128 || len(evidenceRef) > 256 {
return nil, fmt.Errorf("coin seller debit text fields are too long")
}
if _, err := s.activeCoinSellerUser(ctx, sellerUserID, requestID); err != nil {
paidAmountMicro, err := parseUSDTAmountMicro(req.RechargeAmount)
if err != nil {
return nil, err
}
// 币商扣除是运营对币商专用库存的负向调账:只扣 COIN_SELLER_COIN不写普通 COIN也不写进货记录。
// wallet-service 的 AdminCreditAsset 负责余额不足校验、幂等冲突检测、wallet_entries 和 outbox 原子落账。
return s.walletClient.AdminCreditAsset(ctx, &walletv1.AdminCreditAssetRequest{
CommandId: commandID,
TargetUserId: sellerUserID,
AssetType: coinSellerMerchantAssetType,
Amount: -req.CoinAmount,
OperatorUserId: actorID,
Reason: reason,
EvidenceRef: evidenceRef,
AppCode: appctx.FromContext(ctx),
seller, err := s.coinSellerUser(ctx, sellerUserID, requestID)
if err != nil {
return nil, err
}
// 币商扣除按 USDT 进货冲回处理:钱包事务会同时写负向库存分录、负向进货记录和统计 outbox
// 这样后台列表、币商流水和充值聚合都使用同一条账务事实,避免只扣余额却不冲收入。
return s.walletClient.AdminCreditCoinSellerStock(ctx, &walletv1.AdminCreditCoinSellerStockRequest{
CommandId: commandID,
SellerUserId: sellerUserID,
SellerCountryId: seller.CountryID,
SellerRegionId: seller.RegionID,
StockType: coinSellerStockTypeDeduction,
CoinAmount: req.CoinAmount,
PaidCurrencyCode: paidCurrencyUSDT,
PaidAmountMicro: paidAmountMicro,
EvidenceRef: evidenceRef,
OperatorUserId: actorID,
Reason: reason,
AppCode: appctx.FromContext(ctx),
})
}
func (s *Service) activeCoinSellerUser(ctx context.Context, sellerUserID int64, requestID string) (*userclient.User, error) {
func (s *Service) coinSellerUser(ctx context.Context, sellerUserID int64, requestID string) (*userclient.User, error) {
if s == nil || s.userClient == nil {
return nil, fmt.Errorf("user client is not configured")
}
@ -382,8 +391,8 @@ func (s *Service) activeCoinSellerUser(ctx context.Context, sellerUserID int64,
if err != nil {
return nil, err
}
if profile == nil || profile.Status != coinSellerStatusActive || profile.MerchantAssetType != coinSellerMerchantAssetType {
return nil, fmt.Errorf("target user is not an active coin seller")
if profile == nil || profile.MerchantAssetType != coinSellerMerchantAssetType {
return nil, fmt.Errorf("target user is not a coin seller")
}
seller, err := s.userClient.GetUser(ctx, userclient.GetUserRequest{
RequestID: requestID,

View File

@ -237,37 +237,12 @@ func (h *Handler) resolveUserID(ctx context.Context, keyword string) (int64, boo
if keyword == "" {
return 0, false, true
}
if h.userDB == nil {
return 0, false, true
}
appCode := appctx.FromContext(ctx)
nowMs := time.Now().UnixMilli()
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs)
orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs)
args := append([]any{appCode}, matchArgs...)
args = append(args, orderArgs...)
rows, err := h.userDB.QueryContext(ctx, `
SELECT u.user_id
FROM users u
WHERE u.app_code = ? AND `+matchSQL+`
ORDER BY
`+orderSQL+`,
u.user_id DESC
LIMIT 1`,
args...,
)
// 邀请奖励汇总页只把内部 user_id 交给 activity-service展示 ID 的兼容规则集中在 shared 解析器里。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil {
return 0, false, false
}
defer rows.Close()
if !rows.Next() {
return 0, false, rows.Err() == nil
}
var userID int64
if err := rows.Scan(&userID); err != nil {
return 0, false, false
}
return userID, true, rows.Err() == nil
return userID, matched, true
}
func (h *Handler) fillUsers(ctx context.Context, claims []claimDTO) error {

View File

@ -96,6 +96,31 @@ type thirdPartyPaymentMethodDTO struct {
UpdatedAtMS int64 `json:"updatedAtMs"`
}
type temporaryPaymentLinkDTO struct {
OrderID string `json:"orderId"`
AppCode string `json:"appCode"`
CommandID string `json:"commandId,omitempty"`
AudienceType string `json:"audienceType"`
ProductName string `json:"productName"`
CoinAmount int64 `json:"coinAmount"`
USDMinorAmount int64 `json:"usdMinorAmount"`
ProviderCode string `json:"providerCode"`
PaymentMethodID int64 `json:"paymentMethodId"`
CountryCode string `json:"countryCode"`
CurrencyCode string `json:"currencyCode"`
ProviderAmountMinor int64 `json:"providerAmountMinor"`
PayWay string `json:"payWay"`
PayType string `json:"payType"`
PayURL string `json:"payUrl"`
ProviderOrderID string `json:"providerOrderId"`
TxHash string `json:"txHash"`
Status string `json:"status"`
FailureReason string `json:"failureReason"`
TransactionID string `json:"transactionId"`
CreatedAtMS int64 `json:"createdAtMs"`
UpdatedAtMS int64 `json:"updatedAtMs"`
}
func rechargeBillFromProto(item *walletv1.RechargeBill) rechargeBillDTO {
if item == nil {
return rechargeBillDTO{}
@ -247,6 +272,35 @@ func thirdPartyPaymentMethodFromProto(item *walletv1.ThirdPartyPaymentMethod) th
}
}
func temporaryPaymentLinkFromProto(item *walletv1.ExternalRechargeOrder) temporaryPaymentLinkDTO {
if item == nil {
return temporaryPaymentLinkDTO{}
}
return temporaryPaymentLinkDTO{
OrderID: item.GetOrderId(),
AppCode: item.GetAppCode(),
AudienceType: item.GetAudienceType(),
ProductName: item.GetProductName(),
CoinAmount: item.GetCoinAmount(),
USDMinorAmount: item.GetUsdMinorAmount(),
ProviderCode: item.GetProviderCode(),
PaymentMethodID: item.GetPaymentMethodId(),
CountryCode: item.GetCountryCode(),
CurrencyCode: item.GetCurrencyCode(),
ProviderAmountMinor: item.GetProviderAmountMinor(),
PayWay: item.GetPayWay(),
PayType: item.GetPayType(),
PayURL: item.GetPayUrl(),
ProviderOrderID: item.GetProviderOrderId(),
TxHash: item.GetTxHash(),
Status: item.GetStatus(),
FailureReason: item.GetFailureReason(),
TransactionID: item.GetTransactionId(),
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
}
}
func formatUSDTMicro(amount int64) string {
if amount <= 0 {
return "0"

View File

@ -7,6 +7,7 @@ import (
"math"
"strconv"
"strings"
"time"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/walletclient"
@ -37,11 +38,19 @@ func New(wallet walletclient.Client, userDB *sql.DB, audit shared.OperationLogge
func (h *Handler) ListRechargeBills(c *gin.Context) {
options := shared.ListOptions(c)
appCode := appctx.FromContext(c.Request.Context())
userID, ok := h.resolveRechargeBillUserFilter(c, appCode, "user_id 参数不正确", "user_id", "userId")
if !ok {
return
}
sellerUserID, ok := h.resolveRechargeBillUserFilter(c, appCode, "seller_user_id 参数不正确", "seller_user_id", "sellerUserId")
if !ok {
return
}
resp, err := h.wallet.ListRechargeBills(c.Request.Context(), &walletv1.ListRechargeBillsRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appCode,
UserId: queryInt64(c, "user_id", "userId"),
SellerUserId: queryInt64(c, "seller_user_id", "sellerUserId"),
UserId: userID,
SellerUserId: sellerUserID,
RegionId: queryInt64(c, "region_id", "regionId"),
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
Status: options.Status,
@ -68,6 +77,24 @@ func (h *Handler) ListRechargeBills(c *gin.Context) {
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}
func (h *Handler) resolveRechargeBillUserFilter(c *gin.Context, appCode string, invalidMessage string, keys ...string) (int64, bool) {
keyword := strings.TrimSpace(firstQuery(c, keys...))
if keyword == "" {
return 0, true
}
// 充值账单事实在 wallet-service筛选字段只能传内部 user_id后台先把长 ID、默认短号、当前展示号和 active 靓号解析成同一个稳定 ID。
userID, _, err := shared.ResolveExactUserIDOrNumericFallback(c.Request.Context(), h.userDB, appCode, keyword, time.Now().UnixMilli())
if err != nil {
if errors.Is(err, shared.ErrInvalidUserIdentityFilter) {
response.BadRequest(c, invalidMessage)
return 0, false
}
response.ServerError(c, "查询账单用户信息失败")
return 0, false
}
return userID, true
}
// fillRechargeBillUsers 将用户资料写回当前页账单 DTO缺失资料会在 DTO 层保留长 ID避免影响账单事实展示。
func (h *Handler) fillRechargeBillUsers(ctx context.Context, appCode string, items []rechargeBillDTO) error {
userIDs := collectRechargeBillUserIDs(items)
@ -261,6 +288,47 @@ func (h *Handler) ListThirdPartyPaymentChannels(c *gin.Context) {
response.OK(c, gin.H{"items": items, "total": len(items)})
}
func (h *Handler) ListTemporaryPaymentLinks(c *gin.Context) {
options := shared.ListOptions(c)
resp, err := h.wallet.ListTemporaryRechargeOrders(c.Request.Context(), &walletv1.ListTemporaryRechargeOrdersRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
Status: options.Status,
ProviderCode: strings.TrimSpace(firstQuery(c, "provider_code", "providerCode")),
Keyword: options.Keyword,
Page: int32(options.Page),
PageSize: int32(options.PageSize),
})
if err != nil {
writeWalletError(c, err, "获取三方临时支付链接失败")
return
}
items := make([]temporaryPaymentLinkDTO, 0, len(resp.GetOrders()))
for _, item := range resp.GetOrders() {
items = append(items, temporaryPaymentLinkFromProto(item))
}
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}
func (h *Handler) GetTemporaryPaymentLink(c *gin.Context) {
orderID := strings.TrimSpace(c.Param("order_id"))
if orderID == "" {
response.BadRequest(c, "支付链接订单号不能为空")
return
}
// 查询单条订单会进入 wallet-service 的支付状态刷新逻辑admin 页面只发起核验,不在浏览器里拼三方查询参数。
resp, err := h.wallet.GetTemporaryRechargeOrder(c.Request.Context(), &walletv1.GetTemporaryRechargeOrderRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
OrderId: orderID,
})
if err != nil {
writeWalletError(c, err, "校验三方临时支付链接失败")
return
}
response.OK(c, temporaryPaymentLinkFromProto(resp.GetOrder()))
}
func (h *Handler) SetThirdPartyPaymentMethodStatus(c *gin.Context) {
methodID := queryPathInt64(c, "method_id")
if methodID <= 0 {
@ -316,6 +384,14 @@ func (h *Handler) UpdateThirdPartyPaymentRate(c *gin.Context) {
}
func (h *Handler) SyncThirdPartyPaymentRates(c *gin.Context) {
var request thirdPartyPaymentRateSyncRequest
if c.Request.Body != nil && c.Request.ContentLength != 0 {
// 同步接口历史上允许空 body 直接触发;现在弹窗只新增上浮比例,空 body 仍按 0% 处理,避免旧后台或脚本调用被破坏。
if err := c.ShouldBindJSON(&request); err != nil || !validThirdPartyPaymentRateMarkup(request.MarkupPercent) {
response.BadRequest(c, "支付汇率同步参数不正确")
return
}
}
resp, err := h.wallet.ListThirdPartyPaymentChannels(c.Request.Context(), &walletv1.ListThirdPartyPaymentChannelsRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
@ -332,7 +408,7 @@ func (h *Handler) SyncThirdPartyPaymentRates(c *gin.Context) {
response.OK(c, thirdPartyPaymentRateSyncDTO{UpdatedCount: 0})
return
}
// 汇率同步必须以服务端实时拉取结果为准:前端按钮只触发动作,不传任何汇率,避免浏览器篡改本币金额
// 汇率同步必须以服务端实时拉取结果为准:前端只传运营确认的上浮比例,原始汇率、失败切换和最终写库都留在服务端收敛
rateClient := h.exchangeRates
if rateClient == nil {
rateClient = newDefaultExchangeRateClient()
@ -344,10 +420,11 @@ func (h *Handler) SyncThirdPartyPaymentRates(c *gin.Context) {
}
updated := 0
skipped := make(map[string]struct{})
writtenRates := make(map[string]string, len(rateResult.Rates))
for _, method := range methods {
currencyCode := strings.ToUpper(strings.TrimSpace(method.GetCurrencyCode()))
rate := rateResult.Rates[currencyCode]
if rate == "" {
rate, ok := applyThirdPartyPaymentRateMarkup(rateResult.Rates[currencyCode], request.MarkupPercent)
if !ok {
skipped[currencyCode] = struct{}{}
continue
}
@ -361,6 +438,7 @@ func (h *Handler) SyncThirdPartyPaymentRates(c *gin.Context) {
writeWalletError(c, err, "写入支付汇率失败")
return
}
writtenRates[currencyCode] = rate
updated++
}
skippedCurrencies := sortedMapKeys(skipped)
@ -371,7 +449,7 @@ func (h *Handler) SyncThirdPartyPaymentRates(c *gin.Context) {
MissingCurrencies: skippedCurrencies,
SourceNames: rateResult.SourceNames,
Sources: rateResult.Sources,
Rates: rateResult.Rates,
Rates: writtenRates,
})
}
@ -395,6 +473,10 @@ type thirdPartyPaymentRateRequest struct {
USDToCurrencyRate string `json:"usdToCurrencyRate"`
}
type thirdPartyPaymentRateSyncRequest struct {
MarkupPercent float64 `json:"markupPercent"`
}
func queryInt64(c *gin.Context, keys ...string) int64 {
value := strings.TrimSpace(firstQuery(c, keys...))
if value == "" {
@ -491,6 +573,27 @@ func allDigits(value string) bool {
return value != ""
}
func validThirdPartyPaymentRateMarkup(markupPercent float64) bool {
// 上浮比例来自后台运营弹窗,只允许 0-100 的有限数值;负数会压低支付汇率,过大数值会直接放大用户本币支付金额。
return !math.IsNaN(markupPercent) && !math.IsInf(markupPercent, 0) && markupPercent >= 0 && markupPercent <= 100
}
func applyThirdPartyPaymentRateMarkup(rateText string, markupPercent float64) (string, bool) {
rawRate, err := strconv.ParseFloat(strings.TrimSpace(rateText), 64)
if err != nil || rawRate <= 0 || !validThirdPartyPaymentRateMarkup(markupPercent) {
return "", false
}
// 汇率源返回的是实时 USD->本币原值;写库前统一叠加运营设置的上浮比例,再按 1 位小数向上取,保证 H5 下单看到的支付汇率不低于实时汇率。
markedRate := rawRate * (1 + markupPercent/100)
roundedRate := math.Ceil(markedRate*10-1e-9) / 10
return trimOneDecimalRate(strconv.FormatFloat(roundedRate, 'f', 1, 64)), true
}
func trimOneDecimalRate(rateText string) string {
// 产品要求“最多 1 位小数”,整数汇率不保留无意义的 .0,避免后台列表和人工核对时出现多余精度。
return strings.TrimSuffix(rateText, ".0")
}
func writeWalletError(c *gin.Context, err error, fallback string) {
message := fallback
if st, ok := status.FromError(err); ok && strings.TrimSpace(st.Message()) != "" {

View File

@ -13,9 +13,42 @@ import (
"hyapp-admin-server/internal/middleware"
walletv1 "hyapp.local/api/proto/wallet/v1"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
)
func TestListRechargeBillsResolvesDisplayIDsBeforeWalletFilter(t *testing.T) {
db, sqlMock, err := sqlmock.New()
if err != nil {
t.Fatalf("create sql mock failed: %v", err)
}
defer db.Close()
wallet := &mockPaymentWallet{rechargeBillsResp: &walletv1.ListRechargeBillsResponse{}}
router := newPaymentHandlerTestRouter(New(wallet, db, nil))
userID := int64(327702592329093120)
sellerUserID := int64(328453424662192128)
expectUserIdentityLookup(sqlMock, "111", userID)
expectUserIdentityLookup(sqlMock, "167864", sellerUserID)
request := httptest.NewRequest(http.MethodGet, "/admin/payment/recharge-bills?user_id=111&seller_user_id=167864", nil)
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 wallet.lastRechargeBills == nil ||
wallet.lastRechargeBills.GetUserId() != userID ||
wallet.lastRechargeBills.GetSellerUserId() != sellerUserID {
t.Fatalf("recharge bill user filters should be resolved before wallet call: %+v", wallet.lastRechargeBills)
}
if err := sqlMock.ExpectationsWereMet(); err != nil {
t.Fatalf("sql expectations mismatch: %v", err)
}
}
func TestListThirdPartyPaymentChannelsForwardsIncludeDisabledMethods(t *testing.T) {
wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{
Channels: []*walletv1.ThirdPartyPaymentChannel{{
@ -90,6 +123,91 @@ func TestListThirdPartyPaymentChannelsForwardsIncludeDisabledMethods(t *testing.
}
}
func TestListTemporaryPaymentLinksForwardsFilters(t *testing.T) {
wallet := &mockPaymentWallet{temporaryOrdersResp: &walletv1.ListTemporaryRechargeOrdersResponse{
Total: 1,
Orders: []*walletv1.ExternalRechargeOrder{{
OrderId: "tmp_1001",
AppCode: "lalu",
AudienceType: "temporary",
ProductName: "Temporary payment USD 1.99",
CoinAmount: 0,
UsdMinorAmount: 199,
ProviderCode: "mifapay",
PaymentMethodId: 810,
CountryCode: "SA",
CurrencyCode: "SAR",
ProviderAmountMinor: 746,
PayWay: "Card",
PayType: "MADA",
PayUrl: "https://pay.example/tmp_1001",
ProviderOrderId: "mifa_1001",
Status: "redirected",
CreatedAtMs: 1700000000000,
UpdatedAtMs: 1700000000001,
}},
}}
router := newPaymentHandlerTestRouter(New(wallet, nil, nil))
request := httptest.NewRequest(http.MethodGet, "/admin/payment/temporary-links?status=redirected&provider_code=mifapay&keyword=tmp&page=2&page_size=30", nil)
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 wallet.lastTemporaryOrders == nil ||
wallet.lastTemporaryOrders.GetAppCode() != "lalu" ||
wallet.lastTemporaryOrders.GetStatus() != "redirected" ||
wallet.lastTemporaryOrders.GetProviderCode() != "mifapay" ||
wallet.lastTemporaryOrders.GetKeyword() != "tmp" ||
wallet.lastTemporaryOrders.GetPage() != 2 ||
wallet.lastTemporaryOrders.GetPageSize() != 30 {
t.Fatalf("temporary link list request mismatch: %+v", wallet.lastTemporaryOrders)
}
var response adminPaymentTestResponse
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode response failed: %v", err)
}
items := response.Data["items"].([]any)
item := items[0].(map[string]any)
if response.Code != 0 || response.Data["total"].(float64) != 1 || item["orderId"] != "tmp_1001" || item["payUrl"] != "https://pay.example/tmp_1001" || item["status"] != "redirected" {
t.Fatalf("temporary link response mismatch: %+v", response)
}
}
func TestGetTemporaryPaymentLinkRefreshesSingleOrder(t *testing.T) {
wallet := &mockPaymentWallet{temporaryOrderResp: &walletv1.H5RechargeOrderResponse{Order: &walletv1.ExternalRechargeOrder{
OrderId: "tmp_1001",
AppCode: "lalu",
AudienceType: "temporary",
ProviderCode: "v5pay",
PayUrl: "https://pay.example/tmp_1001",
Status: "paid",
}}}
router := newPaymentHandlerTestRouter(New(wallet, nil, nil))
request := httptest.NewRequest(http.MethodGet, "/admin/payment/temporary-links/tmp_1001", nil)
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 wallet.lastTemporaryOrder == nil ||
wallet.lastTemporaryOrder.GetAppCode() != "lalu" ||
wallet.lastTemporaryOrder.GetOrderId() != "tmp_1001" {
t.Fatalf("temporary link get request mismatch: %+v", wallet.lastTemporaryOrder)
}
var response adminPaymentTestResponse
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode response failed: %v", err)
}
if response.Code != 0 || response.Data["orderId"] != "tmp_1001" || response.Data["status"] != "paid" {
t.Fatalf("temporary link get response mismatch: %+v", response)
}
}
func TestSetThirdPartyPaymentMethodStatusForwardsOperator(t *testing.T) {
wallet := &mockPaymentWallet{}
router := newPaymentHandlerTestRouter(New(wallet, nil, nil))
@ -130,7 +248,7 @@ func TestUpdateThirdPartyPaymentRateTrimsRateAndForwardsOperator(t *testing.T) {
}
}
func TestSyncThirdPartyPaymentRatesUpdatesAllMethodsFromFetchedCurrencyRates(t *testing.T) {
func TestSyncThirdPartyPaymentRatesAppliesMarkupAndCeilsToOneDecimal(t *testing.T) {
wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{
Channels: []*walletv1.ThirdPartyPaymentChannel{{
AppCode: "lalu",
@ -157,7 +275,8 @@ func TestSyncThirdPartyPaymentRatesUpdatesAllMethodsFromFetchedCurrencyRates(t *
SourceNames: []string{"open.er-api.com"},
}}
router := newPaymentHandlerTestRouter(handler)
request := httptest.NewRequest(http.MethodPost, "/admin/payment/third-party-rates/sync", nil)
request := httptest.NewRequest(http.MethodPost, "/admin/payment/third-party-rates/sync", bytes.NewBufferString(`{"markupPercent":3}`))
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
@ -178,7 +297,7 @@ func TestSyncThirdPartyPaymentRatesUpdatesAllMethodsFromFetchedCurrencyRates(t *
}
got[req.GetMethodId()] = req.GetUsdToCurrencyRate()
}
if got[810] != "3.75000000" || got[811] != "0.37600000" || got[2310] != "3.75000000" {
if got[810] != "3.9" || got[811] != "0.4" || got[2310] != "3.9" {
t.Fatalf("synced rates mismatch: %+v", got)
}
var response adminPaymentTestResponse
@ -190,6 +309,23 @@ func TestSyncThirdPartyPaymentRatesUpdatesAllMethodsFromFetchedCurrencyRates(t *
}
}
func TestSyncThirdPartyPaymentRatesRejectsInvalidMarkupPercent(t *testing.T) {
wallet := &mockPaymentWallet{}
router := newPaymentHandlerTestRouter(New(wallet, nil, nil))
request := httptest.NewRequest(http.MethodPost, "/admin/payment/third-party-rates/sync", bytes.NewBufferString(`{"markupPercent":-1}`))
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
}
if wallet.lastThirdPartyChannels != nil {
t.Fatalf("invalid markup must fail before reading payment methods: %+v", wallet.lastThirdPartyChannels)
}
}
func TestCreateRechargeProductForwardsWebCoinSellerAudience(t *testing.T) {
wallet := &mockPaymentWallet{}
router := newPaymentHandlerTestRouter(New(wallet, nil, nil))
@ -227,7 +363,10 @@ func newPaymentHandlerTestRouter(handler *Handler) *gin.Engine {
c.Set(middleware.ContextUsername, "tester")
c.Next()
})
router.GET("/admin/payment/recharge-bills", handler.ListRechargeBills)
router.GET("/admin/payment/third-party-channels", handler.ListThirdPartyPaymentChannels)
router.GET("/admin/payment/temporary-links", handler.ListTemporaryPaymentLinks)
router.GET("/admin/payment/temporary-links/:order_id", handler.GetTemporaryPaymentLink)
router.PATCH("/admin/payment/third-party-methods/:method_id/status", handler.SetThirdPartyPaymentMethodStatus)
router.PATCH("/admin/payment/third-party-rates/:method_id", handler.UpdateThirdPartyPaymentRate)
router.POST("/admin/payment/third-party-rates/sync", handler.SyncThirdPartyPaymentRates)
@ -240,17 +379,50 @@ type adminPaymentTestResponse struct {
Data map[string]any `json:"data"`
}
func expectUserIdentityLookup(sqlMock sqlmock.Sqlmock, keyword string, userID int64) {
sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*LIMIT 1`).
WithArgs(
"lalu",
keyword,
keyword,
keyword,
sqlmock.AnyArg(),
keyword,
"%"+keyword+"%",
keyword,
keyword,
keyword,
sqlmock.AnyArg(),
keyword,
).
WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(userID))
}
type mockPaymentWallet struct {
walletclient.Client
lastRechargeBills *walletv1.ListRechargeBillsRequest
rechargeBillsResp *walletv1.ListRechargeBillsResponse
lastThirdPartyChannels *walletv1.ListThirdPartyPaymentChannelsRequest
thirdPartyChannelsResp *walletv1.ListThirdPartyPaymentChannelsResponse
lastTemporaryOrders *walletv1.ListTemporaryRechargeOrdersRequest
temporaryOrdersResp *walletv1.ListTemporaryRechargeOrdersResponse
lastTemporaryOrder *walletv1.GetTemporaryRechargeOrderRequest
temporaryOrderResp *walletv1.H5RechargeOrderResponse
lastSetMethodStatus *walletv1.SetThirdPartyPaymentMethodStatusRequest
lastUpdateRate *walletv1.UpdateThirdPartyPaymentRateRequest
updateRateRequests []*walletv1.UpdateThirdPartyPaymentRateRequest
lastCreateProduct *walletv1.CreateRechargeProductRequest
}
func (m *mockPaymentWallet) ListRechargeBills(_ context.Context, req *walletv1.ListRechargeBillsRequest) (*walletv1.ListRechargeBillsResponse, error) {
m.lastRechargeBills = req
if m.rechargeBillsResp != nil {
return m.rechargeBillsResp, nil
}
return &walletv1.ListRechargeBillsResponse{}, nil
}
func (m *mockPaymentWallet) ListThirdPartyPaymentChannels(_ context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error) {
m.lastThirdPartyChannels = req
if m.thirdPartyChannelsResp != nil {
@ -259,6 +431,22 @@ func (m *mockPaymentWallet) ListThirdPartyPaymentChannels(_ context.Context, req
return &walletv1.ListThirdPartyPaymentChannelsResponse{}, nil
}
func (m *mockPaymentWallet) ListTemporaryRechargeOrders(_ context.Context, req *walletv1.ListTemporaryRechargeOrdersRequest) (*walletv1.ListTemporaryRechargeOrdersResponse, error) {
m.lastTemporaryOrders = req
if m.temporaryOrdersResp != nil {
return m.temporaryOrdersResp, nil
}
return &walletv1.ListTemporaryRechargeOrdersResponse{}, nil
}
func (m *mockPaymentWallet) GetTemporaryRechargeOrder(_ context.Context, req *walletv1.GetTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) {
m.lastTemporaryOrder = req
if m.temporaryOrderResp != nil {
return m.temporaryOrderResp, nil
}
return &walletv1.H5RechargeOrderResponse{Order: &walletv1.ExternalRechargeOrder{OrderId: req.GetOrderId(), Status: "pending"}}, nil
}
func (m *mockPaymentWallet) SetThirdPartyPaymentMethodStatus(_ context.Context, req *walletv1.SetThirdPartyPaymentMethodStatusRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error) {
m.lastSetMethodStatus = req
status := "disabled"

View File

@ -13,6 +13,8 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.GET("/admin/payment/recharge-bills", middleware.RequirePermission("payment-bill:view"), h.ListRechargeBills)
protected.GET("/admin/payment/third-party-channels", middleware.RequirePermission("payment-third-party:view"), h.ListThirdPartyPaymentChannels)
protected.GET("/admin/payment/temporary-links", middleware.RequirePermission("payment-temporary-link:view"), h.ListTemporaryPaymentLinks)
protected.GET("/admin/payment/temporary-links/:order_id", middleware.RequirePermission("payment-temporary-link:view"), h.GetTemporaryPaymentLink)
protected.PATCH("/admin/payment/third-party-methods/:method_id/status", middleware.RequirePermission("payment-third-party:update"), h.SetThirdPartyPaymentMethodStatus)
protected.PATCH("/admin/payment/third-party-rates/:method_id", middleware.RequirePermission("payment-third-party:update"), h.UpdateThirdPartyPaymentRate)
protected.POST("/admin/payment/third-party-rates/sync", middleware.RequirePermission("payment-third-party:update"), h.SyncThirdPartyPaymentRates)

View File

@ -187,37 +187,12 @@ func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string) (int64
if keyword == "" {
return 0, false, true
}
if h.userDB == nil {
return 0, false, true
}
appCode := appctx.FromContext(ctx)
nowMs := time.Now().UnixMilli()
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs)
orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs)
args := append([]any{appCode}, matchArgs...)
args = append(args, orderArgs...)
rows, err := h.userDB.QueryContext(ctx, `
SELECT u.user_id
FROM users u
WHERE u.app_code = ? AND `+matchSQL+`
ORDER BY
`+orderSQL+`,
u.user_id DESC
LIMIT 1`,
args...,
)
// 注册奖励领取记录的 owner 是 activity-service列表关键词先解析成内部 user_id再保持下游分页和 total 同源。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil {
return 0, false, false
}
defer rows.Close()
if !rows.Next() {
return 0, false, rows.Err() == nil
}
var userID int64
if err := rows.Scan(&userID); err != nil {
return 0, false, false
}
return userID, true, rows.Err() == nil
return userID, matched, true
}
func (h *Handler) fillClaimUsers(ctx context.Context, claims []claimDTO) error {

View File

@ -139,6 +139,9 @@ type grantDTO struct {
Reason string `json:"reason"`
OperatorUserID int64 `json:"operatorUserId"`
Operator *operatorDTO `json:"operator,omitempty"`
RevokedAtMS int64 `json:"revokedAtMs"`
RevokedByUserID int64 `json:"revokedByUserId,string"`
RevokeReason string `json:"revokeReason"`
Items []grantItemDTO `json:"items"`
CreatedAtMS int64 `json:"createdAtMs"`
UpdatedAtMS int64 `json:"updatedAtMs"`
@ -436,6 +439,9 @@ func grantFromProto(grant *walletv1.ResourceGrant) grantDTO {
Status: grant.GetStatus(),
Reason: grant.GetReason(),
OperatorUserID: grant.GetOperatorUserId(),
RevokedAtMS: grant.GetRevokedAtMs(),
RevokedByUserID: grant.GetRevokedByUserId(),
RevokeReason: grant.GetRevokeReason(),
Items: items,
CreatedAtMS: grant.GetCreatedAtMs(),
UpdatedAtMS: grant.GetUpdatedAtMs(),

View File

@ -3,6 +3,7 @@ package resource
import (
"context"
"database/sql"
"errors"
"fmt"
"sort"
"strconv"
@ -601,6 +602,28 @@ func (h *Handler) GrantResourceGroup(c *gin.Context) {
response.Created(c, grant)
}
func (h *Handler) RevokeResourceGrant(c *gin.Context) {
grantID := strings.TrimSpace(c.Param("grant_id"))
if grantID == "" {
response.BadRequest(c, "资源赠送记录不存在")
return
}
resp, err := h.wallet.RevokeResourceGrant(c.Request.Context(), &walletv1.RevokeResourceGrantRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
GrantId: grantID,
Reason: "admin revoke resource grant",
OperatorUserId: actorID(c),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
grant := grantFromProto(resp.GetGrant())
h.auditLog(c, "revoke-resource-grant", "resource_grants", grant.GrantID, "success", fmt.Sprintf("target_user_id=%d", grant.TargetUserID))
response.OK(c, grant)
}
func (h *Handler) LookupResourceGrantTarget(c *gin.Context) {
keyword := strings.TrimSpace(firstQuery(c, "user_id", "userId", "keyword"))
if keyword == "" {
@ -642,13 +665,25 @@ func (h *Handler) LookupResourceGrantTarget(c *gin.Context) {
func (h *Handler) ListResourceGrants(c *gin.Context) {
options := shared.ListOptions(c)
targetUserID, ok := optionalInt64Query(c, "target_user_id", "targetUserId")
if !ok {
appCode := appctx.FromContext(c.Request.Context())
targetUserID, _, err := shared.ResolveExactUserIDOrNumericFallback(
c.Request.Context(),
h.userDB,
appCode,
firstQuery(c, "target_user_id", "targetUserId"),
time.Now().UnixMilli(),
)
if err != nil {
if errors.Is(err, shared.ErrInvalidUserIdentityFilter) {
response.BadRequest(c, "target_user_id 参数不正确")
return
}
response.ServerError(c, "查询赠送用户失败")
return
}
resp, err := h.wallet.ListResourceGrants(c.Request.Context(), &walletv1.ListResourceGrantsRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
AppCode: appCode,
TargetUserId: targetUserID,
Status: options.Status,
Page: int32(options.Page),
@ -704,18 +739,16 @@ func (h *Handler) ListResourceShopPurchaseOrders(c *gin.Context) {
}
userKeyword := strings.TrimSpace(firstQuery(c, "user_keyword", "userKeyword", "user"))
if userID == 0 && userKeyword != "" {
resolvedUserID, filtered, err := h.resolveResourceShopPurchaseUserID(c.Request.Context(), appCode, userKeyword)
resolvedUserID, matched, err := h.resolveResourceShopPurchaseUserID(c.Request.Context(), appCode, userKeyword)
if err != nil {
if err == sql.ErrNoRows {
response.OK(c, response.Page{Items: []resourceShopPurchaseOrderDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return
}
response.ServerError(c, "查询购买用户失败")
return
}
if filtered {
userID = resolvedUserID
if !matched {
response.OK(c, response.Page{Items: []resourceShopPurchaseOrderDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return
}
userID = resolvedUserID
}
ctx, cancel := h.walletRequestContext(c)
defer cancel()

View File

@ -14,6 +14,7 @@ import (
"hyapp-admin-server/internal/middleware"
walletv1 "hyapp.local/api/proto/wallet/v1"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
)
@ -135,6 +136,67 @@ func TestDeleteGiftOnlyCallsGiftConfigDelete(t *testing.T) {
}
}
func TestIdentityAutoGrantConfigRouteDoesNotHitResourceGroupID(t *testing.T) {
router := newResourceHandlerTestRouter(New(&mockResourceWallet{}, nil, nil, time.Second, nil))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/admin/resource-groups/identity-auto-grant-config", nil)
router.ServeHTTP(recorder, request)
// 这里故意使用 nil store让配置 handler 返回 500如果路由被 :group_id 抢走,会变成 400 的 ID 参数错误。
if recorder.Code != http.StatusInternalServerError {
t.Fatalf("identity config route should hit config handler, got %d %s", recorder.Code, recorder.Body.String())
}
if strings.Contains(recorder.Body.String(), "ID 参数不正确") {
t.Fatalf("identity config route should not be parsed as group_id: %s", recorder.Body.String())
}
}
func TestListResourceGrantsResolvesDisplayIDBeforeWalletFilter(t *testing.T) {
db, sqlMock, err := sqlmock.New()
if err != nil {
t.Fatalf("create sql mock failed: %v", err)
}
defer db.Close()
wallet := &mockResourceWallet{}
router := newResourceHandlerTestRouter(New(wallet, nil, db, time.Second, nil))
resolvedUserID := int64(327702592329093120)
sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*LIMIT 1`).
WithArgs(
"lalu",
"111",
"111",
"111",
sqlmock.AnyArg(),
"111",
"%111%",
"111",
"111",
"111",
sqlmock.AnyArg(),
"111",
).
WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(resolvedUserID))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/admin/resource-grants?target_user_id=111", nil)
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("list grants status mismatch: %d %s", recorder.Code, recorder.Body.String())
}
if len(wallet.listGrantRequests) != 1 {
t.Fatalf("expected one wallet list request, got %d", len(wallet.listGrantRequests))
}
if got := wallet.listGrantRequests[0].GetTargetUserId(); got != resolvedUserID {
t.Fatalf("target display id should be resolved before wallet filter: got %d want %d", got, resolvedUserID)
}
if err := sqlMock.ExpectationsWereMet(); err != nil {
t.Fatalf("sql expectations mismatch: %v", err)
}
}
func newResourceHandlerTestRouter(handler *Handler) *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
@ -143,18 +205,39 @@ func newResourceHandlerTestRouter(handler *Handler) *gin.Engine {
c.Set(middleware.ContextRequestID, "resource-handler-test")
c.Set(middleware.ContextUserID, uint(7))
c.Set(middleware.ContextUsername, "tester")
// 测试路由走真实 RegisterRoutes权限一次性放开避免中间件短路导致无法覆盖路由优先级。
c.Set(middleware.ContextPermissions, []string{
"emoji-pack:create",
"emoji-pack:view",
"gift:delete",
"gift:view",
"gift:update",
"gift:create",
"gift:status",
"resource:create",
"resource-grant:create",
"resource-grant:revoke",
"resource-grant:view",
"resource-group:create",
"resource-group:update",
"resource-group:view",
"resource-shop:update",
"resource-shop:view",
"resource:update",
"resource:view",
})
c.Next()
})
router.PUT("/admin/resources/mp4-layouts/batch", handler.UpdateMP4ResourceLayouts)
router.DELETE("/admin/gifts/:gift_id", handler.DeleteGift)
RegisterRoutes(router.Group(""), handler)
return router
}
type mockResourceWallet struct {
walletclient.Client
resources map[int64]*walletv1.Resource
updates []*walletv1.UpdateResourceRequest
deletedGifts []*walletv1.DeleteGiftConfigRequest
resources map[int64]*walletv1.Resource
updates []*walletv1.UpdateResourceRequest
deletedGifts []*walletv1.DeleteGiftConfigRequest
listGrantRequests []*walletv1.ListResourceGrantsRequest
}
func (m *mockResourceWallet) GetResource(ctx context.Context, req *walletv1.GetResourceRequest) (*walletv1.GetResourceResponse, error) {
@ -200,3 +283,10 @@ func (m *mockResourceWallet) DeleteGiftConfig(ctx context.Context, req *walletv1
Name: "Rose",
}}, nil
}
func (m *mockResourceWallet) ListResourceGrants(ctx context.Context, req *walletv1.ListResourceGrantsRequest) (*walletv1.ListResourceGrantsResponse, error) {
// 列表过滤的修正点发生在 admin-server 调 wallet 之前;测试只需要捕获请求形状,
// 返回空页即可避免后续用户资料补全查询干扰断言。
m.listGrantRequests = append(m.listGrantRequests, req)
return &walletv1.ListResourceGrantsResponse{Grants: []*walletv1.ResourceGrant{}, Total: 0}, nil
}

View File

@ -0,0 +1,304 @@
package resource
import (
"encoding/json"
"errors"
"fmt"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/response"
walletv1 "hyapp.local/api/proto/wallet/v1"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
const resourceIdentityAutoGrantConfigGroup = "resource-identity-auto-grant-config"
var resourceIdentityAutoGrantTypes = []string{"host", "bd", "bd_leader", "agency", "manager"}
type resourceIdentityAutoGrantConfigDTO struct {
Items []resourceIdentityAutoGrantItemDTO `json:"items"`
UpdatedByAdminID int64 `json:"updatedByAdminId"`
CreatedAtMS int64 `json:"createdAtMs"`
UpdatedAtMS int64 `json:"updatedAtMs"`
}
type resourceIdentityAutoGrantItemDTO struct {
IdentityType string `json:"identityType"`
ResourceGroupID int64 `json:"resourceGroupId"`
ResourceGroup *resourceGroupDTO `json:"resourceGroup,omitempty"`
}
type resourceIdentityAutoGrantConfigValue struct {
Items []resourceIdentityAutoGrantStoredItem `json:"items"`
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
}
type resourceIdentityAutoGrantStoredItem struct {
IdentityType string `json:"identity_type"`
ResourceGroupID int64 `json:"resource_group_id"`
}
type resourceIdentityAutoGrantConfigRequest struct {
Items []resourceIdentityAutoGrantItemRequest `json:"items"`
}
type resourceIdentityAutoGrantItemRequest struct {
IdentityType string `json:"identity_type"`
IdentityTypeCamel string `json:"identityType"`
ResourceGroupID int64 `json:"resource_group_id"`
ResourceGroupIDJS int64 `json:"resourceGroupId"`
}
func (h *Handler) GetResourceIdentityAutoGrantConfig(c *gin.Context) {
if h.store == nil {
response.ServerError(c, "后台配置存储未初始化")
return
}
// 身份资源组属于 admin 后台低频配置,不进入 wallet 协议;读取时从 admin_app_configs 取当前 App 的快照。
config, err := h.loadIdentityAutoGrantConfig(c)
if err != nil {
response.ServerError(c, "加载身份资源组配置失败")
return
}
response.OK(c, config)
}
func (h *Handler) UpdateResourceIdentityAutoGrantConfig(c *gin.Context) {
if h.store == nil {
response.ServerError(c, "后台配置存储未初始化")
return
}
var req resourceIdentityAutoGrantConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "身份资源组配置参数不正确")
return
}
// 前端保存的是整份配置;后端也要求 5 个身份一次性提交,避免半包请求把未提交身份误清空。
items, err := normalizeIdentityAutoGrantItems(req.Items)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// resource_group_id=0 表示不自动发放;大于 0 必须先确认 wallet 中存在,防止保存悬空资源组。
if err := h.validateIdentityAutoGrantResourceGroups(c, items); err != nil {
response.BadRequest(c, err.Error())
return
}
payload, err := json.Marshal(resourceIdentityAutoGrantConfigValue{
Items: items,
UpdatedByAdminID: actorID(c),
})
if err != nil {
response.ServerError(c, "保存身份资源组配置失败")
return
}
appCode := appctx.FromContext(c.Request.Context())
// 配置按 App 隔离Key 使用中间件解析出的 app_code禁止请求体自行覆盖作用域。
item := model.AppConfig{
Group: resourceIdentityAutoGrantConfigGroup,
Key: appCode,
Value: string(payload),
Description: "用户身份自动发放资源组配置",
}
if err := h.store.UpsertAppConfigs([]model.AppConfig{item}); err != nil {
response.ServerError(c, "保存身份资源组配置失败")
return
}
// 保存后重新读取并补资源组详情,保证返回值和 GET 接口结构一致,前端无需自己拼保存后的展示数据。
config, err := h.loadIdentityAutoGrantConfig(c)
if err != nil {
response.ServerError(c, "加载身份资源组配置失败")
return
}
h.auditLog(c, "update-identity-auto-grant-config", "resource_identity_auto_grant_configs", appCode, "success", identityAutoGrantAuditDetail(items))
response.OK(c, config)
}
func (h *Handler) loadIdentityAutoGrantConfig(c *gin.Context) (resourceIdentityAutoGrantConfigDTO, error) {
appCode := appctx.FromContext(c.Request.Context())
item, err := h.store.GetAppConfig(resourceIdentityAutoGrantConfigGroup, appCode)
if errors.Is(err, gorm.ErrRecordNotFound) {
// 首次进入页面没有配置行时返回完整身份集合,前端可以直接展示空配置而不是走错误态。
return resourceIdentityAutoGrantConfigDTO{Items: defaultIdentityAutoGrantItems()}, nil
}
if err != nil {
return resourceIdentityAutoGrantConfigDTO{}, err
}
config, err := identityAutoGrantConfigFromValue(item.Value)
if err != nil {
return resourceIdentityAutoGrantConfigDTO{}, err
}
config.CreatedAtMS = item.CreatedAtMS
config.UpdatedAtMS = item.UpdatedAtMS
// 资源组详情只用于展示已选项名称;补全失败不应该让整个配置页打不开。
h.fillIdentityAutoGrantResourceGroups(c, config.Items)
return config, nil
}
func identityAutoGrantConfigFromValue(raw string) (resourceIdentityAutoGrantConfigDTO, error) {
var value resourceIdentityAutoGrantConfigValue
if strings.TrimSpace(raw) != "" {
if err := json.Unmarshal([]byte(raw), &value); err != nil {
return resourceIdentityAutoGrantConfigDTO{}, err
}
}
itemsByType := make(map[string]resourceIdentityAutoGrantStoredItem, len(value.Items))
for _, item := range value.Items {
identityType := strings.TrimSpace(item.IdentityType)
if isIdentityAutoGrantType(identityType) && item.ResourceGroupID >= 0 {
// 读取历史配置时按当前支持的身份集合过滤,旧脏数据不继续扩散到前端表单。
itemsByType[identityType] = resourceIdentityAutoGrantStoredItem{IdentityType: identityType, ResourceGroupID: item.ResourceGroupID}
}
}
items := make([]resourceIdentityAutoGrantItemDTO, 0, len(resourceIdentityAutoGrantTypes))
for _, identityType := range resourceIdentityAutoGrantTypes {
// 输出顺序固定,避免前端每次打开弹窗时身份行抖动。
item := itemsByType[identityType]
items = append(items, resourceIdentityAutoGrantItemDTO{
IdentityType: identityType,
ResourceGroupID: item.ResourceGroupID,
})
}
return resourceIdentityAutoGrantConfigDTO{Items: items, UpdatedByAdminID: value.UpdatedByAdminID}, nil
}
func defaultIdentityAutoGrantItems() []resourceIdentityAutoGrantItemDTO {
items := make([]resourceIdentityAutoGrantItemDTO, 0, len(resourceIdentityAutoGrantTypes))
for _, identityType := range resourceIdentityAutoGrantTypes {
items = append(items, resourceIdentityAutoGrantItemDTO{IdentityType: identityType})
}
return items
}
func normalizeIdentityAutoGrantItems(items []resourceIdentityAutoGrantItemRequest) ([]resourceIdentityAutoGrantStoredItem, error) {
if len(items) != len(resourceIdentityAutoGrantTypes) {
return nil, fmt.Errorf("必须配置所有身份")
}
itemByType := make(map[string]resourceIdentityAutoGrantStoredItem, len(items))
for _, item := range items {
// 当前前端提交 snake_case这里兼容 camelCase避免旧构建或调试脚本因为字段名差异无法保存。
identityType := strings.TrimSpace(firstNonEmpty(item.IdentityType, item.IdentityTypeCamel))
if !isIdentityAutoGrantType(identityType) {
return nil, fmt.Errorf("身份参数不正确")
}
if _, exists := itemByType[identityType]; exists {
return nil, fmt.Errorf("身份不能重复配置")
}
// 0 是显式关闭自动发放,所以只拒绝负数;正数存在性在后续 wallet 查询里验证。
resourceGroupID := item.ResourceGroupID
if resourceGroupID == 0 {
resourceGroupID = item.ResourceGroupIDJS
}
if resourceGroupID < 0 {
return nil, fmt.Errorf("资源组必须是有效资源组")
}
itemByType[identityType] = resourceIdentityAutoGrantStoredItem{
IdentityType: identityType,
ResourceGroupID: resourceGroupID,
}
}
normalized := make([]resourceIdentityAutoGrantStoredItem, 0, len(resourceIdentityAutoGrantTypes))
for _, identityType := range resourceIdentityAutoGrantTypes {
// 按白名单顺序重建数组,存储层不依赖前端传入顺序。
item, ok := itemByType[identityType]
if !ok {
return nil, fmt.Errorf("必须配置所有身份")
}
normalized = append(normalized, item)
}
return normalized, nil
}
func (h *Handler) validateIdentityAutoGrantResourceGroups(c *gin.Context, items []resourceIdentityAutoGrantStoredItem) error {
if h.wallet == nil {
for _, item := range items {
if item.ResourceGroupID > 0 {
// 没有 wallet client 时只能接受全关闭配置,避免把未校验的正数资源组写入运行配置。
return fmt.Errorf("资源组服务未初始化")
}
}
return nil
}
checked := make(map[int64]struct{}, len(items))
ctx, cancel := h.walletRequestContext(c)
defer cancel()
for _, item := range items {
if item.ResourceGroupID == 0 {
continue
}
if _, ok := checked[item.ResourceGroupID]; ok {
continue
}
checked[item.ResourceGroupID] = struct{}{}
// 保存前只校验唯一资源组 ID一份配置里多个身份共用同组时不重复打 wallet。
_, err := h.wallet.GetResourceGroup(ctx, &walletv1.GetResourceGroupRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
GroupId: item.ResourceGroupID,
})
if err != nil {
return fmt.Errorf("资源组 %d 不存在或不可用", item.ResourceGroupID)
}
}
return nil
}
func (h *Handler) fillIdentityAutoGrantResourceGroups(c *gin.Context, items []resourceIdentityAutoGrantItemDTO) {
if h.wallet == nil {
return
}
groupByID := make(map[int64]*resourceGroupDTO, len(items))
ctx, cancel := h.walletRequestContext(c)
defer cancel()
for index := range items {
groupID := items[index].ResourceGroupID
if groupID == 0 {
continue
}
if group, ok := groupByID[groupID]; ok {
items[index].ResourceGroup = group
continue
}
// 已保存配置可能指向被禁用或被删除的资源组;详情查询失败时保留 ID让前端至少能展示 #ID 并允许重新选择。
resp, err := h.wallet.GetResourceGroup(ctx, &walletv1.GetResourceGroupRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
GroupId: groupID,
})
if err != nil {
continue
}
group := resourceGroupFromProto(resp.GetGroup())
groupByID[groupID] = &group
items[index].ResourceGroup = &group
}
}
func isIdentityAutoGrantType(value string) bool {
for _, candidate := range resourceIdentityAutoGrantTypes {
if value == candidate {
return true
}
}
return false
}
func identityAutoGrantAuditDetail(items []resourceIdentityAutoGrantStoredItem) string {
parts := make([]string, 0, len(items))
for _, item := range items {
parts = append(parts, fmt.Sprintf("%s=%d", item.IdentityType, item.ResourceGroupID))
}
return strings.Join(parts, ",")
}

View File

@ -2,7 +2,6 @@ package resource
import (
"context"
"fmt"
"strings"
"time"
@ -163,29 +162,11 @@ func (h *Handler) resolveResourceShopPurchaseUserID(ctx context.Context, appCode
if keyword == "" {
return 0, false, nil
}
if h == nil || h.userDB == nil {
return 0, false, fmt.Errorf("user mysql is not configured")
if h == nil {
return 0, false, nil
}
nowMs := time.Now().UnixMilli()
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs)
orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs)
args := append([]any{appCode}, matchArgs...)
args = append(args, orderArgs...)
row := h.userDB.QueryRowContext(ctx, `
SELECT u.user_id
FROM users u
WHERE u.app_code = ? AND `+matchSQL+`
ORDER BY
`+orderSQL+`,
u.user_id DESC
LIMIT 1`,
args...,
)
var userID int64
if err := row.Scan(&userID); err != nil {
return 0, true, err
}
return userID, true, nil
// 资源商店购买记录同样由 wallet-service 按内部 user_id 过滤;后台输入允许短号、靓号和昵称,先解析再下传。
return shared.ResolveExactUserID(ctx, h.userDB, appCode, keyword, time.Now().UnixMilli())
}
func (h *Handler) enrichGrantOperators(ctx context.Context, grants []grantDTO) error {

View File

@ -25,6 +25,8 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.GET("/admin/resource-groups", middleware.RequirePermission("resource-group:view"), h.ListResourceGroups)
protected.POST("/admin/resource-groups", middleware.RequirePermission("resource-group:create"), h.CreateResourceGroup)
protected.GET("/admin/resource-groups/identity-auto-grant-config", middleware.RequirePermission("resource-group:view"), h.GetResourceIdentityAutoGrantConfig)
protected.PUT("/admin/resource-groups/identity-auto-grant-config", middleware.RequirePermission("resource-group:update"), h.UpdateResourceIdentityAutoGrantConfig)
protected.GET("/admin/resource-groups/:group_id", middleware.RequirePermission("resource-group:view"), h.GetResourceGroup)
protected.PUT("/admin/resource-groups/:group_id", middleware.RequirePermission("resource-group:update"), h.UpdateResourceGroup)
protected.PUT("/admin/resource-groups/:group_id/items", middleware.RequirePermission("resource-group:update"), h.UpdateResourceGroup)
@ -43,6 +45,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.POST("/admin/resource-grants/resource", middleware.RequirePermission("resource-grant:create"), h.GrantResource)
protected.POST("/admin/resource-grants/group", middleware.RequirePermission("resource-grant:create"), h.GrantResourceGroup)
protected.POST("/admin/resource-grants/:grant_id/revoke", middleware.RequirePermission("resource-grant:revoke"), h.RevokeResourceGrant)
protected.GET("/admin/resource-grants/target", middleware.RequirePermission("resource-grant:create"), h.LookupResourceGrantTarget)
protected.GET("/admin/resource-grants", middleware.RequirePermission("resource-grant:view"), h.ListResourceGrants)

View File

@ -0,0 +1,61 @@
package riskconfig
import (
"fmt"
"hyapp-admin-server/internal/integration/userclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
"github.com/gin-gonic/gin"
)
type Handler struct {
user userclient.Client
audit shared.OperationLogger
}
func New(user userclient.Client, audit shared.OperationLogger) *Handler {
return &Handler{user: user, audit: audit}
}
type registerRiskConfigRequest struct {
MaxAccountsPerDevice int32 `json:"max_accounts_per_device"`
}
func (h *Handler) GetRegisterRiskConfig(c *gin.Context) {
config, err := h.user.GetRegisterRiskConfig(c.Request.Context(), userclient.GetRegisterRiskConfigRequest{
RequestID: middleware.CurrentRequestID(c),
Caller: "admin-server",
})
if err != nil {
response.ServerError(c, "获取风控配置失败")
return
}
response.OK(c, config)
}
func (h *Handler) UpdateRegisterRiskConfig(c *gin.Context) {
var req registerRiskConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "风控配置参数不正确")
return
}
if req.MaxAccountsPerDevice < 1 {
response.BadRequest(c, "单设备最大账号数必须大于 0")
return
}
config, err := h.user.UpdateRegisterRiskConfig(c.Request.Context(), userclient.UpdateRegisterRiskConfigRequest{
RequestID: middleware.CurrentRequestID(c),
Caller: "admin-server",
MaxAccountsPerDevice: req.MaxAccountsPerDevice,
OperatorAdminID: int64(middleware.CurrentUserID(c)),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
shared.OperationLog(c, h.audit, "update-risk-config", "auth_risk_configs", "success", fmt.Sprintf("max_accounts_per_device=%d", config.MaxAccountsPerDevice))
response.OK(c, config)
}

View File

@ -0,0 +1,16 @@
package riskconfig
import (
"hyapp-admin-server/internal/middleware"
"github.com/gin-gonic/gin"
)
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
if h == nil {
return
}
protected.GET("/admin/users/risk-config", middleware.RequirePermission("risk-config:view"), h.GetRegisterRiskConfig)
protected.PUT("/admin/users/risk-config", middleware.RequirePermission("risk-config:update"), h.UpdateRegisterRiskConfig)
}

View File

@ -202,41 +202,16 @@ func (h *Handler) resolveOwnerUserID(ctx context.Context, keyword string) (int64
return 0, false, true
}
if h.userDB == nil {
// userDB 缺失时只允许长 ID 精确筛选,避免把短号或昵称误当成 settlement.owner_user_id 查询。
if parsed, err := strconv.ParseInt(keyword, 10, 64); err == nil && parsed > 0 {
return parsed, true, true
}
return 0, false, true
// userDB 缺失时只允许正整数长 ID 精确筛选,避免把短号或昵称误当成 settlement.owner_user_id 查询。
userID, matched, err := shared.ResolveExactUserIDOrNumericFallback(ctx, nil, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
return userID, matched, err == nil
}
appCode := appctx.FromContext(ctx)
nowMs := time.Now().UnixMilli()
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs)
orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs)
args := append([]any{appCode}, matchArgs...)
args = append(args, orderArgs...)
// 表头筛选先把运营输入解析成唯一房主 user_id再交给 activity-service 的 owner_user_id 条件,保证分页 total 和列表数据同源。
rows, err := h.userDB.QueryContext(ctx, `
SELECT u.user_id
FROM users u
WHERE u.app_code = ? AND `+matchSQL+`
ORDER BY
`+orderSQL+`,
u.user_id DESC
LIMIT 1`,
args...,
)
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil {
return 0, false, false
}
defer rows.Close()
if !rows.Next() {
return 0, false, rows.Err() == nil
}
var userID int64
if err := rows.Scan(&userID); err != nil {
return 0, false, false
}
return userID, true, rows.Err() == nil
return userID, matched, true
}
func (h *Handler) fillOwnerUsers(ctx context.Context, settlements []settlementDTO) error {

View File

@ -163,37 +163,12 @@ func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string) (int64
if keyword == "" {
return 0, false, true
}
if h.userDB == nil {
return 0, false, true
}
appCode := appctx.FromContext(ctx)
nowMs := time.Now().UnixMilli()
matchSQL, matchArgs := shared.UserIdentityExactSQL("u", "u.user_id", keyword, nowMs)
orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs)
args := append([]any{appCode}, matchArgs...)
args = append(args, orderArgs...)
rows, err := h.userDB.QueryContext(ctx, `
SELECT u.user_id
FROM users u
WHERE u.app_code = ? AND `+matchSQL+`
ORDER BY
`+orderSQL+`,
u.user_id DESC
LIMIT 1`,
args...,
)
// 签到领取记录按 activity-service 的内部 user_id 查;短号、靓号和昵称先在 admin 侧解析,避免下游误用展示 ID。
userID, matched, err := shared.ResolveExactUserID(ctx, h.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
if err != nil {
return 0, false, false
}
defer rows.Close()
if !rows.Next() {
return 0, false, rows.Err() == nil
}
var userID int64
if err := rows.Scan(&userID); err != nil {
return 0, false, false
}
return userID, true, rows.Err() == nil
return userID, matched, true
}
func (h *Handler) fillClaimUsers(ctx context.Context, claims []claimDTO) error {

View File

@ -0,0 +1,63 @@
package shared
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
)
var ErrInvalidUserIdentityFilter = errors.New("invalid user identity filter")
// ResolveExactUserID 把后台输入框里的用户身份解析成 owner service 使用的内部 user_id。
// 查询条件复用统一身份 SQL精确长 ID、当前展示号、默认短号、active 靓号优先,昵称只做最后的模糊兜底。
func ResolveExactUserID(ctx context.Context, db *sql.DB, appCode string, keyword string, nowMs int64) (int64, bool, error) {
keyword = strings.TrimSpace(keyword)
if keyword == "" || db == nil {
return 0, false, nil
}
matchSQL, matchArgs := UserIdentityExactSQL("u", "u.user_id", keyword, nowMs)
orderSQL, orderArgs := UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMs)
args := append([]any{appCode}, matchArgs...)
args = append(args, orderArgs...)
row := db.QueryRowContext(ctx, `
SELECT u.user_id
FROM users u
WHERE u.app_code = ? AND `+matchSQL+`
ORDER BY
`+orderSQL+`,
u.user_id DESC
LIMIT 1`,
args...,
)
var userID int64
if err := row.Scan(&userID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, false, nil
}
return 0, false, err
}
return userID, true, nil
}
// ResolveExactUserIDOrNumericFallback 用于列表 owner 在 wallet/activity 等下游服务的场景。
// 下游只认内部 user_id后台先按短号/靓号解析;解析不到时只接受正整数长 ID兼容用户资料缺失的历史事实记录。
func ResolveExactUserIDOrNumericFallback(ctx context.Context, db *sql.DB, appCode string, keyword string, nowMs int64) (int64, bool, error) {
keyword = strings.TrimSpace(keyword)
if keyword == "" {
return 0, false, nil
}
if db != nil {
userID, matched, err := ResolveExactUserID(ctx, db, appCode, keyword, nowMs)
if err != nil || matched {
return userID, matched, err
}
}
userID, err := strconv.ParseInt(keyword, 10, 64)
if err != nil || userID <= 0 {
return 0, true, fmt.Errorf("%w: %s", ErrInvalidUserIdentityFilter, keyword)
}
return userID, true, nil
}

View File

@ -26,6 +26,8 @@ var defaultPermissions = []model.Permission{
{Name: "靓号池更新", Code: "pretty-id:update", Kind: "button"},
{Name: "靓号批量生成", Code: "pretty-id:generate", Kind: "button"},
{Name: "靓号后台发放", Code: "pretty-id:grant", Kind: "button"},
{Name: "风控配置查看", Code: "risk-config:view", Kind: "menu"},
{Name: "风控配置更新", Code: "risk-config:update", Kind: "button"},
{Name: "地区屏蔽查看", Code: "region-block:view", Kind: "menu"},
{Name: "地区屏蔽更新", Code: "region-block:update", Kind: "button"},
{Name: "房间查看", Code: "room:view", Kind: "menu"},
@ -57,6 +59,7 @@ var defaultPermissions = []model.Permission{
{Name: "资源组更新", Code: "resource-group:update", Kind: "button"},
{Name: "资源赠送查看", Code: "resource-grant:view", Kind: "menu"},
{Name: "资源赠送创建", Code: "resource-grant:create", Kind: "button"},
{Name: "资源赠送撤销", Code: "resource-grant:revoke", Kind: "button"},
{Name: "礼物查看", Code: "gift:view", Kind: "menu"},
{Name: "礼物创建", Code: "gift:create", Kind: "button"},
{Name: "礼物更新", Code: "gift:update", Kind: "button"},
@ -108,6 +111,7 @@ var defaultPermissions = []model.Permission{
{Name: "支付账单查看", Code: "payment-bill:view", Kind: "menu"},
{Name: "三方支付查看", Code: "payment-third-party:view", Kind: "menu"},
{Name: "三方支付更新", Code: "payment-third-party:update", Kind: "button"},
{Name: "三方临时支付链接查看", Code: "payment-temporary-link:view", Kind: "menu"},
{Name: "内购配置查看", Code: "payment-product:view", Kind: "menu"},
{Name: "内购配置创建", Code: "payment-product:create", Kind: "button"},
{Name: "内购配置更新", Code: "payment-product:update", Kind: "button"},
@ -139,6 +143,8 @@ var defaultPermissions = []model.Permission{
{Name: "红包配置更新", Code: "red-packet:update", Kind: "button"},
{Name: "CP 配置查看", Code: "cp-config:view", Kind: "menu"},
{Name: "CP 配置更新", Code: "cp-config:update", Kind: "button"},
{Name: "CP 排行活动查看", Code: "cp-weekly-rank:view", Kind: "menu"},
{Name: "CP 排行活动更新", Code: "cp-weekly-rank:update", Kind: "button"},
{Name: "VIP 配置查看", Code: "vip-config:view", Kind: "menu"},
{Name: "VIP 配置更新", Code: "vip-config:update", Kind: "button"},
{Name: "VIP 赠送", Code: "vip-config:grant", Kind: "button"},
@ -263,7 +269,8 @@ func (s *Store) seedMenus() error {
{ParentID: &appUsersID, Title: "登录日志", Code: "app-user-login-logs", Path: "/app/users/login-logs", Icon: "login", PermissionCode: "app-user:view", Sort: 62, Visible: true},
{ParentID: &appUsersID, Title: "等级配置", Code: "app-user-level-config", Path: "/app/users/level-config", Icon: "military_tech", PermissionCode: "level-config:view", Sort: 63, Visible: true},
{ParentID: &appUsersID, Title: "靓号管理", Code: "app-user-pretty-ids", Path: "/app/users/pretty-ids", Icon: "tag", PermissionCode: "pretty-id:view", Sort: 64, Visible: true},
{ParentID: &appUsersID, Title: "地区屏蔽", Code: "app-user-region-blocks", Path: "/app/users/region-blocks", Icon: "public", PermissionCode: "region-block:view", Sort: 65, Visible: true},
{ParentID: &appUsersID, Title: "风控管理", Code: "app-user-risk-config", Path: "/app/users/risk-config", Icon: "shield", PermissionCode: "risk-config:view", Sort: 65, Visible: true},
{ParentID: &appUsersID, Title: "地区屏蔽", Code: "app-user-region-blocks", Path: "/app/users/region-blocks", Icon: "public", PermissionCode: "region-block:view", Sort: 66, Visible: true},
{ParentID: &roomsID, Title: "房间列表", Code: "room-list", Path: "/rooms", Icon: "room", PermissionCode: "room:view", Sort: 65, Visible: true},
{ParentID: &roomsID, Title: "房间置顶", Code: "room-pins", Path: "/rooms/pins", Icon: "push_pin", PermissionCode: "room-pin:view", Sort: 66, Visible: true},
{ParentID: &roomsID, Title: "房间配置", Code: "room-config", Path: "/rooms/config", Icon: "settings", PermissionCode: "room-config:view", Sort: 67, Visible: true},
@ -290,7 +297,8 @@ func (s *Store) seedMenus() error {
{ParentID: &operationsID, Title: "全服通知", Code: "operation-full-server-notice", Path: "/operations/full-server-notices", Icon: "campaign", PermissionCode: "full-server-notice:view", Sort: 75, Visible: true},
{ParentID: &paymentID, Title: "账单列表", Code: "payment-bill-list", Path: "/payment/bills", Icon: "receipt", PermissionCode: "payment-bill:view", Sort: 68, Visible: true},
{ParentID: &paymentID, Title: "三方支付", Code: "payment-third-party", Path: "/payment/third-party", Icon: "wallet", PermissionCode: "payment-third-party:view", Sort: 69, Visible: true},
{ParentID: &paymentID, Title: "支付内购商品", Code: "payment-recharge-products", Path: "/payment/recharge-products", Icon: "wallet", PermissionCode: "payment-product: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: &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: "achievement-config", Path: "/activities/achievements", Icon: "military_tech", PermissionCode: "achievement:view", Sort: 71, Visible: true},
@ -299,10 +307,11 @@ func (s *Store) seedMenus() error {
{ParentID: &activityID, Title: "用户榜单", Code: "user-leaderboard", Path: "/activities/user-leaderboards", Icon: "leaderboard", PermissionCode: "user-leaderboard:view", Sort: 74, Visible: true},
{ParentID: &activityID, Title: "红包配置", Code: "red-packet", Path: "/activities/red-packets", Icon: "redeem", PermissionCode: "red-packet:view", Sort: 75, Visible: true},
{ParentID: &activityID, Title: "CP配置", Code: "cp-config", Path: "/activities/cp-config", Icon: "favorite", PermissionCode: "cp-config:view", Sort: 76, Visible: true},
{ParentID: &activityID, Title: "VIP配置", Code: "vip-config", Path: "/activities/vip-config", Icon: "workspace_premium", PermissionCode: "vip-config:view", Sort: 77, Visible: true},
{ParentID: &activityID, Title: "周星配置", Code: "weekly-star", Path: "/activities/weekly-star", Icon: "star", PermissionCode: "weekly-star:view", Sort: 78, Visible: true},
{ParentID: &activityID, Title: "代理开业活动", Code: "agency-opening", Path: "/activities/agency-opening", Icon: "workspace_premium", PermissionCode: "agency-opening:view", Sort: 79, Visible: true},
{ParentID: &activityID, Title: "转盘抽奖", Code: "wheel", Path: "/activities/wheel", Icon: "casino", PermissionCode: "wheel:view", Sort: 80, Visible: true},
{ParentID: &activityID, Title: "CP排行活动", Code: "cp-weekly-rank", Path: "/activities/cp-weekly-rank", Icon: "leaderboard", PermissionCode: "cp-weekly-rank:view", Sort: 77, Visible: true},
{ParentID: &activityID, Title: "VIP配置", Code: "vip-config", Path: "/activities/vip-config", Icon: "workspace_premium", PermissionCode: "vip-config:view", Sort: 78, Visible: true},
{ParentID: &activityID, Title: "周星配置", Code: "weekly-star", Path: "/activities/weekly-star", Icon: "star", PermissionCode: "weekly-star:view", Sort: 79, Visible: true},
{ParentID: &activityID, Title: "代理开业活动", Code: "agency-opening", Path: "/activities/agency-opening", Icon: "workspace_premium", PermissionCode: "agency-opening:view", Sort: 80, Visible: true},
{ParentID: &activityID, Title: "转盘抽奖", Code: "wheel", Path: "/activities/wheel", Icon: "casino", PermissionCode: "wheel:view", Sort: 81, Visible: true},
{ParentID: &gameID, Title: "游戏列表", Code: "game-list", Path: "/games", Icon: "sports_esports", PermissionCode: "game:view", Sort: 70, Visible: true},
{ParentID: &gameID, Title: "自研游戏", Code: "self-games", Path: "/games/self-games", Icon: "settings", PermissionCode: "game:view", Sort: 80, Visible: true},
{ParentID: &gameID, Title: "全站机器人", Code: "game-robots", Path: "/games/robots", Icon: "team", PermissionCode: "game:view", Sort: 90, Visible: true},
@ -527,6 +536,7 @@ func defaultRolePermissionCodes(code string) []string {
"app-user:view", "app-user:update", "app-user:status", "app-user:password",
"level-config:view", "level-config:update",
"pretty-id:view", "pretty-id:update", "pretty-id:generate", "pretty-id:grant",
"risk-config:view", "risk-config:update",
"region-block:view", "region-block:update",
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update", "room-whitelist:view", "room-whitelist:update", "room-robot:view", "room-robot:create", "room-robot:update",
"app-config:view", "app-config:update",
@ -534,7 +544,7 @@ func defaultRolePermissionCodes(code string) []string {
"resource:view", "resource:create", "resource:update",
"resource-shop:view", "resource-shop:update",
"resource-group:view", "resource-group:create", "resource-group:update",
"resource-grant:view", "resource-grant:create",
"resource-grant:view", "resource-grant:create", "resource-grant:revoke",
"gift:view", "gift:create", "gift:update", "gift:status", "gift:delete",
"emoji-pack:view", "emoji-pack:create",
"country:view", "country:create", "country:update", "country:status",
@ -543,7 +553,7 @@ func defaultRolePermissionCodes(code string) []string {
"agency:view", "agency:create", "agency:status", "agency:delete",
"bd:view", "bd:create", "bd:update",
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "coin-seller:exchange-rate",
"coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "full-server-notice:view", "full-server-notice:send", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
"coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "full-server-notice:view", "full-server-notice:send", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-temporary-link:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
"lucky-gift:view", "lucky-gift:update", "wheel:view", "wheel:update",
"game:view", "game:create", "game:update", "game:status", "game:delete",
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
@ -551,7 +561,7 @@ func defaultRolePermissionCodes(code string) []string {
"seven-day-checkin:view", "seven-day-checkin:update",
"room-rocket:view", "room-rocket:update",
"red-packet:view", "red-packet:update",
"cp-config:view", "cp-config:update",
"cp-config:view", "cp-config:update", "cp-weekly-rank:view", "cp-weekly-rank:update",
"vip-config:view", "vip-config:update", "vip-config:grant",
"weekly-star:view", "weekly-star:create", "weekly-star:update", "weekly-star:settle",
"agency-opening:view", "agency-opening:create", "agency-opening:update",
@ -560,7 +570,7 @@ func defaultRolePermissionCodes(code string) []string {
"upload:create",
}
case "auditor":
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "pretty-id:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-whitelist:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "full-server-notice:view", "lucky-gift:view", "wheel:view", "payment-bill:view", "payment-third-party:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "vip-config:view", "weekly-star:view", "agency-opening:view", "role:view", "permission:view", "job:view"}
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "pretty-id:view", "risk-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-whitelist:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "full-server-notice:view", "lucky-gift:view", "wheel:view", "payment-bill:view", "payment-third-party:view", "payment-temporary-link:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "cp-weekly-rank:view", "vip-config:view", "weekly-star:view", "agency-opening:view", "role:view", "permission:view", "job:view"}
case "readonly":
return []string{
"overview:view",
@ -568,6 +578,7 @@ func defaultRolePermissionCodes(code string) []string {
"app-user:view",
"level-config:view",
"pretty-id:view",
"risk-config:view",
"region-block:view",
"room:view",
"room-pin:view",
@ -601,6 +612,7 @@ func defaultRolePermissionCodes(code string) []string {
"wheel:view",
"payment-bill:view",
"payment-third-party:view",
"payment-temporary-link:view",
"payment-product:view",
"game:view",
"daily-task:view",
@ -609,6 +621,7 @@ func defaultRolePermissionCodes(code string) []string {
"room-rocket:view",
"red-packet:view",
"cp-config:view",
"cp-weekly-rank:view",
"vip-config:view",
"weekly-star:view",
"agency-opening:view",
@ -633,11 +646,12 @@ func defaultRolePermissionMigrationCodes(code string) []string {
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
"level-config:view", "level-config:update",
"pretty-id:view", "pretty-id:update", "pretty-id:generate", "pretty-id:grant",
"risk-config:view", "risk-config:update",
"region-block:view", "region-block:update",
"resource:view", "resource:create", "resource:update",
"resource-shop:view", "resource-shop:update",
"resource-group:view", "resource-group:create", "resource-group:update",
"resource-grant:view", "resource-grant:create",
"resource-grant:view", "resource-grant:create", "resource-grant:revoke",
"gift:view", "gift:create", "gift:update", "gift:status", "gift:delete",
"emoji-pack:view", "emoji-pack:create",
"upload:create",
@ -645,7 +659,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
"region:view", "region:create", "region:update", "region:status",
"host-agency-policy:view", "host-agency-policy:create", "host-agency-policy:update", "host-agency-policy:delete", "host-agency-policy:publish", "team-salary-policy:view", "team-salary-policy:create", "team-salary-policy:update", "team-salary-policy:delete", "host-salary-settlement:view", "host-salary-settlement:settle",
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "coin-seller:exchange-rate",
"coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "full-server-notice:view", "full-server-notice:send", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
"coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "full-server-notice:view", "full-server-notice:send", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-temporary-link:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
"lucky-gift:view", "lucky-gift:update", "wheel:view", "wheel:update",
"game:view", "game:create", "game:update", "game:status", "game:delete",
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
@ -653,13 +667,13 @@ func defaultRolePermissionMigrationCodes(code string) []string {
"seven-day-checkin:view", "seven-day-checkin:update",
"room-rocket:view", "room-rocket:update",
"red-packet:view", "red-packet:update",
"cp-config:view", "cp-config:update",
"cp-config:view", "cp-config:update", "cp-weekly-rank:view", "cp-weekly-rank:update",
"vip-config:view", "vip-config:update", "vip-config:grant",
"weekly-star:view", "weekly-star:create", "weekly-star:update", "weekly-star:settle",
"agency-opening:view", "agency-opening:create", "agency-opening:update",
}
case "auditor", "readonly":
return []string{"level-config:view", "pretty-id:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-whitelist:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-seller:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "full-server-notice:view", "lucky-gift:view", "wheel:view", "payment-bill:view", "payment-third-party:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "vip-config:view", "weekly-star:view", "agency-opening:view"}
return []string{"level-config:view", "pretty-id:view", "risk-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-whitelist:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-seller:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "full-server-notice:view", "lucky-gift:view", "wheel:view", "payment-bill:view", "payment-third-party:view", "payment-temporary-link:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "cp-weekly-rank:view", "vip-config:view", "weekly-star:view", "agency-opening:view"}
default:
return nil
}

View File

@ -14,6 +14,7 @@ import (
"hyapp-admin-server/internal/modules/coinledger"
"hyapp-admin-server/internal/modules/countryregion"
"hyapp-admin-server/internal/modules/cprelation"
"hyapp-admin-server/internal/modules/cpweeklyrank"
"hyapp-admin-server/internal/modules/cumulativerechargereward"
"hyapp-admin-server/internal/modules/dailytask"
"hyapp-admin-server/internal/modules/dashboard"
@ -38,6 +39,7 @@ import (
"hyapp-admin-server/internal/modules/registrationreward"
reportmodule "hyapp-admin-server/internal/modules/report"
resourcemodule "hyapp-admin-server/internal/modules/resource"
"hyapp-admin-server/internal/modules/riskconfig"
"hyapp-admin-server/internal/modules/roomadmin"
"hyapp-admin-server/internal/modules/roomrocket"
"hyapp-admin-server/internal/modules/roomturnoverreward"
@ -68,6 +70,7 @@ type Handlers struct {
CoinLedger *coinledger.Handler
CountryRegion *countryregion.Handler
CPRelation *cprelation.Handler
CPWeeklyRank *cpweeklyrank.Handler
CumulativeRecharge *cumulativerechargereward.Handler
DailyTask *dailytask.Handler
Dashboard *dashboard.Handler
@ -92,6 +95,7 @@ type Handlers struct {
RegistrationReward *registrationreward.Handler
RegionBlock *regionblock.Handler
Resource *resourcemodule.Handler
RiskConfig *riskconfig.Handler
RoomAdmin *roomadmin.Handler
RoomRocket *roomrocket.Handler
RoomTurnoverReward *roomturnoverreward.Handler
@ -132,12 +136,14 @@ func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
appconfig.RegisterRoutes(protected, h.AppConfig)
countryregion.RegisterRoutes(protected, h.CountryRegion)
cprelation.RegisterRoutes(protected, h.CPRelation)
cpweeklyrank.RegisterRoutes(protected, h.CPWeeklyRank)
cumulativerechargereward.RegisterRoutes(protected, h.CumulativeRecharge)
dailytask.RegisterRoutes(protected, h.DailyTask)
firstrechargereward.RegisterRoutes(protected, h.FirstRechargeReward)
fullservernotice.RegisterRoutes(protected, h.FullServerNotice)
registrationreward.RegisterRoutes(protected, h.RegistrationReward)
regionblock.RegisterRoutes(protected, h.RegionBlock)
riskconfig.RegisterRoutes(protected, h.RiskConfig)
gamemanagement.RegisterRoutes(protected, h.Game)
giftdiamond.RegisterRoutes(protected, h.GiftDiamond)
roomadmin.RegisterRoutes(protected, h.RoomAdmin)

View File

@ -0,0 +1,54 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 房间置顶是房间管理里的运营入口;生产环境关闭 bootstrap 时,只能依赖 migration 补齐菜单和权限事实。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
('房间置顶查看', 'room-pin:view', 'menu', '允许查看房间置顶列表', @now_ms, @now_ms),
('房间置顶创建', 'room-pin:create', 'button', '允许新增或恢复房间置顶', @now_ms, @now_ms),
('房间置顶取消', 'room-pin:cancel', 'button', '允许取消房间置顶', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
kind = VALUES(kind),
description = VALUES(description),
updated_at_ms = @now_ms;
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
SELECT parent.id, '房间置顶', 'room-pins', '/rooms/pins', 'push_pin', 'room-pin:view', 66, TRUE, @now_ms, @now_ms
FROM admin_menus parent
WHERE parent.code = 'rooms'
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
title = VALUES(title),
path = VALUES(path),
icon = VALUES(icon),
permission_code = VALUES(permission_code),
sort = VALUES(sort),
visible = VALUES(visible),
updated_at_ms = @now_ms;
-- 置顶入口排在房间配置、白名单和机器人房间前面;显式排序避免多次修复或历史状态不同造成排序漂移。
UPDATE admin_menus
SET sort = CASE code
WHEN 'room-config' THEN 67
WHEN 'room-whitelist' THEN 68
WHEN 'room-robots' THEN 69
ELSE sort
END,
updated_at_ms = @now_ms
WHERE code IN ('room-config', 'room-whitelist', 'room-robots');
-- 平台和运营角色可以执行置顶变更;只读和审计角色只能看到列表,避免误给写权限。
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('platform-admin', 'ops-admin')
AND admin_permission.code IN ('room-pin:view', 'room-pin:create', 'room-pin:cancel');
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('auditor', 'readonly')
AND admin_permission.code = 'room-pin:view';

View File

@ -0,0 +1,19 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 资源赠送撤销会触发钱包扣回和权益失效,必须独立成按钮权限,不能复用创建权限。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
('资源赠送撤销', 'resource-grant:revoke', 'button', '允许撤销成功的资源组赠送', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
kind = VALUES(kind),
description = VALUES(description),
updated_at_ms = @now_ms;
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('platform-admin', 'ops-admin')
AND admin_permission.code = 'resource-grant:revoke';

View File

@ -0,0 +1,46 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 注册风控配置由 user-service 执行,后台只负责维护菜单、权限和跨服务配置入口。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
('风控配置查看', 'risk-config:view', 'menu', '', @now_ms, @now_ms),
('风控配置更新', 'risk-config:update', 'button', '', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
kind = VALUES(kind),
description = VALUES(description),
updated_at_ms = @now_ms;
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
SELECT parent.id, '风控管理', 'app-user-risk-config', '/app/users/risk-config', 'shield', 'risk-config:view', 65, TRUE, @now_ms, @now_ms
FROM admin_menus parent
WHERE parent.code = 'app-users'
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
title = VALUES(title),
path = VALUES(path),
icon = VALUES(icon),
permission_code = VALUES(permission_code),
sort = VALUES(sort),
visible = VALUES(visible),
updated_at_ms = @now_ms;
UPDATE admin_menus
SET sort = 66, updated_at_ms = @now_ms
WHERE code = 'app-user-region-blocks'
AND sort < 66;
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('platform-admin', 'ops-admin')
AND admin_permission.code IN ('risk-config:view', 'risk-config:update');
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('auditor', 'readonly')
AND admin_permission.code = 'risk-config:view';

View File

@ -0,0 +1,37 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 三方临时支付链接只用于内部追踪临时支付订单;权限独立于三方支付配置更新权限,避免只读运营误改渠道配置。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
('三方临时支付链接查看', 'payment-temporary-link:view', 'menu', '', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
kind = VALUES(kind),
description = VALUES(description),
updated_at_ms = @now_ms;
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
SELECT parent.id, '三方临时支付链接', 'payment-temporary-links', '/payment/temporary-links', 'receipt', 'payment-temporary-link:view', 70, TRUE, @now_ms, @now_ms
FROM admin_menus parent
WHERE parent.code = 'payment'
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
title = VALUES(title),
path = VALUES(path),
icon = VALUES(icon),
permission_code = VALUES(permission_code),
sort = VALUES(sort),
visible = VALUES(visible),
updated_at_ms = @now_ms;
UPDATE admin_menus
SET sort = 71, updated_at_ms = @now_ms
WHERE code = 'payment-recharge-products';
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('platform-admin', 'ops-admin', 'auditor', 'readonly')
AND admin_permission.code = 'payment-temporary-link:view';

View File

@ -0,0 +1,57 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CP排行活动对应 H5 的 cp-weekly-rank 活动读接口后台只维护启停、Top 奖励资源组和查看发奖事实。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
('CP 排行活动查看', 'cp-weekly-rank:view', 'menu', '', @now_ms, @now_ms),
('CP 排行活动更新', 'cp-weekly-rank:update', 'button', '', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
kind = VALUES(kind),
description = VALUES(description),
updated_at_ms = @now_ms;
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
SELECT parent.id, 'CP排行活动', 'cp-weekly-rank', '/activities/cp-weekly-rank', 'leaderboard', 'cp-weekly-rank:view', 77, TRUE, @now_ms, @now_ms
FROM admin_menus parent
WHERE parent.code = 'activities'
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
title = VALUES(title),
path = VALUES(path),
icon = VALUES(icon),
permission_code = VALUES(permission_code),
sort = VALUES(sort),
visible = VALUES(visible),
updated_at_ms = @now_ms;
UPDATE admin_menus
SET sort = 78, updated_at_ms = @now_ms
WHERE code = 'vip-config';
UPDATE admin_menus
SET sort = 79, updated_at_ms = @now_ms
WHERE code = 'weekly-star';
UPDATE admin_menus
SET sort = 80, updated_at_ms = @now_ms
WHERE code = 'agency-opening';
UPDATE admin_menus
SET sort = 81, updated_at_ms = @now_ms
WHERE code = 'wheel';
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('platform-admin', 'ops-admin')
AND admin_permission.code IN ('cp-weekly-rank:view', 'cp-weekly-rank:update');
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('auditor', 'readonly')
AND admin_permission.code = 'cp-weekly-rank:view';

View File

@ -24,6 +24,13 @@ red_packet_broadcast_worker:
enabled: true
task_event_worker:
enabled: true
message_action_confirm_worker:
enabled: true
outbox_poll_interval: "1s"
outbox_batch_size: 100
outbox_lock_ttl: "30s"
outbox_max_retry: 8
publish_timeout: "5s"
lucky_gift_worker:
enabled: true
worker_poll_interval: "1s"
@ -82,7 +89,14 @@ rocketmq:
consumer_group: "hyapp-activity-user-region-broadcast"
task_consumer_group: "hyapp-activity-task-user-outbox"
invite_activity_consumer_group: "hyapp-activity-invite-user-outbox"
message_action_consumer_group: "hyapp-activity-message-action-user-outbox"
consumer_max_reconsume_times: 16
message_action_outbox:
enabled: true
topic: "hyapp_message_action_outbox"
producer_group: "hyapp-activity-message-action-outbox"
send_timeout: "5s"
retry: 2
consumer:
room_outbox_poll_interval_ms: 1000
batch_size: 100

View File

@ -24,6 +24,13 @@ red_packet_broadcast_worker:
enabled: true
task_event_worker:
enabled: true
message_action_confirm_worker:
enabled: true
outbox_poll_interval: "1s"
outbox_batch_size: 100
outbox_lock_ttl: "30s"
outbox_max_retry: 8
publish_timeout: "5s"
lucky_gift_worker:
enabled: true
worker_poll_interval: "1s"
@ -82,7 +89,14 @@ rocketmq:
consumer_group: "hyapp-activity-user-region-broadcast"
task_consumer_group: "hyapp-activity-task-user-outbox"
invite_activity_consumer_group: "hyapp-activity-invite-user-outbox"
message_action_consumer_group: "hyapp-activity-message-action-user-outbox"
consumer_max_reconsume_times: 16
message_action_outbox:
enabled: true
topic: "hyapp_message_action_outbox"
producer_group: "hyapp-activity-message-action-outbox"
send_timeout: "5s"
retry: 2
consumer:
room_outbox_poll_interval_ms: 1000
batch_size: 100

View File

@ -24,6 +24,13 @@ red_packet_broadcast_worker:
enabled: true
task_event_worker:
enabled: true
message_action_confirm_worker:
enabled: true
outbox_poll_interval: "1s"
outbox_batch_size: 100
outbox_lock_ttl: "30s"
outbox_max_retry: 8
publish_timeout: "5s"
lucky_gift_worker:
enabled: true
worker_poll_interval: "1s"
@ -82,7 +89,14 @@ rocketmq:
consumer_group: "hyapp-activity-user-region-broadcast"
task_consumer_group: "hyapp-activity-task-user-outbox"
invite_activity_consumer_group: "hyapp-activity-invite-user-outbox"
message_action_consumer_group: "hyapp-activity-message-action-user-outbox"
consumer_max_reconsume_times: 16
message_action_outbox:
enabled: true
topic: "hyapp_message_action_outbox"
producer_group: "hyapp-activity-message-action-outbox"
send_timeout: "5s"
retry: 2
consumer:
room_outbox_poll_interval_ms: 1000
batch_size: 100

View File

@ -1201,6 +1201,55 @@ CREATE TABLE IF NOT EXISTS message_fanout_jobs (
KEY idx_message_fanout_status (app_code, status, next_run_at_ms, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息分发任务表';
CREATE TABLE IF NOT EXISTS message_action_confirms (
app_code VARCHAR(32) NOT NULL COMMENT 'App 租户编码',
confirm_id VARCHAR(96) NOT NULL COMMENT 'App 操作确认 ID',
source_name VARCHAR(64) NOT NULL COMMENT '来源事实名,例如 user_outbox',
source_event_id VARCHAR(128) NOT NULL COMMENT '来源 outbox event_id',
business_type VARCHAR(64) NOT NULL COMMENT '业务类型,例如 role_invitation',
business_subtype VARCHAR(64) NOT NULL COMMENT '业务子类型,例如 host/agency/bd',
business_id VARCHAR(128) NOT NULL COMMENT '业务 owner 的事实 ID',
sender_user_id BIGINT NOT NULL COMMENT 'IM 发送方用户 ID',
receiver_user_id BIGINT NOT NULL COMMENT 'IM 接收方用户 ID',
message_text VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'IM 展示文案快照',
status VARCHAR(32) NOT NULL COMMENT 'pending/processing/accepted/rejected/expired/canceled/failed',
action VARCHAR(32) NOT NULL DEFAULT '' COMMENT '终态动作 accept/reject',
command_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '处理动作 command_id',
processed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '处理完成 UTC epoch ms',
locked_by VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'processing 锁持有者',
lock_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'processing 锁过期 UTC epoch ms',
error_message VARCHAR(512) NOT NULL DEFAULT '' COMMENT '最后一次处理失败原因',
metadata_json JSON 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, confirm_id),
UNIQUE KEY uk_message_action_source (app_code, source_name, source_event_id),
KEY idx_message_action_receiver_status (app_code, receiver_user_id, status, created_at_ms, confirm_id),
KEY idx_message_action_conversation (app_code, receiver_user_id, sender_user_id, created_at_ms, confirm_id),
KEY idx_message_action_lock (app_code, status, lock_until_ms, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='App IM 操作确认消息状态';
CREATE TABLE IF NOT EXISTS message_action_outbox (
app_code VARCHAR(32) NOT NULL COMMENT 'App 租户编码',
event_id VARCHAR(128) NOT NULL COMMENT '确认消息 outbox 事件 ID',
confirm_id VARCHAR(96) NOT NULL COMMENT '关联 confirm_id',
event_type VARCHAR(96) NOT NULL COMMENT '事件类型,例如 ConfirmMessageCreated',
status VARCHAR(32) NOT NULL COMMENT 'pending/running/retryable/delivered/failed',
worker_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'MQ 投递 worker ID',
lock_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'MQ 投递锁过期 UTC epoch ms',
retry_count INT NOT NULL DEFAULT 0 COMMENT '投递重试次数',
next_retry_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '下一次投递 UTC epoch ms',
last_error VARCHAR(512) NOT NULL DEFAULT '' COMMENT '最后一次投递失败原因',
payload_json JSON NOT NULL COMMENT '投递给 notice-service 的确认 IM 快照',
created_at_ms BIGINT NOT NULL COMMENT '创建 UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新 UTC epoch ms',
PRIMARY KEY (app_code, event_id),
UNIQUE KEY uk_message_action_outbox_confirm (app_code, confirm_id, event_type),
KEY idx_message_action_outbox_status_created (app_code, status, next_retry_at_ms, created_at_ms, event_id),
KEY idx_message_action_outbox_lock (app_code, status, lock_until_ms, created_at_ms, event_id),
KEY idx_message_action_outbox_retention (app_code, status, updated_at_ms, event_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='确认消息 IM 投递 outbox';
-- 本地开发必须开箱即可验证幸运礼物 v2 链路INSERT IGNORE 只补缺省奖池,不覆盖后台已发布版本。
SET @lucky_seed_now_ms := 1779259000000;
INSERT IGNORE INTO lucky_gift_rule_versions (

View File

@ -0,0 +1,48 @@
CREATE TABLE IF NOT EXISTS message_action_confirms (
app_code VARCHAR(32) NOT NULL COMMENT 'App 租户编码',
confirm_id VARCHAR(96) NOT NULL COMMENT 'App 操作确认 ID',
source_name VARCHAR(64) NOT NULL COMMENT '来源事实名,例如 user_outbox',
source_event_id VARCHAR(128) NOT NULL COMMENT '来源 outbox event_id',
business_type VARCHAR(64) NOT NULL COMMENT '业务类型,例如 role_invitation',
business_subtype VARCHAR(64) NOT NULL COMMENT '业务子类型,例如 host/agency/bd',
business_id VARCHAR(128) NOT NULL COMMENT '业务 owner 的事实 ID',
sender_user_id BIGINT NOT NULL COMMENT 'IM 发送方用户 ID',
receiver_user_id BIGINT NOT NULL COMMENT 'IM 接收方用户 ID',
message_text VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'IM 展示文案快照',
status VARCHAR(32) NOT NULL COMMENT 'pending/processing/accepted/rejected/expired/canceled/failed',
action VARCHAR(32) NOT NULL DEFAULT '' COMMENT '终态动作 accept/reject',
command_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '处理动作 command_id',
processed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '处理完成 UTC epoch ms',
locked_by VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'processing 锁持有者',
lock_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'processing 锁过期 UTC epoch ms',
error_message VARCHAR(512) NOT NULL DEFAULT '' COMMENT '最后一次处理失败原因',
metadata_json JSON 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, confirm_id),
UNIQUE KEY uk_message_action_source (app_code, source_name, source_event_id),
KEY idx_message_action_receiver_status (app_code, receiver_user_id, status, created_at_ms, confirm_id),
KEY idx_message_action_conversation (app_code, receiver_user_id, sender_user_id, created_at_ms, confirm_id),
KEY idx_message_action_lock (app_code, status, lock_until_ms, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='App IM 操作确认消息状态';
CREATE TABLE IF NOT EXISTS message_action_outbox (
app_code VARCHAR(32) NOT NULL COMMENT 'App 租户编码',
event_id VARCHAR(128) NOT NULL COMMENT '确认消息 outbox 事件 ID',
confirm_id VARCHAR(96) NOT NULL COMMENT '关联 confirm_id',
event_type VARCHAR(96) NOT NULL COMMENT '事件类型,例如 ConfirmMessageCreated',
status VARCHAR(32) NOT NULL COMMENT 'pending/running/retryable/delivered/failed',
worker_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'MQ 投递 worker ID',
lock_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'MQ 投递锁过期 UTC epoch ms',
retry_count INT NOT NULL DEFAULT 0 COMMENT '投递重试次数',
next_retry_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '下一次投递 UTC epoch ms',
last_error VARCHAR(512) NOT NULL DEFAULT '' COMMENT '最后一次投递失败原因',
payload_json JSON NOT NULL COMMENT '投递给 notice-service 的确认 IM 快照',
created_at_ms BIGINT NOT NULL COMMENT '创建 UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新 UTC epoch ms',
PRIMARY KEY (app_code, event_id),
UNIQUE KEY uk_message_action_outbox_confirm (app_code, confirm_id, event_type),
KEY idx_message_action_outbox_status_created (app_code, status, next_retry_at_ms, created_at_ms, event_id),
KEY idx_message_action_outbox_lock (app_code, status, lock_until_ms, created_at_ms, event_id),
KEY idx_message_action_outbox_retention (app_code, status, updated_at_ms, event_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='确认消息 IM 投递 outbox';

View File

@ -0,0 +1,153 @@
package app
import (
"context"
"encoding/json"
"log/slog"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/logx"
"hyapp/pkg/messagemq"
"hyapp/pkg/rocketmqx"
"hyapp/pkg/usermq"
"hyapp/services/activity-service/internal/config"
messagedomain "hyapp/services/activity-service/internal/domain/message"
)
const userOutboxEventRoleInvitationCreated = "RoleInvitationCreated"
func newMessageActionUserOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
consumer, err := rocketmqx.NewConsumer(userOutboxMessageActionConsumerConfig(cfg.RocketMQ))
if err != nil {
return nil, err
}
if err := consumer.Subscribe(cfg.RocketMQ.UserOutbox.Topic, usermq.TagUserOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
event, appCode, ok, err := roleInvitationConfirmEventFromUserMessage(message.Body)
if err != nil || !ok {
return err
}
_, _, err = services.actionConfirm.ConsumeRoleInvitationCreated(appcode.WithContext(ctx, appCode), event)
return err
}); err != nil {
_ = consumer.Shutdown()
return nil, err
}
return consumer, nil
}
type roleInvitationCreatedPayload struct {
InvitationID int64 `json:"invitation_id"`
InvitationType string `json:"invitation_type"`
InviterUserID int64 `json:"inviter_user_id"`
TargetUserID int64 `json:"target_user_id"`
AgencyName string `json:"agency_name"`
Inviter messagedomain.RoleInvitationUserSnapshot `json:"inviter"`
Target messagedomain.RoleInvitationUserSnapshot `json:"target"`
CreatedAtMS int64 `json:"created_at_ms"`
ParentBDUserID int64 `json:"parent_bd_user_id"`
ParentLeaderUserID int64 `json:"parent_leader_user_id"`
}
func roleInvitationConfirmEventFromUserMessage(body []byte) (messagedomain.RoleInvitationConfirmEvent, string, bool, error) {
message, err := usermq.DecodeUserOutboxMessage(body)
if err != nil {
return messagedomain.RoleInvitationConfirmEvent{}, "", false, err
}
if message.EventType != userOutboxEventRoleInvitationCreated {
return messagedomain.RoleInvitationConfirmEvent{}, message.AppCode, false, nil
}
var payload roleInvitationCreatedPayload
if err := json.Unmarshal([]byte(message.PayloadJSON), &payload); err != nil {
return messagedomain.RoleInvitationConfirmEvent{}, message.AppCode, true, err
}
return messagedomain.RoleInvitationConfirmEvent{
SourceEventID: message.EventID,
InvitationID: payload.InvitationID,
InvitationType: payload.InvitationType,
InviterUserID: payload.InviterUserID,
TargetUserID: payload.TargetUserID,
AgencyName: payload.AgencyName,
Inviter: payload.Inviter,
Target: payload.Target,
CreatedAtMS: firstPositiveMS(payload.CreatedAtMS, message.OccurredAtMS),
RawPayloadJSON: message.PayloadJSON,
ParentBDUserID: payload.ParentBDUserID,
ParentLeaderUserID: payload.ParentLeaderUserID,
}, message.AppCode, true, nil
}
func (a *App) runMessageActionOutboxWorker(ctx context.Context) {
options := a.messageActionWorkerOptions
ticker := time.NewTicker(options.OutboxPollInterval)
defer ticker.Stop()
for {
processed, err := a.processMessageActionOutboxBatch(ctx)
if err != nil && ctx.Err() == nil {
logx.Error(ctx, "message_action_outbox_publish_failed", err, slog.String("node_id", a.workerNodeID))
}
if processed >= options.OutboxBatchSize {
continue
}
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}
func (a *App) processMessageActionOutboxBatch(ctx context.Context) (int, error) {
options := a.messageActionWorkerOptions
workerCtx := appcode.WithContext(ctx, appcode.Default)
records, err := a.actionConfirm.ClaimPendingOutbox(workerCtx, options.OutboxBatchSize, options.OutboxLockTTL)
if err != nil {
return 0, err
}
for _, record := range records {
publishCtx, cancel := context.WithTimeout(context.Background(), options.PublishTimeout)
err := a.publishMessageActionOutboxRecord(publishCtx, record)
cancel()
recordCtx := appcode.WithContext(context.Background(), record.AppCode)
if err != nil {
nextRetryAtMS := time.Now().UTC().Add(options.OutboxPollInterval).UnixMilli()
if markErr := a.actionConfirm.MarkOutboxRetryable(recordCtx, record.EventID, err, nextRetryAtMS); markErr != nil {
return len(records), markErr
}
return len(records), err
}
if err := a.actionConfirm.MarkOutboxDelivered(recordCtx, record.EventID); err != nil {
return len(records), err
}
}
return len(records), nil
}
func (a *App) publishMessageActionOutboxRecord(ctx context.Context, record messagedomain.ActionConfirmOutbox) error {
body, err := messagemq.EncodeActionOutboxMessage(messagemq.ActionOutboxMessage{
AppCode: record.AppCode,
EventID: record.EventID,
EventType: record.EventType,
ConfirmID: record.ConfirmID,
PayloadJSON: record.PayloadJSON,
OccurredAtMS: record.CreatedAtMS,
})
if err != nil {
return err
}
return a.messageActionProducer.SendSync(ctx, rocketmqx.Message{
Topic: a.messageActionOutboxTopic,
Tag: messagemq.TagMessageActionOutboxEvent,
Keys: []string{record.EventID, record.ConfirmID},
Body: body,
})
}
func firstPositiveMS(values ...int64) int64 {
for _, value := range values {
if value > 0 {
return value
}
}
return time.Now().UTC().UnixMilli()
}

View File

@ -21,32 +21,38 @@ import (
firstrechargeservice "hyapp/services/activity-service/internal/service/firstrecharge"
inviteactivityservice "hyapp/services/activity-service/internal/service/inviteactivity"
luckygiftservice "hyapp/services/activity-service/internal/service/luckygift"
messageservice "hyapp/services/activity-service/internal/service/message"
mysqlstorage "hyapp/services/activity-service/internal/storage/mysql"
)
// App 装配 activity-service 的 gRPC 入口、健康检查、后台 worker 和外部连接。
// 具体业务模块的创建和注册都放在独立文件里,避免启动入口继续膨胀成一个难维护的大函数。
type App struct {
server *grpc.Server
listener net.Listener
health *grpchealth.ServingChecker
healthHTTP *healthhttp.Server
mysqlRepo *mysqlstorage.Repository
broadcast *broadcastservice.Service
luckyGift *luckygiftservice.Service
firstRechargeReward *firstrechargeservice.Service
cumulativeRecharge *cumulativerechargeservice.Service
inviteActivityReward *inviteactivityservice.Service
broadcastWorkerEnabled bool
luckyGiftWorkerEnabled bool
workerNodeID string
luckyGiftWorkerOptions luckygiftservice.WorkerOptions
mqConsumers []*rocketmqx.Consumer
userConn *grpc.ClientConn
walletConn *grpc.ClientConn
roomConn *grpc.ClientConn
workers *serviceapp.BackgroundGroup
closeOnce sync.Once
server *grpc.Server
listener net.Listener
health *grpchealth.ServingChecker
healthHTTP *healthhttp.Server
mysqlRepo *mysqlstorage.Repository
broadcast *broadcastservice.Service
luckyGift *luckygiftservice.Service
firstRechargeReward *firstrechargeservice.Service
cumulativeRecharge *cumulativerechargeservice.Service
inviteActivityReward *inviteactivityservice.Service
actionConfirm *messageservice.ActionConfirmService
broadcastWorkerEnabled bool
luckyGiftWorkerEnabled bool
messageActionWorkerEnabled bool
workerNodeID string
luckyGiftWorkerOptions luckygiftservice.WorkerOptions
messageActionWorkerOptions config.MessageActionConfirmWorkerConfig
messageActionOutboxTopic string
mqConsumers []*rocketmqx.Consumer
messageActionProducer *rocketmqx.Producer
userConn *grpc.ClientConn
walletConn *grpc.ClientConn
roomConn *grpc.ClientConn
workers *serviceapp.BackgroundGroup
closeOnce sync.Once
}
// New 初始化 activity-service 应用。
@ -61,8 +67,10 @@ func New(cfg config.Config) (*App, error) {
var listener net.Listener
var clients externalClients
var mqConsumers []*rocketmqx.Consumer
var messageActionProducer *rocketmqx.Producer
cleanup := func() {
shutdownConsumers(mqConsumers)
servicemq.ShutdownProducers([]*rocketmqx.Producer{messageActionProducer})
clients.Close()
if listener != nil {
_ = listener.Close()
@ -94,6 +102,13 @@ func New(cfg config.Config) (*App, error) {
cleanup()
return nil, err
}
if cfg.RocketMQ.MessageActionOutbox.Enabled {
messageActionProducer, err = rocketmqx.NewProducer(messageActionProducerConfig(cfg.RocketMQ))
if err != nil {
cleanup()
return nil, err
}
}
health, healthHTTP, err := newHealthServers(cfg, server, repository)
if err != nil {
cleanup()
@ -101,25 +116,30 @@ func New(cfg config.Config) (*App, error) {
}
return &App{
server: server,
listener: listener,
health: health,
healthHTTP: healthHTTP,
mysqlRepo: repository,
broadcast: services.broadcast,
luckyGift: services.luckyGift,
firstRechargeReward: services.firstRechargeReward,
cumulativeRecharge: services.cumulativeRecharge,
inviteActivityReward: services.inviteActivityReward,
broadcastWorkerEnabled: cfg.Broadcast.Enabled,
luckyGiftWorkerEnabled: cfg.LuckyGiftWorker.Enabled,
workerNodeID: cfg.NodeID,
luckyGiftWorkerOptions: luckyGiftWorkerOptions(cfg.NodeID, cfg.LuckyGiftWorker),
mqConsumers: mqConsumers,
userConn: clients.userConn,
walletConn: clients.walletConn,
roomConn: clients.roomConn,
workers: serviceapp.NewBackground(context.Background()),
server: server,
listener: listener,
health: health,
healthHTTP: healthHTTP,
mysqlRepo: repository,
broadcast: services.broadcast,
luckyGift: services.luckyGift,
firstRechargeReward: services.firstRechargeReward,
cumulativeRecharge: services.cumulativeRecharge,
inviteActivityReward: services.inviteActivityReward,
actionConfirm: services.actionConfirm,
broadcastWorkerEnabled: cfg.Broadcast.Enabled,
luckyGiftWorkerEnabled: cfg.LuckyGiftWorker.Enabled,
messageActionWorkerEnabled: cfg.MessageActionConfirmWorker.Enabled,
workerNodeID: cfg.NodeID,
luckyGiftWorkerOptions: luckyGiftWorkerOptions(cfg.NodeID, cfg.LuckyGiftWorker),
messageActionWorkerOptions: cfg.MessageActionConfirmWorker,
messageActionOutboxTopic: cfg.RocketMQ.MessageActionOutbox.Topic,
mqConsumers: mqConsumers,
messageActionProducer: messageActionProducer,
userConn: clients.userConn,
walletConn: clients.walletConn,
roomConn: clients.roomConn,
workers: serviceapp.NewBackground(context.Background()),
}, nil
}
@ -141,6 +161,11 @@ func (a *App) Run() error {
a.luckyGift.RunWorker(ctx, a.luckyGiftWorkerOptions)
})
}
if a.messageActionWorkerEnabled && a.actionConfirm != nil && a.messageActionProducer != nil && a.workers != nil {
a.workers.Go(func(ctx context.Context) {
a.runMessageActionOutboxWorker(ctx)
})
}
err := a.server.Serve(a.listener)
if a.workers != nil {
a.workers.StopAndWait()
@ -201,9 +226,13 @@ func (a *App) closeHealthHTTP() {
}
func (a *App) startMQ() error {
if err := servicemq.StartProducers([]*rocketmqx.Producer{a.messageActionProducer}); err != nil {
return err
}
return servicemq.StartConsumers(a.mqConsumers)
}
func (a *App) shutdownMQ() {
servicemq.ShutdownConsumers(a.mqConsumers)
servicemq.ShutdownProducers([]*rocketmqx.Producer{a.messageActionProducer})
}

View File

@ -9,6 +9,7 @@ import (
func registerGRPCServers(server *grpc.Server, services *serviceBundle) {
activityv1.RegisterActivityServiceServer(server, grpcserver.NewServer(services.activity))
activityv1.RegisterMessageInboxServiceServer(server, grpcserver.NewMessageServer(services.message))
activityv1.RegisterMessageActionConfirmServiceServer(server, grpcserver.NewActionConfirmServer(services.actionConfirm))
// cron 入口只做调度适配;具体状态机仍在各 activity service 内部。
// weekly-star 和 room-turnover 都依赖 wallet 发奖,所以这里必须把同一批已装配的 service 注入 cron server。
activityv1.RegisterActivityCronServiceServer(server, grpcserver.NewCronServer(services.message, services.growth, services.achievement, services.weeklyStar, services.roomTurnoverReward, services.agencyOpening, services.cpWeeklyRank))

View File

@ -80,6 +80,14 @@ func buildMQConsumers(cfg config.Config, services *serviceBundle) ([]*rocketmqx.
}
mqConsumers = append(mqConsumers, consumer)
}
if cfg.MessageActionConfirmWorker.Enabled && cfg.RocketMQ.UserOutbox.Enabled {
consumer, err := newMessageActionUserOutboxConsumer(cfg, services)
if err != nil {
shutdownConsumers(mqConsumers)
return nil, err
}
mqConsumers = append(mqConsumers, consumer)
}
if cfg.RedPacketBroadcastWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled {
consumer, err := newRedPacketWalletConsumer(cfg, services)
if err != nil {
@ -317,10 +325,31 @@ func userOutboxInviteActivityConsumerConfig(cfg config.RocketMQConfig) rocketmqx
return rocketMQConsumerConfig(cfg, cfg.UserOutbox.InviteActivityConsumerGroup, cfg.UserOutbox.ConsumerMaxReconsumeTimes)
}
func userOutboxMessageActionConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
return rocketMQConsumerConfig(cfg, cfg.UserOutbox.MessageActionConsumerGroup, cfg.UserOutbox.ConsumerMaxReconsumeTimes)
}
func walletOutboxConsumerConfig(cfg config.RocketMQConfig, group string) rocketmqx.ConsumerConfig {
return rocketMQConsumerConfig(cfg, group, cfg.WalletOutbox.ConsumerMaxReconsumeTimes)
}
func messageActionProducerConfig(cfg config.RocketMQConfig) rocketmqx.ProducerConfig {
return rocketmqx.ProducerConfig{
EndpointConfig: rocketmqx.EndpointConfig{
NameServers: cfg.NameServers,
NameServerDomain: cfg.NameServerDomain,
AccessKey: cfg.AccessKey,
SecretKey: cfg.SecretKey,
SecurityToken: cfg.SecurityToken,
Namespace: cfg.Namespace,
VIPChannel: cfg.VIPChannel,
},
GroupName: cfg.MessageActionOutbox.ProducerGroup,
SendTimeout: cfg.MessageActionOutbox.SendTimeout,
Retry: cfg.MessageActionOutbox.Retry,
}
}
func redPacketWalletOutboxTopic(cfg config.WalletOutboxMQConfig) string {
// 钱包实时 topic 显式配置后,红包 worker 只消费实时通道,避免继续被普通账务 topic 的高频消息拖慢。
if cfg.RealtimeTopic != "" {

View File

@ -31,6 +31,7 @@ import (
type serviceBundle struct {
activity *activityservice.Service
message *messageservice.Service
actionConfirm *messageservice.ActionConfirmService
task *taskservice.Service
registrationReward *registrationrewardservice.Service
sevenDayCheckIn *sevendaycheckinservice.Service
@ -55,6 +56,7 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
activitySvc := activityservice.New(activityservice.Config{NodeID: cfg.NodeID}, repository)
messageSvc := messageservice.New(messageservice.Config{NodeID: cfg.NodeID}, repository, messageservice.WithTargetSource(activityclient.NewGRPCUserTargetSource(clients.userConn)))
actionConfirmSvc := messageservice.NewActionConfirm(messageservice.Config{NodeID: cfg.NodeID}, repository, activityclient.NewGRPCRoleInvitationClient(clients.userConn))
taskSvc := taskservice.New(repository, walletClient)
registrationRewardSvc := registrationrewardservice.New(repository, walletClient)
sevenDayCheckInSvc := sevendaycheckinservice.New(repository, walletClient)
@ -106,6 +108,7 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
return &serviceBundle{
activity: activitySvc,
message: messageSvc,
actionConfirm: actionConfirmSvc,
task: taskSvc,
registrationReward: registrationRewardSvc,
sevenDayCheckIn: sevenDayCheckInSvc,

View File

@ -0,0 +1,65 @@
package client
import (
"context"
"google.golang.org/grpc"
userv1 "hyapp.local/api/proto/user/v1"
"hyapp/pkg/appcode"
messageservice "hyapp/services/activity-service/internal/service/message"
)
// GRPCRoleInvitationClient calls user-service because role invitation state remains owned by user-service.
type GRPCRoleInvitationClient struct {
client userv1.UserHostServiceClient
}
func NewGRPCRoleInvitationClient(conn *grpc.ClientConn) *GRPCRoleInvitationClient {
return &GRPCRoleInvitationClient{client: userv1.NewUserHostServiceClient(conn)}
}
func (c *GRPCRoleInvitationClient) ProcessRoleInvitation(ctx context.Context, input messageservice.ProcessRoleInvitationInput) (messageservice.RoleInvitationState, error) {
resp, err := c.client.ProcessRoleInvitation(ctx, &userv1.ProcessRoleInvitationRequest{
Meta: &userv1.RequestMeta{
RequestId: "message-action-confirm-process-role-invitation",
Caller: "activity-service",
AppCode: appcode.FromContext(ctx),
},
CommandId: input.CommandID,
ActorUserId: input.ActorUserID,
InvitationId: input.InvitationID,
Action: input.Action,
Reason: input.Reason,
})
if err != nil {
return messageservice.RoleInvitationState{}, err
}
return roleInvitationStateFromProto(resp.GetInvitation()), nil
}
func (c *GRPCRoleInvitationClient) GetRoleInvitation(ctx context.Context, actorUserID int64, invitationID int64) (messageservice.RoleInvitationState, error) {
resp, err := c.client.GetRoleInvitation(ctx, &userv1.GetRoleInvitationRequest{
Meta: &userv1.RequestMeta{
RequestId: "message-action-confirm-get-role-invitation",
Caller: "activity-service",
AppCode: appcode.FromContext(ctx),
},
ActorUserId: actorUserID,
InvitationId: invitationID,
})
if err != nil {
return messageservice.RoleInvitationState{}, err
}
return roleInvitationStateFromProto(resp.GetInvitation()), nil
}
func roleInvitationStateFromProto(invitation *userv1.RoleInvitation) messageservice.RoleInvitationState {
if invitation == nil {
return messageservice.RoleInvitationState{}
}
return messageservice.RoleInvitationState{
InvitationID: invitation.GetInvitationId(),
InvitationType: invitation.GetInvitationType(),
Status: invitation.GetStatus(),
}
}

View File

@ -49,7 +49,9 @@ type Config struct {
MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
Consumer ConsumerConfig `yaml:"consumer"`
MessageInbox MessageInboxConfig `yaml:"message_inbox"`
Log logx.Config `yaml:"log"`
// MessageActionConfirmWorker 控制通用确认消息的 user_outbox 消费和 IM outbox 发布。
MessageActionConfirmWorker MessageActionConfirmWorkerConfig `yaml:"message_action_confirm_worker"`
Log logx.Config `yaml:"log"`
}
// ConsumerConfig 保存 room outbox 消费底座配置。
@ -63,6 +65,16 @@ type MessageInboxConfig struct {
UnreadCacheTTL string `yaml:"unread_cache_ttl"`
}
// MessageActionConfirmWorkerConfig 保存确认消息消费和 MQ 发布策略。
type MessageActionConfirmWorkerConfig struct {
Enabled bool `yaml:"enabled"`
OutboxPollInterval time.Duration `yaml:"outbox_poll_interval"`
OutboxBatchSize int `yaml:"outbox_batch_size"`
OutboxLockTTL time.Duration `yaml:"outbox_lock_ttl"`
OutboxMaxRetry int `yaml:"outbox_max_retry"`
PublishTimeout time.Duration `yaml:"publish_timeout"`
}
// FirstRechargeRewardWorkerConfig 保存钱包充值事实消费策略。
type FirstRechargeRewardWorkerConfig struct {
// Enabled 控制是否启动 wallet_outbox MQ 消费;关闭后仍可通过 gRPC 消费入口补偿。
@ -149,17 +161,18 @@ type BroadcastConfig struct {
// RocketMQConfig 描述 activity-service 消费房间事实的 MQ 连接。
type RocketMQConfig struct {
Enabled bool `yaml:"enabled"`
NameServers []string `yaml:"name_servers"`
NameServerDomain string `yaml:"name_server_domain"`
AccessKey string `yaml:"access_key"`
SecretKey string `yaml:"secret_key"`
SecurityToken string `yaml:"security_token"`
Namespace string `yaml:"namespace"`
VIPChannel bool `yaml:"vip_channel"`
RoomOutbox RoomOutboxMQConfig `yaml:"room_outbox"`
WalletOutbox WalletOutboxMQConfig `yaml:"wallet_outbox"`
UserOutbox UserOutboxMQConfig `yaml:"user_outbox"`
Enabled bool `yaml:"enabled"`
NameServers []string `yaml:"name_servers"`
NameServerDomain string `yaml:"name_server_domain"`
AccessKey string `yaml:"access_key"`
SecretKey string `yaml:"secret_key"`
SecurityToken string `yaml:"security_token"`
Namespace string `yaml:"namespace"`
VIPChannel bool `yaml:"vip_channel"`
RoomOutbox RoomOutboxMQConfig `yaml:"room_outbox"`
WalletOutbox WalletOutboxMQConfig `yaml:"wallet_outbox"`
UserOutbox UserOutboxMQConfig `yaml:"user_outbox"`
MessageActionOutbox MessageActionOutboxMQConfig `yaml:"message_action_outbox"`
}
// RoomOutboxMQConfig 控制 activity 对 room_outbox topic 的消费位点。
@ -190,9 +203,19 @@ type UserOutboxMQConfig struct {
ConsumerGroup string `yaml:"consumer_group"`
TaskConsumerGroup string `yaml:"task_consumer_group"`
InviteActivityConsumerGroup string `yaml:"invite_activity_consumer_group"`
MessageActionConsumerGroup string `yaml:"message_action_consumer_group"`
ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"`
}
// MessageActionOutboxMQConfig 控制确认消息 outbox 投递到 notice-service 的 MQ producer。
type MessageActionOutboxMQConfig struct {
Enabled bool `yaml:"enabled"`
Topic string `yaml:"topic"`
ProducerGroup string `yaml:"producer_group"`
SendTimeout time.Duration `yaml:"send_timeout"`
Retry int `yaml:"retry"`
}
// Default 返回本地默认配置。
func Default() Config {
return Config{
@ -220,6 +243,14 @@ func Default() Config {
TaskEventWorker: TaskEventWorkerConfig{
Enabled: true,
},
MessageActionConfirmWorker: MessageActionConfirmWorkerConfig{
Enabled: true,
OutboxPollInterval: time.Second,
OutboxBatchSize: 100,
OutboxLockTTL: 30 * time.Second,
OutboxMaxRetry: 8,
PublishTimeout: 5 * time.Second,
},
LuckyGiftWorker: LuckyGiftWorkerConfig{
Enabled: false,
WorkerPollInterval: time.Second,
@ -291,8 +322,16 @@ func defaultRocketMQConfig() RocketMQConfig {
ConsumerGroup: "hyapp-activity-user-region-broadcast",
TaskConsumerGroup: "hyapp-activity-task-user-outbox",
InviteActivityConsumerGroup: "hyapp-activity-invite-user-outbox",
MessageActionConsumerGroup: "hyapp-activity-message-action-user-outbox",
ConsumerMaxReconsumeTimes: 16,
},
MessageActionOutbox: MessageActionOutboxMQConfig{
Enabled: false,
Topic: "hyapp_message_action_outbox",
ProducerGroup: "hyapp-activity-message-action-outbox",
SendTimeout: 5 * time.Second,
Retry: 2,
},
}
}
@ -386,6 +425,21 @@ func Load(path string) (Config, error) {
if cfg.LuckyGiftWorker.PublishTimeout <= 0 {
cfg.LuckyGiftWorker.PublishTimeout = 5 * time.Second
}
if cfg.MessageActionConfirmWorker.OutboxPollInterval <= 0 {
cfg.MessageActionConfirmWorker.OutboxPollInterval = time.Second
}
if cfg.MessageActionConfirmWorker.OutboxBatchSize <= 0 {
cfg.MessageActionConfirmWorker.OutboxBatchSize = 100
}
if cfg.MessageActionConfirmWorker.OutboxLockTTL <= 0 {
cfg.MessageActionConfirmWorker.OutboxLockTTL = 30 * time.Second
}
if cfg.MessageActionConfirmWorker.OutboxMaxRetry <= 0 {
cfg.MessageActionConfirmWorker.OutboxMaxRetry = 8
}
if cfg.MessageActionConfirmWorker.PublishTimeout <= 0 {
cfg.MessageActionConfirmWorker.PublishTimeout = 5 * time.Second
}
rocketMQ, err := normalizeRocketMQConfig(cfg.RocketMQ)
if err != nil {
return Config{}, err
@ -397,6 +451,9 @@ func Load(path string) (Config, error) {
if cfg.InviteActivityRewardWorker.Enabled && !cfg.RocketMQ.UserOutbox.Enabled {
return Config{}, errors.New("invite activity reward worker requires rocketmq.user_outbox.enabled")
}
if cfg.MessageActionConfirmWorker.Enabled && (!cfg.RocketMQ.UserOutbox.Enabled || !cfg.RocketMQ.MessageActionOutbox.Enabled) {
return Config{}, errors.New("message action confirm worker requires rocketmq.user_outbox.enabled and rocketmq.message_action_outbox.enabled")
}
if cfg.LuckyGiftWorker.Enabled && !cfg.TencentIM.Enabled {
return Config{}, errors.New("lucky gift worker requires tencent_im.enabled")
}
@ -455,10 +512,25 @@ func normalizeRocketMQConfig(cfg RocketMQConfig) (RocketMQConfig, error) {
if cfg.UserOutbox.InviteActivityConsumerGroup = strings.TrimSpace(cfg.UserOutbox.InviteActivityConsumerGroup); cfg.UserOutbox.InviteActivityConsumerGroup == "" {
cfg.UserOutbox.InviteActivityConsumerGroup = defaults.UserOutbox.InviteActivityConsumerGroup
}
if cfg.UserOutbox.MessageActionConsumerGroup = strings.TrimSpace(cfg.UserOutbox.MessageActionConsumerGroup); cfg.UserOutbox.MessageActionConsumerGroup == "" {
cfg.UserOutbox.MessageActionConsumerGroup = defaults.UserOutbox.MessageActionConsumerGroup
}
if cfg.UserOutbox.ConsumerMaxReconsumeTimes <= 0 {
cfg.UserOutbox.ConsumerMaxReconsumeTimes = defaults.UserOutbox.ConsumerMaxReconsumeTimes
}
if cfg.RoomOutbox.Enabled || cfg.WalletOutbox.Enabled || cfg.UserOutbox.Enabled {
if cfg.MessageActionOutbox.Topic = strings.TrimSpace(cfg.MessageActionOutbox.Topic); cfg.MessageActionOutbox.Topic == "" {
cfg.MessageActionOutbox.Topic = defaults.MessageActionOutbox.Topic
}
if cfg.MessageActionOutbox.ProducerGroup = strings.TrimSpace(cfg.MessageActionOutbox.ProducerGroup); cfg.MessageActionOutbox.ProducerGroup == "" {
cfg.MessageActionOutbox.ProducerGroup = defaults.MessageActionOutbox.ProducerGroup
}
if cfg.MessageActionOutbox.SendTimeout <= 0 {
cfg.MessageActionOutbox.SendTimeout = defaults.MessageActionOutbox.SendTimeout
}
if cfg.MessageActionOutbox.Retry < 0 {
cfg.MessageActionOutbox.Retry = defaults.MessageActionOutbox.Retry
}
if cfg.RoomOutbox.Enabled || cfg.WalletOutbox.Enabled || cfg.UserOutbox.Enabled || cfg.MessageActionOutbox.Enabled {
cfg.Enabled = true
}
if cfg.Enabled {

View File

@ -77,9 +77,10 @@ type Settlement struct {
// Status is the H5 read model for current week leaderboard and configured rewards.
type Status struct {
Config Config
Entries []Entry
PeriodStartMS int64
PeriodEndMS int64
ServerTimeMS int64
Config Config
Entries []Entry
PreviousEntries []Entry
PeriodStartMS int64
PeriodEndMS int64
ServerTimeMS int64
}

View File

@ -0,0 +1,85 @@
package message
const (
ActionConfirmBusinessRoleInvitation = "role_invitation"
ActionConfirmSourceUserOutbox = "user_outbox"
ActionConfirmEventCreated = "ConfirmMessageCreated"
ActionConfirmStatusPending = "pending"
ActionConfirmStatusProcessing = "processing"
ActionConfirmStatusAccepted = "accepted"
ActionConfirmStatusRejected = "rejected"
ActionConfirmStatusExpired = "expired"
ActionConfirmStatusCanceled = "canceled"
ActionConfirmStatusFailed = "failed"
ActionConfirmActionAccept = "accept"
ActionConfirmActionReject = "reject"
)
// ActionConfirm 是 IM 操作按钮的后端状态机App 只能拿 confirm_id 操作,业务参数全部来自这行事实。
type ActionConfirm struct {
AppCode string
ConfirmID string
SourceName string
SourceEventID string
BusinessType string
BusinessSubtype string
BusinessID string
SenderUserID int64
ReceiverUserID int64
MessageText string
Status string
Action string
CommandID string
ProcessedAtMS int64
LockedBy string
LockUntilMS int64
ErrorMessage string
MetadataJSON string
CreatedAtMS int64
UpdatedAtMS int64
}
// ActionConfirmOutbox 是确认消息创建后交给 notice-service 投递 IM 的可靠事件箱。
type ActionConfirmOutbox struct {
AppCode string
EventID string
ConfirmID string
EventType string
Status string
WorkerID string
LockUntilMS int64
RetryCount int
NextRetryAtMS int64
LastError string
PayloadJSON string
CreatedAtMS int64
UpdatedAtMS int64
}
// RoleInvitationConfirmEvent 是 user_outbox 中 RoleInvitationCreated 的最小投影。
type RoleInvitationConfirmEvent struct {
SourceEventID string
InvitationID int64
InvitationType string
InviterUserID int64
TargetUserID int64
AgencyName string
Inviter RoleInvitationUserSnapshot
Target RoleInvitationUserSnapshot
CreatedAtMS int64
RawPayloadJSON string
ParentBDUserID int64
ParentLeaderUserID int64
}
// RoleInvitationUserSnapshot 是生成 IM 展示文案时使用的用户资料快照,不作为权限依据。
type RoleInvitationUserSnapshot struct {
UserID int64 `json:"user_id"`
DisplayUserID string `json:"display_user_id"`
Username string `json:"username"`
Avatar string `json:"avatar"`
}

View File

@ -120,11 +120,19 @@ func (s *Service) GetStatus(ctx context.Context, limit int32, nowMS int64) (doma
if limit <= 0 {
limit = 3
}
// H5 同时需要当前周期榜单和上一完整 UTC 周期的前三名;两个窗口都从 user-service 的周期新增 gift_value 聚合读取,避免 gateway/H5 误用累计亲密榜。
entries, err := s.rankSource.ListCPWeeklyRankEntries(ctx, config.RelationType, result.PeriodStartMS, result.PeriodEndMS, limit)
if err != nil {
return domain.Status{}, err
}
result.Entries = entries
previousStart, previousEnd := PreviousWeekWindow(now)
previousLimit := int32(3)
previousEntries, err := s.rankSource.ListCPWeeklyRankEntries(ctx, config.RelationType, previousStart.UnixMilli(), previousEnd.UnixMilli(), previousLimit)
if err != nil {
return domain.Status{}, err
}
result.PreviousEntries = previousEntries
return result, nil
}
@ -136,6 +144,7 @@ func (s *Service) ListSettlements(ctx context.Context, periodStartMS int64, peri
}
// ProcessSettlementBatch freezes the previous complete UTC week and grants each Top relationship reward to both users.
// The rank source already returns weekly delta score, so settlement only persists that frozen score and never re-reads cumulative intimacy.
func (s *Service) ProcessSettlementBatch(ctx context.Context, runID string, workerID string, batchSize int32) (claimed int32, processed int32, success int32, failure int32, hasMore bool, err error) {
if err := s.requireRepository(); err != nil {
return 0, 0, 0, 0, false, err

View File

@ -0,0 +1,142 @@
package cpweeklyrank_test
import (
"context"
"errors"
"strconv"
"testing"
"time"
"hyapp/pkg/appcode"
domain "hyapp/services/activity-service/internal/domain/cpweeklyrank"
service "hyapp/services/activity-service/internal/service/cpweeklyrank"
)
func TestGetStatusReturnsPreviousEntriesFromPreviousWeek(t *testing.T) {
now := time.Date(2026, 6, 24, 12, 0, 0, 0, time.UTC)
currentStart, currentEnd := service.WeekWindow(now)
previousStart, previousEnd := service.PreviousWeekWindow(now)
rankSource := &fakeCPWeeklyRankSource{
windows: map[[2]int64][]domain.Entry{
{currentStart.UnixMilli(), currentEnd.UnixMilli()}: cpWeeklyRankEntries(2, 1000),
{previousStart.UnixMilli(), previousEnd.UnixMilli()}: cpWeeklyRankEntries(5, 5000),
},
}
svc := service.New(&fakeCPWeeklyRankRepository{config: domain.Config{
AppCode: "lalu",
ActivityCode: domain.ActivityCode,
Enabled: true,
RelationType: "cp",
TopCount: 10,
}}, rankSource, nil)
svc.SetClock(func() time.Time { return now })
status, err := svc.GetStatus(appcode.WithContext(context.Background(), "lalu"), 1, 0)
if err != nil {
t.Fatalf("GetStatus failed: %v", err)
}
if len(status.Entries) != 1 {
t.Fatalf("current entries mismatch: %+v", status.Entries)
}
if len(status.PreviousEntries) != 3 {
t.Fatalf("previous entries should be limited to 3, got %+v", status.PreviousEntries)
}
if status.PreviousEntries[0].UserA.Username != "left-1" || status.PreviousEntries[0].UserA.Avatar != "https://avatar/left-1.png" ||
status.PreviousEntries[0].UserB.Username != "right-1" || status.PreviousEntries[0].UserB.Avatar != "https://avatar/right-1.png" ||
status.PreviousEntries[0].Score != 5001 {
t.Fatalf("previous entry should keep user snapshots and intimacy score: %+v", status.PreviousEntries[0])
}
if len(rankSource.calls) != 2 {
t.Fatalf("rank source call count mismatch: %+v", rankSource.calls)
}
if rankSource.calls[0].startMS != currentStart.UnixMilli() || rankSource.calls[0].endMS != currentEnd.UnixMilli() || rankSource.calls[0].limit != 1 {
t.Fatalf("current window call mismatch: %+v", rankSource.calls[0])
}
if rankSource.calls[1].startMS != previousStart.UnixMilli() || rankSource.calls[1].endMS != previousEnd.UnixMilli() || rankSource.calls[1].limit != 3 {
t.Fatalf("previous window call mismatch: %+v", rankSource.calls[1])
}
}
type fakeCPWeeklyRankRepository struct {
config domain.Config
}
func (r *fakeCPWeeklyRankRepository) GetCPWeeklyRankConfig(context.Context) (domain.Config, bool, error) {
return r.config, true, nil
}
func (r *fakeCPWeeklyRankRepository) UpdateCPWeeklyRankConfig(context.Context, domain.Config, int64) (domain.Config, error) {
return domain.Config{}, errors.New("not implemented")
}
func (r *fakeCPWeeklyRankRepository) ListCPWeeklyRankSettlements(context.Context, int64, int64) ([]domain.Settlement, error) {
return nil, errors.New("not implemented")
}
func (r *fakeCPWeeklyRankRepository) PrepareCPWeeklyRankSettlements(context.Context, domain.Config, []domain.Entry, int64, int64, int64) ([]domain.Settlement, error) {
return nil, errors.New("not implemented")
}
func (r *fakeCPWeeklyRankRepository) ListPendingCPWeeklyRankSettlements(context.Context, int64, int64, int) ([]domain.Settlement, error) {
return nil, errors.New("not implemented")
}
func (r *fakeCPWeeklyRankRepository) MarkCPWeeklyRankSettlementGranted(context.Context, string, string, int64) error {
return errors.New("not implemented")
}
func (r *fakeCPWeeklyRankRepository) MarkCPWeeklyRankSettlementFailed(context.Context, string, string, int64) error {
return errors.New("not implemented")
}
type fakeCPWeeklyRankSource struct {
calls []fakeCPWeeklyRankSourceCall
windows map[[2]int64][]domain.Entry
}
type fakeCPWeeklyRankSourceCall struct {
relationType string
startMS int64
endMS int64
limit int32
}
func (s *fakeCPWeeklyRankSource) ListCPWeeklyRankEntries(_ context.Context, relationType string, startMS int64, endMS int64, limit int32) ([]domain.Entry, error) {
s.calls = append(s.calls, fakeCPWeeklyRankSourceCall{
relationType: relationType,
startMS: startMS,
endMS: endMS,
limit: limit,
})
entries := append([]domain.Entry(nil), s.windows[[2]int64{startMS, endMS}]...)
if limit > 0 && len(entries) > int(limit) {
entries = entries[:limit]
}
return entries, nil
}
func cpWeeklyRankEntries(count int, baseScore int64) []domain.Entry {
entries := make([]domain.Entry, 0, count)
for index := 1; index <= count; index++ {
suffix := strconv.Itoa(index)
entries = append(entries, domain.Entry{
Rank: int64(index),
RelationshipID: "cp-rel-" + suffix,
RelationType: "cp",
Score: baseScore + int64(index),
UserA: domain.User{
UserID: int64(1000 + index),
DisplayUserID: "L" + suffix,
Username: "left-" + suffix,
Avatar: "https://avatar/left-" + suffix + ".png",
},
UserB: domain.User{
UserID: int64(2000 + index),
DisplayUserID: "R" + suffix,
Username: "right-" + suffix,
Avatar: "https://avatar/right-" + suffix + ".png",
},
})
}
return entries
}

View File

@ -0,0 +1,443 @@
package message
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"hyapp/pkg/idgen"
"hyapp/pkg/xerr"
messagedomain "hyapp/services/activity-service/internal/domain/message"
)
const (
actionConfirmDefaultLockTTL = 30 * time.Second
actionConfirmDefaultLimit = 50
actionConfirmMaxLimit = 100
)
// ActionConfirmRepository is the durable boundary for confirm rows and their IM outbox.
type ActionConfirmRepository interface {
SaveActionConfirmWithOutbox(ctx context.Context, confirm messagedomain.ActionConfirm, outbox messagedomain.ActionConfirmOutbox) (bool, messagedomain.ActionConfirm, error)
ClaimActionConfirm(ctx context.Context, confirmID string, actorUserID int64, action string, commandID string, workerID string, nowMS int64, lockUntilMS int64) (messagedomain.ActionConfirm, bool, error)
CompleteActionConfirm(ctx context.Context, confirmID string, commandID string, status string, action string, processedAtMS int64) (messagedomain.ActionConfirm, error)
ReleaseActionConfirm(ctx context.Context, confirmID string, commandID string, errorMessage string, nowMS int64) error
BatchGetActionConfirms(ctx context.Context, userID int64, confirmIDs []string) ([]messagedomain.ActionConfirm, error)
ListConversationActionConfirms(ctx context.Context, userID int64, peerUserID int64, limit int, cursorCreatedAtMS int64, cursorConfirmID string) ([]messagedomain.ActionConfirm, error)
ClaimPendingActionConfirmOutbox(ctx context.Context, workerID string, nowMS int64, lockUntilMS int64, batchSize int) ([]messagedomain.ActionConfirmOutbox, error)
MarkActionConfirmOutboxDelivered(ctx context.Context, eventID string, nowMS int64) error
MarkActionConfirmOutboxRetryable(ctx context.Context, eventID string, lastErr string, nextRetryAtMS int64, nowMS int64) error
}
// RoleInvitationClient is the owner-service boundary used when a confirm action maps to a role invitation.
type RoleInvitationClient interface {
ProcessRoleInvitation(ctx context.Context, input ProcessRoleInvitationInput) (RoleInvitationState, error)
GetRoleInvitation(ctx context.Context, actorUserID int64, invitationID int64) (RoleInvitationState, error)
}
// ProcessRoleInvitationInput is the normalized owner-service command assembled from a confirm row.
type ProcessRoleInvitationInput struct {
CommandID string
ActorUserID int64
InvitationID int64
Action string
Reason string
}
// RoleInvitationState is the small role-invitation projection needed to reconcile confirm status.
type RoleInvitationState struct {
InvitationID int64
InvitationType string
Status string
}
// ActionConfirmService owns IM confirm status and delegates business mutations back to owner services.
type ActionConfirmService struct {
cfg Config
repository ActionConfirmRepository
roleInvitationClient RoleInvitationClient
now func() time.Time
}
// NewActionConfirm creates the generic confirm-message service.
func NewActionConfirm(cfg Config, repository ActionConfirmRepository, roleInvitationClient RoleInvitationClient) *ActionConfirmService {
return &ActionConfirmService{cfg: cfg, repository: repository, roleInvitationClient: roleInvitationClient, now: time.Now}
}
// SetClock injects deterministic time for tests.
func (s *ActionConfirmService) SetClock(now func() time.Time) {
if now != nil {
s.now = now
}
}
// ConsumeRoleInvitationCreated materializes one user-service invitation fact into an actionable IM confirm.
func (s *ActionConfirmService) ConsumeRoleInvitationCreated(ctx context.Context, event messagedomain.RoleInvitationConfirmEvent) (messagedomain.ActionConfirm, bool, error) {
if err := s.requireRepository(); err != nil {
return messagedomain.ActionConfirm{}, false, err
}
if event.SourceEventID == "" || event.InvitationID <= 0 || event.InviterUserID <= 0 || event.TargetUserID <= 0 {
return messagedomain.ActionConfirm{}, false, xerr.New(xerr.InvalidArgument, "role invitation confirm event is incomplete")
}
subtype := strings.TrimSpace(event.InvitationType)
if subtype == "" {
return messagedomain.ActionConfirm{}, false, xerr.New(xerr.InvalidArgument, "invitation_type is required")
}
nowMS := firstPositiveInt64(event.CreatedAtMS, s.now().UnixMilli())
messageText := roleInvitationMessageText(event)
metadataJSON := strings.TrimSpace(event.RawPayloadJSON)
if metadataJSON == "" || !json.Valid([]byte(metadataJSON)) {
metadataJSON = "{}"
}
confirmID := idgen.New("cfm")
payloadJSON, err := confirmIMPayloadJSON(confirmID, event, messageText, nowMS)
if err != nil {
return messagedomain.ActionConfirm{}, false, err
}
confirm := messagedomain.ActionConfirm{
ConfirmID: confirmID,
SourceName: messagedomain.ActionConfirmSourceUserOutbox,
SourceEventID: event.SourceEventID,
BusinessType: messagedomain.ActionConfirmBusinessRoleInvitation,
BusinessSubtype: subtype,
BusinessID: strconv.FormatInt(event.InvitationID, 10),
SenderUserID: event.InviterUserID,
ReceiverUserID: event.TargetUserID,
MessageText: messageText,
Status: messagedomain.ActionConfirmStatusPending,
MetadataJSON: metadataJSON,
CreatedAtMS: nowMS,
UpdatedAtMS: nowMS,
}
outbox := messagedomain.ActionConfirmOutbox{
EventID: idgen.New("maout"),
ConfirmID: confirm.ConfirmID,
EventType: messagedomain.ActionConfirmEventCreated,
Status: "pending",
PayloadJSON: payloadJSON,
CreatedAtMS: nowMS,
UpdatedAtMS: nowMS,
}
created, saved, err := s.repository.SaveActionConfirmWithOutbox(ctx, confirm, outbox)
return saved, created, err
}
// AcceptActionConfirm executes the accept branch for the current receiver.
func (s *ActionConfirmService) AcceptActionConfirm(ctx context.Context, actorUserID int64, confirmID string, commandID string) (messagedomain.ActionConfirm, error) {
return s.processAction(ctx, actorUserID, confirmID, commandID, messagedomain.ActionConfirmActionAccept, "")
}
// RejectActionConfirm executes the reject branch for the current receiver.
func (s *ActionConfirmService) RejectActionConfirm(ctx context.Context, actorUserID int64, confirmID string, commandID string, reason string) (messagedomain.ActionConfirm, error) {
return s.processAction(ctx, actorUserID, confirmID, commandID, messagedomain.ActionConfirmActionReject, reason)
}
func (s *ActionConfirmService) processAction(ctx context.Context, actorUserID int64, confirmID string, commandID string, action string, reason string) (messagedomain.ActionConfirm, error) {
if err := s.requireRepository(); err != nil {
return messagedomain.ActionConfirm{}, err
}
if s.roleInvitationClient == nil {
return messagedomain.ActionConfirm{}, xerr.New(xerr.Unavailable, "role invitation client is not configured")
}
confirmID = strings.TrimSpace(confirmID)
commandID = strings.TrimSpace(commandID)
if actorUserID <= 0 || confirmID == "" || commandID == "" {
return messagedomain.ActionConfirm{}, xerr.New(xerr.InvalidArgument, "actor_user_id, confirm_id and command_id are required")
}
nowMS := s.now().UnixMilli()
claimed, ok, err := s.repository.ClaimActionConfirm(ctx, confirmID, actorUserID, action, commandID, s.workerID(), nowMS, nowMS+actionConfirmDefaultLockTTL.Milliseconds())
if err != nil || !ok {
return claimed, err
}
if isTerminalConfirmStatus(claimed.Status) {
return claimed, nil
}
invitationID, err := strconv.ParseInt(claimed.BusinessID, 10, 64)
if err != nil || invitationID <= 0 {
_ = s.repository.ReleaseActionConfirm(ctx, confirmID, commandID, "invalid business_id", s.now().UnixMilli())
return messagedomain.ActionConfirm{}, xerr.New(xerr.InvalidArgument, "confirm business_id is invalid")
}
if claimed.BusinessType != messagedomain.ActionConfirmBusinessRoleInvitation {
_ = s.repository.ReleaseActionConfirm(ctx, confirmID, commandID, "unsupported business_type", s.now().UnixMilli())
return messagedomain.ActionConfirm{}, xerr.New(xerr.InvalidArgument, "confirm business_type is unsupported")
}
state, err := s.roleInvitationClient.ProcessRoleInvitation(ctx, ProcessRoleInvitationInput{
CommandID: commandID,
ActorUserID: actorUserID,
InvitationID: invitationID,
Action: action,
Reason: strings.TrimSpace(reason),
})
if err != nil {
reconciled, reconcileErr := s.reconcileRoleInvitation(ctx, claimed, actorUserID, commandID, action)
if reconcileErr == nil && isTerminalConfirmStatus(reconciled.Status) {
return reconciled, nil
}
_ = s.repository.ReleaseActionConfirm(ctx, confirmID, commandID, err.Error(), s.now().UnixMilli())
return messagedomain.ActionConfirm{}, err
}
return s.repository.CompleteActionConfirm(ctx, confirmID, commandID, confirmStatusFromRoleInvitation(state.Status), actionFromRoleInvitationStatus(state.Status, action), s.now().UnixMilli())
}
func (s *ActionConfirmService) reconcileRoleInvitation(ctx context.Context, confirm messagedomain.ActionConfirm, actorUserID int64, commandID string, action string) (messagedomain.ActionConfirm, error) {
invitationID, err := strconv.ParseInt(confirm.BusinessID, 10, 64)
if err != nil || invitationID <= 0 {
return messagedomain.ActionConfirm{}, xerr.New(xerr.InvalidArgument, "confirm business_id is invalid")
}
state, err := s.roleInvitationClient.GetRoleInvitation(ctx, actorUserID, invitationID)
if err != nil {
return messagedomain.ActionConfirm{}, err
}
status := confirmStatusFromRoleInvitation(state.Status)
if !isTerminalConfirmStatus(status) {
return messagedomain.ActionConfirm{}, xerr.New(xerr.Conflict, "role invitation is not terminal")
}
return s.repository.CompleteActionConfirm(ctx, confirm.ConfirmID, commandID, status, actionFromRoleInvitationStatus(state.Status, action), s.now().UnixMilli())
}
// BatchGetActionConfirmStatus returns only confirms visible to the current user.
func (s *ActionConfirmService) BatchGetActionConfirmStatus(ctx context.Context, userID int64, confirmIDs []string) ([]messagedomain.ActionConfirm, error) {
if err := s.requireRepository(); err != nil {
return nil, err
}
if userID <= 0 {
return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
}
normalized := make([]string, 0, len(confirmIDs))
seen := make(map[string]struct{}, len(confirmIDs))
for _, confirmID := range confirmIDs {
confirmID = strings.TrimSpace(confirmID)
if confirmID == "" {
continue
}
if _, ok := seen[confirmID]; ok {
continue
}
seen[confirmID] = struct{}{}
normalized = append(normalized, confirmID)
}
if len(normalized) == 0 {
return []messagedomain.ActionConfirm{}, nil
}
if len(normalized) > 100 {
return nil, xerr.New(xerr.InvalidArgument, "confirm_ids is too large")
}
return s.repository.BatchGetActionConfirms(ctx, userID, normalized)
}
// ListConversationActionConfirms returns confirm states for one C2C peer conversation.
func (s *ActionConfirmService) ListConversationActionConfirms(ctx context.Context, userID int64, peerUserID int64, pageSize int32, pageToken string) ([]messagedomain.ActionConfirm, string, error) {
if err := s.requireRepository(); err != nil {
return nil, "", err
}
if userID <= 0 || peerUserID <= 0 {
return nil, "", xerr.New(xerr.InvalidArgument, "user_id and peer_user_id are required")
}
limit := int(pageSize)
if limit <= 0 {
limit = actionConfirmDefaultLimit
}
if limit > actionConfirmMaxLimit {
limit = actionConfirmMaxLimit
}
cursor, err := decodeActionConfirmCursor(pageToken)
if err != nil {
return nil, "", err
}
items, err := s.repository.ListConversationActionConfirms(ctx, userID, peerUserID, limit+1, cursor.CreatedAtMS, cursor.ConfirmID)
if err != nil {
return nil, "", err
}
if len(items) <= limit {
return items, "", nil
}
page := items[:limit]
return page, encodeActionConfirmCursor(page[len(page)-1]), nil
}
// ClaimPendingOutbox locks pending action-message outbox rows for the MQ publisher.
func (s *ActionConfirmService) ClaimPendingOutbox(ctx context.Context, batchSize int, lockTTL time.Duration) ([]messagedomain.ActionConfirmOutbox, error) {
if err := s.requireRepository(); err != nil {
return nil, err
}
if batchSize <= 0 {
batchSize = 100
}
if lockTTL <= 0 {
lockTTL = 30 * time.Second
}
nowMS := s.now().UnixMilli()
return s.repository.ClaimPendingActionConfirmOutbox(ctx, s.workerID(), nowMS, nowMS+lockTTL.Milliseconds(), batchSize)
}
func (s *ActionConfirmService) MarkOutboxDelivered(ctx context.Context, eventID string) error {
return s.repository.MarkActionConfirmOutboxDelivered(ctx, eventID, s.now().UnixMilli())
}
func (s *ActionConfirmService) MarkOutboxRetryable(ctx context.Context, eventID string, cause error, nextRetryAtMS int64) error {
message := ""
if cause != nil {
message = cause.Error()
}
return s.repository.MarkActionConfirmOutboxRetryable(ctx, eventID, message, nextRetryAtMS, s.now().UnixMilli())
}
func (s *ActionConfirmService) requireRepository() error {
if s == nil || s.repository == nil {
return xerr.New(xerr.Unavailable, "action confirm repository is not configured")
}
return nil
}
func (s *ActionConfirmService) workerID() string {
nodeID := strings.TrimSpace(s.cfg.NodeID)
if nodeID == "" {
nodeID = "activity"
}
return "message-action-confirm-" + nodeID
}
type confirmIMPayload struct {
Type string `json:"type"`
Version int `json:"version"`
ConfirmID string `json:"confirm_id"`
BusinessType string `json:"business_type"`
BusinessSubtype string `json:"business_subtype"`
SenderUserID string `json:"sender_user_id"`
ReceiverUserID string `json:"receiver_user_id"`
Msg string `json:"msg"`
Actions []confirmIMAction `json:"actions"`
CreatedAtMS int64 `json:"created_at_ms"`
}
type confirmIMAction struct {
Action string `json:"action"`
Text string `json:"text"`
}
func confirmIMPayloadJSON(confirmID string, event messagedomain.RoleInvitationConfirmEvent, messageText string, createdAtMS int64) (string, error) {
payload := confirmIMPayload{
Type: "im_confirm",
Version: 1,
ConfirmID: confirmID,
BusinessType: messagedomain.ActionConfirmBusinessRoleInvitation,
BusinessSubtype: event.InvitationType,
SenderUserID: strconv.FormatInt(event.InviterUserID, 10),
ReceiverUserID: strconv.FormatInt(event.TargetUserID, 10),
Msg: messageText,
Actions: []confirmIMAction{
{Action: messagedomain.ActionConfirmActionReject, Text: "Reject"},
{Action: messagedomain.ActionConfirmActionAccept, Text: "Accept"},
},
CreatedAtMS: createdAtMS,
}
raw, err := json.Marshal(payload)
if err != nil {
return "", err
}
return string(raw), nil
}
func roleInvitationMessageText(event messagedomain.RoleInvitationConfirmEvent) string {
name := firstNonEmptyText(event.Inviter.Username, event.Inviter.DisplayUserID, strconv.FormatInt(event.InviterUserID, 10))
agencyName := firstNonEmptyText(event.AgencyName, "guild")
switch event.InvitationType {
case "host":
return fmt.Sprintf("%s has invited you to join the %s guild", name, agencyName)
case "agency":
return fmt.Sprintf("%s has invited you to create the %s guild", name, agencyName)
case "bd":
return fmt.Sprintf("%s has invited you to become BD", name)
default:
return fmt.Sprintf("%s has sent you a confirmation request", name)
}
}
func confirmStatusFromRoleInvitation(status string) string {
switch strings.TrimSpace(status) {
case "accepted":
return messagedomain.ActionConfirmStatusAccepted
case "rejected":
return messagedomain.ActionConfirmStatusRejected
case "cancelled":
return messagedomain.ActionConfirmStatusCanceled
case "expired":
return messagedomain.ActionConfirmStatusExpired
default:
return messagedomain.ActionConfirmStatusPending
}
}
func actionFromRoleInvitationStatus(status string, fallback string) string {
switch strings.TrimSpace(status) {
case "accepted":
return messagedomain.ActionConfirmActionAccept
case "rejected":
return messagedomain.ActionConfirmActionReject
default:
return fallback
}
}
func isTerminalConfirmStatus(status string) bool {
switch status {
case messagedomain.ActionConfirmStatusAccepted, messagedomain.ActionConfirmStatusRejected, messagedomain.ActionConfirmStatusExpired, messagedomain.ActionConfirmStatusCanceled, messagedomain.ActionConfirmStatusFailed:
return true
default:
return false
}
}
func firstNonEmptyText(values ...string) string {
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
return value
}
}
return ""
}
func firstPositiveInt64(values ...int64) int64 {
for _, value := range values {
if value > 0 {
return value
}
}
return 0
}
type actionConfirmCursor struct {
CreatedAtMS int64 `json:"created_at_ms"`
ConfirmID string `json:"confirm_id"`
}
func encodeActionConfirmCursor(confirm messagedomain.ActionConfirm) string {
raw, err := json.Marshal(actionConfirmCursor{CreatedAtMS: confirm.CreatedAtMS, ConfirmID: confirm.ConfirmID})
if err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(raw)
}
func decodeActionConfirmCursor(value string) (actionConfirmCursor, error) {
value = strings.TrimSpace(value)
if value == "" {
return actionConfirmCursor{}, nil
}
raw, err := base64.RawURLEncoding.DecodeString(value)
if err != nil {
return actionConfirmCursor{}, xerr.New(xerr.PageTokenInvalid, "page token is invalid")
}
var cursor actionConfirmCursor
if err := json.Unmarshal(raw, &cursor); err != nil {
return actionConfirmCursor{}, xerr.New(xerr.PageTokenInvalid, "page token is invalid")
}
if cursor.CreatedAtMS <= 0 || strings.TrimSpace(cursor.ConfirmID) == "" {
return actionConfirmCursor{}, xerr.New(xerr.PageTokenInvalid, "page token is invalid")
}
return cursor, nil
}

View File

@ -0,0 +1,387 @@
package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
messagedomain "hyapp/services/activity-service/internal/domain/message"
)
// SaveActionConfirmWithOutbox creates the confirm row and IM outbox in one transaction.
func (r *Repository) SaveActionConfirmWithOutbox(ctx context.Context, confirm messagedomain.ActionConfirm, outbox messagedomain.ActionConfirmOutbox) (bool, messagedomain.ActionConfirm, error) {
if r == nil || r.db == nil {
return false, messagedomain.ActionConfirm{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
app := appcode.FromContext(ctx)
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return false, messagedomain.ActionConfirm{}, err
}
defer tx.Rollback()
confirm.AppCode = app
outbox.AppCode = app
_, err = tx.ExecContext(ctx, `
INSERT INTO message_action_confirms (
app_code, confirm_id, source_name, source_event_id, business_type, business_subtype, business_id,
sender_user_id, receiver_user_id, message_text, status, action, command_id, processed_at_ms,
locked_by, lock_until_ms, error_message, metadata_json, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '', 0, '', 0, '', CAST(? AS JSON), ?, ?)`,
app, confirm.ConfirmID, confirm.SourceName, confirm.SourceEventID, confirm.BusinessType, confirm.BusinessSubtype, confirm.BusinessID,
confirm.SenderUserID, confirm.ReceiverUserID, confirm.MessageText, confirm.Status, nullJSONText(confirm.MetadataJSON), confirm.CreatedAtMS, confirm.UpdatedAtMS,
)
if err != nil {
if isMySQLDuplicate(err) {
existing, existingErr := getActionConfirmBySource(ctx, tx, confirm.SourceName, confirm.SourceEventID)
return false, existing, existingErr
}
return false, messagedomain.ActionConfirm{}, err
}
_, err = tx.ExecContext(ctx, `
INSERT INTO message_action_outbox (
app_code, event_id, confirm_id, event_type, status, worker_id, lock_until_ms,
retry_count, next_retry_at_ms, last_error, payload_json, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, '', 0, 0, 0, '', CAST(? AS JSON), ?, ?)`,
app, outbox.EventID, confirm.ConfirmID, outbox.EventType, outbox.Status, outbox.PayloadJSON, outbox.CreatedAtMS, outbox.UpdatedAtMS,
)
if err != nil {
if isMySQLDuplicate(err) {
existing, existingErr := getActionConfirmBySource(ctx, tx, confirm.SourceName, confirm.SourceEventID)
return false, existing, existingErr
}
return false, messagedomain.ActionConfirm{}, err
}
if err := tx.Commit(); err != nil {
return false, messagedomain.ActionConfirm{}, err
}
return true, confirm, nil
}
// ClaimActionConfirm moves a pending or expired-processing confirm into processing under row lock.
func (r *Repository) ClaimActionConfirm(ctx context.Context, confirmID string, actorUserID int64, action string, commandID string, workerID string, nowMS int64, lockUntilMS int64) (messagedomain.ActionConfirm, bool, error) {
if r == nil || r.db == nil {
return messagedomain.ActionConfirm{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return messagedomain.ActionConfirm{}, false, err
}
defer tx.Rollback()
confirm, err := getActionConfirmByIDForUpdate(ctx, tx, confirmID)
if err != nil {
return messagedomain.ActionConfirm{}, false, err
}
if confirm.ReceiverUserID != actorUserID {
return messagedomain.ActionConfirm{}, false, xerr.New(xerr.PermissionDenied, "action confirm is not visible to actor")
}
if isActionConfirmTerminal(confirm.Status) {
if err := tx.Commit(); err != nil {
return messagedomain.ActionConfirm{}, false, err
}
return confirm, false, nil
}
if confirm.Status == messagedomain.ActionConfirmStatusProcessing && confirm.LockUntilMS > nowMS {
return messagedomain.ActionConfirm{}, false, xerr.New(xerr.Conflict, "action confirm is processing")
}
_, err = tx.ExecContext(ctx, `
UPDATE message_action_confirms
SET status = ?, action = '', command_id = ?, locked_by = ?, lock_until_ms = ?, error_message = '', updated_at_ms = ?
WHERE app_code = ? AND confirm_id = ?`,
messagedomain.ActionConfirmStatusProcessing, commandID, workerID, lockUntilMS, nowMS, appcode.FromContext(ctx), confirmID,
)
if err != nil {
return messagedomain.ActionConfirm{}, false, err
}
if err := tx.Commit(); err != nil {
return messagedomain.ActionConfirm{}, false, err
}
confirm.Status = messagedomain.ActionConfirmStatusProcessing
confirm.CommandID = commandID
confirm.LockedBy = workerID
confirm.LockUntilMS = lockUntilMS
confirm.UpdatedAtMS = nowMS
return confirm, true, nil
}
// CompleteActionConfirm writes the terminal state after the owner service accepts the business command.
func (r *Repository) CompleteActionConfirm(ctx context.Context, confirmID string, commandID string, status string, action string, processedAtMS int64) (messagedomain.ActionConfirm, error) {
result, err := r.db.ExecContext(ctx, `
UPDATE message_action_confirms
SET status = ?, action = ?, processed_at_ms = ?, locked_by = '', lock_until_ms = 0, error_message = '', updated_at_ms = ?
WHERE app_code = ? AND confirm_id = ? AND command_id = ?`,
status, action, processedAtMS, processedAtMS, appcode.FromContext(ctx), confirmID, commandID,
)
if err != nil {
return messagedomain.ActionConfirm{}, err
}
if affected, err := result.RowsAffected(); err != nil {
return messagedomain.ActionConfirm{}, err
} else if affected == 0 {
return messagedomain.ActionConfirm{}, xerr.New(xerr.Conflict, "action confirm processing lock is lost")
}
return r.getActionConfirmByID(ctx, confirmID)
}
// ReleaseActionConfirm returns a failed processing attempt to pending so the user can retry with a new command.
func (r *Repository) ReleaseActionConfirm(ctx context.Context, confirmID string, commandID string, errorMessage string, nowMS int64) error {
_, err := r.db.ExecContext(ctx, `
UPDATE message_action_confirms
SET status = ?, locked_by = '', lock_until_ms = 0, error_message = ?, updated_at_ms = ?
WHERE app_code = ? AND confirm_id = ? AND command_id = ? AND status = ?`,
messagedomain.ActionConfirmStatusPending, truncateMessageActionError(errorMessage), nowMS, appcode.FromContext(ctx), confirmID, commandID, messagedomain.ActionConfirmStatusProcessing,
)
return err
}
// BatchGetActionConfirms returns current-user-visible confirm rows for a bounded ID list.
func (r *Repository) BatchGetActionConfirms(ctx context.Context, userID int64, confirmIDs []string) ([]messagedomain.ActionConfirm, error) {
if len(confirmIDs) == 0 {
return []messagedomain.ActionConfirm{}, nil
}
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(confirmIDs)), ",")
args := []any{appcode.FromContext(ctx), userID}
for _, confirmID := range confirmIDs {
args = append(args, confirmID)
}
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
SELECT %s
FROM message_action_confirms
WHERE app_code = ? AND receiver_user_id = ? AND confirm_id IN (`+placeholders+`)
ORDER BY created_at_ms DESC, confirm_id DESC`, actionConfirmColumns), args...)
if err != nil {
return nil, err
}
defer rows.Close()
return scanActionConfirmRows(rows)
}
// ListConversationActionConfirms reads one cursor page for confirms sent by a specific peer.
func (r *Repository) ListConversationActionConfirms(ctx context.Context, userID int64, peerUserID int64, limit int, cursorCreatedAtMS int64, cursorConfirmID string) ([]messagedomain.ActionConfirm, error) {
args := []any{appcode.FromContext(ctx), userID, peerUserID}
cursorSQL := ""
if cursorCreatedAtMS > 0 || cursorConfirmID != "" {
cursorSQL = " AND (created_at_ms < ? OR (created_at_ms = ? AND confirm_id < ?))"
args = append(args, cursorCreatedAtMS, cursorCreatedAtMS, cursorConfirmID)
}
args = append(args, limit)
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
SELECT %s
FROM message_action_confirms
WHERE app_code = ? AND receiver_user_id = ? AND sender_user_id = ?`+cursorSQL+`
ORDER BY created_at_ms DESC, confirm_id DESC
LIMIT ?`, actionConfirmColumns), args...)
if err != nil {
return nil, err
}
defer rows.Close()
return scanActionConfirmRows(rows)
}
// ClaimPendingActionConfirmOutbox locks pending/retryable confirm IM facts for RocketMQ publishing.
func (r *Repository) ClaimPendingActionConfirmOutbox(ctx context.Context, workerID string, nowMS int64, lockUntilMS int64, batchSize int) ([]messagedomain.ActionConfirmOutbox, error) {
if batchSize <= 0 {
batchSize = 100
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
records := make([]messagedomain.ActionConfirmOutbox, 0, batchSize)
branches := []struct {
query string
args []any
}{
{query: actionOutboxClaimQuery("status = 'pending' AND next_retry_at_ms <= ?"), args: []any{appcode.FromContext(ctx), nowMS}},
{query: actionOutboxClaimQuery("status = 'retryable' AND next_retry_at_ms <= ?"), args: []any{appcode.FromContext(ctx), nowMS}},
{query: actionOutboxClaimQuery("status = 'running' AND lock_until_ms <= ?"), args: []any{appcode.FromContext(ctx), nowMS}},
}
for _, branch := range branches {
remaining := batchSize - len(records)
if remaining <= 0 {
break
}
args := append(append([]any{}, branch.args...), remaining)
items, err := queryActionConfirmOutboxRows(ctx, tx, branch.query, args...)
if err != nil {
return nil, err
}
records = append(records, items...)
}
for _, record := range records {
if _, err := tx.ExecContext(ctx, `
UPDATE message_action_outbox
SET status = 'running', worker_id = ?, lock_until_ms = ?, updated_at_ms = ?
WHERE app_code = ? AND event_id = ?`,
workerID, lockUntilMS, nowMS, record.AppCode, record.EventID,
); err != nil {
return nil, err
}
}
if err := tx.Commit(); err != nil {
return nil, err
}
for index := range records {
records[index].Status = "running"
records[index].WorkerID = workerID
records[index].LockUntilMS = lockUntilMS
}
return records, nil
}
// MarkActionConfirmOutboxDelivered records that RocketMQ accepted the confirm IM fact.
func (r *Repository) MarkActionConfirmOutboxDelivered(ctx context.Context, eventID string, nowMS int64) error {
_, err := r.db.ExecContext(ctx, `
UPDATE message_action_outbox
SET status = 'delivered', worker_id = '', lock_until_ms = 0, last_error = '', updated_at_ms = ?
WHERE app_code = ? AND event_id = ?`,
nowMS, appcode.FromContext(ctx), eventID,
)
return err
}
// MarkActionConfirmOutboxRetryable releases a failed publish with a bounded retry delay.
func (r *Repository) MarkActionConfirmOutboxRetryable(ctx context.Context, eventID string, lastErr string, nextRetryAtMS int64, nowMS int64) error {
_, err := r.db.ExecContext(ctx, `
UPDATE message_action_outbox
SET status = 'retryable', worker_id = '', lock_until_ms = 0, retry_count = retry_count + 1,
next_retry_at_ms = ?, last_error = ?, updated_at_ms = ?
WHERE app_code = ? AND event_id = ? AND status = 'running'`,
nextRetryAtMS, truncateMessageActionError(lastErr), nowMS, appcode.FromContext(ctx), eventID,
)
return err
}
const actionConfirmColumns = `
app_code, confirm_id, source_name, source_event_id, business_type, business_subtype, business_id,
sender_user_id, receiver_user_id, message_text, status, action, command_id, processed_at_ms,
locked_by, lock_until_ms, error_message, COALESCE(CAST(metadata_json AS CHAR), ''), created_at_ms, updated_at_ms`
func (r *Repository) getActionConfirmByID(ctx context.Context, confirmID string) (messagedomain.ActionConfirm, error) {
return scanActionConfirm(r.db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT %s FROM message_action_confirms WHERE app_code = ? AND confirm_id = ?`,
actionConfirmColumns), appcode.FromContext(ctx), confirmID))
}
func getActionConfirmByIDForUpdate(ctx context.Context, tx *sql.Tx, confirmID string) (messagedomain.ActionConfirm, error) {
return scanActionConfirm(tx.QueryRowContext(ctx, fmt.Sprintf(`
SELECT %s FROM message_action_confirms WHERE app_code = ? AND confirm_id = ? FOR UPDATE`,
actionConfirmColumns), appcode.FromContext(ctx), confirmID))
}
func getActionConfirmBySource(ctx context.Context, tx *sql.Tx, sourceName string, sourceEventID string) (messagedomain.ActionConfirm, error) {
return scanActionConfirm(tx.QueryRowContext(ctx, fmt.Sprintf(`
SELECT %s FROM message_action_confirms WHERE app_code = ? AND source_name = ? AND source_event_id = ?`,
actionConfirmColumns), appcode.FromContext(ctx), sourceName, sourceEventID))
}
func scanActionConfirm(scanner inboxScanner) (messagedomain.ActionConfirm, error) {
var confirm messagedomain.ActionConfirm
err := scanner.Scan(
&confirm.AppCode,
&confirm.ConfirmID,
&confirm.SourceName,
&confirm.SourceEventID,
&confirm.BusinessType,
&confirm.BusinessSubtype,
&confirm.BusinessID,
&confirm.SenderUserID,
&confirm.ReceiverUserID,
&confirm.MessageText,
&confirm.Status,
&confirm.Action,
&confirm.CommandID,
&confirm.ProcessedAtMS,
&confirm.LockedBy,
&confirm.LockUntilMS,
&confirm.ErrorMessage,
&confirm.MetadataJSON,
&confirm.CreatedAtMS,
&confirm.UpdatedAtMS,
)
if errors.Is(err, sql.ErrNoRows) {
return messagedomain.ActionConfirm{}, xerr.New(xerr.NotFound, "action confirm not found")
}
return confirm, err
}
func scanActionConfirmRows(rows *sql.Rows) ([]messagedomain.ActionConfirm, error) {
items := make([]messagedomain.ActionConfirm, 0)
for rows.Next() {
item, err := scanActionConfirm(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func actionOutboxClaimQuery(condition string) string {
return `
SELECT app_code, event_id, confirm_id, event_type, status, worker_id, lock_until_ms,
retry_count, next_retry_at_ms, last_error, CAST(payload_json AS CHAR), created_at_ms, updated_at_ms
FROM message_action_outbox
WHERE app_code = ? AND ` + condition + `
ORDER BY created_at_ms ASC, event_id ASC
LIMIT ? FOR UPDATE SKIP LOCKED`
}
func queryActionConfirmOutboxRows(ctx context.Context, tx *sql.Tx, query string, args ...any) ([]messagedomain.ActionConfirmOutbox, error) {
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
records := make([]messagedomain.ActionConfirmOutbox, 0)
for rows.Next() {
var record messagedomain.ActionConfirmOutbox
if err := rows.Scan(
&record.AppCode,
&record.EventID,
&record.ConfirmID,
&record.EventType,
&record.Status,
&record.WorkerID,
&record.LockUntilMS,
&record.RetryCount,
&record.NextRetryAtMS,
&record.LastError,
&record.PayloadJSON,
&record.CreatedAtMS,
&record.UpdatedAtMS,
); err != nil {
return nil, err
}
records = append(records, record)
}
return records, rows.Err()
}
func isActionConfirmTerminal(status string) bool {
switch status {
case messagedomain.ActionConfirmStatusAccepted, messagedomain.ActionConfirmStatusRejected, messagedomain.ActionConfirmStatusExpired, messagedomain.ActionConfirmStatusCanceled, messagedomain.ActionConfirmStatusFailed:
return true
default:
return false
}
}
func nullJSONText(value string) any {
if strings.TrimSpace(value) == "" {
return "{}"
}
return value
}
func truncateMessageActionError(message string) string {
message = strings.TrimSpace(message)
if len(message) <= 512 {
return message
}
return message[:512]
}

View File

@ -0,0 +1,94 @@
package grpc
import (
"context"
activityv1 "hyapp.local/api/proto/activity/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
messagedomain "hyapp/services/activity-service/internal/domain/message"
messageservice "hyapp/services/activity-service/internal/service/message"
)
// ActionConfirmServer adapts generic IM action confirmations to gRPC.
type ActionConfirmServer struct {
activityv1.UnimplementedMessageActionConfirmServiceServer
svc *messageservice.ActionConfirmService
}
func NewActionConfirmServer(svc *messageservice.ActionConfirmService) *ActionConfirmServer {
return &ActionConfirmServer{svc: svc}
}
func (s *ActionConfirmServer) AcceptActionConfirm(ctx context.Context, req *activityv1.AcceptActionConfirmRequest) (*activityv1.AcceptActionConfirmResponse, error) {
if s.svc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "action confirm service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
confirm, err := s.svc.AcceptActionConfirm(ctx, req.GetActorUserId(), req.GetConfirmId(), req.GetCommandId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &activityv1.AcceptActionConfirmResponse{Confirm: actionConfirmProto(confirm)}, nil
}
func (s *ActionConfirmServer) RejectActionConfirm(ctx context.Context, req *activityv1.RejectActionConfirmRequest) (*activityv1.RejectActionConfirmResponse, error) {
if s.svc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "action confirm service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
confirm, err := s.svc.RejectActionConfirm(ctx, req.GetActorUserId(), req.GetConfirmId(), req.GetCommandId(), req.GetReason())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &activityv1.RejectActionConfirmResponse{Confirm: actionConfirmProto(confirm)}, nil
}
func (s *ActionConfirmServer) BatchGetActionConfirmStatus(ctx context.Context, req *activityv1.BatchGetActionConfirmStatusRequest) (*activityv1.BatchGetActionConfirmStatusResponse, error) {
if s.svc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "action confirm service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
items, err := s.svc.BatchGetActionConfirmStatus(ctx, req.GetUserId(), req.GetConfirmIds())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &activityv1.BatchGetActionConfirmStatusResponse{Items: make([]*activityv1.ActionConfirm, 0, len(items))}
for _, item := range items {
resp.Items = append(resp.Items, actionConfirmProto(item))
}
return resp, nil
}
func (s *ActionConfirmServer) ListConversationActionConfirms(ctx context.Context, req *activityv1.ListConversationActionConfirmsRequest) (*activityv1.ListConversationActionConfirmsResponse, error) {
if s.svc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "action confirm service is not configured"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
items, nextPageToken, err := s.svc.ListConversationActionConfirms(ctx, req.GetUserId(), req.GetPeerUserId(), req.GetPageSize(), req.GetPageToken())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &activityv1.ListConversationActionConfirmsResponse{Items: make([]*activityv1.ActionConfirm, 0, len(items)), NextPageToken: nextPageToken}
for _, item := range items {
resp.Items = append(resp.Items, actionConfirmProto(item))
}
return resp, nil
}
func actionConfirmProto(confirm messagedomain.ActionConfirm) *activityv1.ActionConfirm {
return &activityv1.ActionConfirm{
ConfirmId: confirm.ConfirmID,
BusinessType: confirm.BusinessType,
BusinessSubtype: confirm.BusinessSubtype,
BusinessId: confirm.BusinessID,
SenderUserId: confirm.SenderUserID,
ReceiverUserId: confirm.ReceiverUserID,
Status: confirm.Status,
Action: confirm.Action,
ProcessedAtMs: confirm.ProcessedAtMS,
CreatedAtMs: confirm.CreatedAtMS,
UpdatedAtMs: confirm.UpdatedAtMS,
}
}

View File

@ -30,11 +30,12 @@ func (s *CPWeeklyRankServer) GetCPWeeklyRankStatus(ctx context.Context, req *act
return nil, xerr.ToGRPCError(err)
}
return &activityv1.GetCPWeeklyRankStatusResponse{
Config: cpWeeklyRankConfigToProto(status.Config),
Entries: cpWeeklyRankEntriesToProto(status.Entries),
PeriodStartMs: status.PeriodStartMS,
PeriodEndMs: status.PeriodEndMS,
ServerTimeMs: status.ServerTimeMS,
Config: cpWeeklyRankConfigToProto(status.Config),
Entries: cpWeeklyRankEntriesToProto(status.Entries),
PreviousEntries: cpWeeklyRankEntriesToProto(status.PreviousEntries),
PeriodStartMs: status.PeriodStartMS,
PeriodEndMs: status.PeriodEndMS,
ServerTimeMs: status.ServerTimeMS,
}, nil
}

View File

@ -98,6 +98,7 @@ func New(cfg config.Config) (*App, error) {
var appRegistryClient client.AppRegistryClient = client.NewGRPCAppRegistryClient(userConn)
var walletClient client.WalletClient = client.NewGRPCWalletClient(walletConn)
var messageClient client.MessageInboxClient = client.NewGRPCMessageInboxClient(activityConn)
var messageActionClient client.MessageActionConfirmClient = client.NewGRPCMessageActionConfirmClient(activityConn)
var taskClient client.TaskClient = client.NewGRPCTaskClient(activityConn)
var growthLevelClient client.GrowthLevelClient = client.NewGRPCGrowthLevelClient(activityConn)
var achievementClient client.AchievementClient = client.NewGRPCAchievementClient(activityConn)
@ -173,6 +174,7 @@ func New(cfg config.Config) (*App, error) {
handler.SetAppRegistryClient(appRegistryClient)
handler.SetWalletClient(walletClient)
handler.SetMessageInboxClient(messageClient)
handler.SetMessageActionConfirmClient(messageActionClient)
handler.SetTaskClient(taskClient)
handler.SetGrowthLevelClient(growthLevelClient)
handler.SetAchievementClient(achievementClient)

View File

@ -16,15 +16,32 @@ type MessageInboxClient interface {
DeleteInboxMessage(ctx context.Context, req *activityv1.DeleteInboxMessageRequest) (*activityv1.DeleteInboxMessageResponse, error)
}
// MessageActionConfirmClient abstracts gateway access to generic IM action confirmations.
type MessageActionConfirmClient interface {
AcceptActionConfirm(ctx context.Context, req *activityv1.AcceptActionConfirmRequest) (*activityv1.AcceptActionConfirmResponse, error)
RejectActionConfirm(ctx context.Context, req *activityv1.RejectActionConfirmRequest) (*activityv1.RejectActionConfirmResponse, error)
BatchGetActionConfirmStatus(ctx context.Context, req *activityv1.BatchGetActionConfirmStatusRequest) (*activityv1.BatchGetActionConfirmStatusResponse, error)
ListConversationActionConfirms(ctx context.Context, req *activityv1.ListConversationActionConfirmsRequest) (*activityv1.ListConversationActionConfirmsResponse, error)
}
type grpcMessageInboxClient struct {
client activityv1.MessageInboxServiceClient
}
type grpcMessageActionConfirmClient struct {
client activityv1.MessageActionConfirmServiceClient
}
// NewGRPCMessageInboxClient builds an inbox client backed by activity-service MessageInboxService.
func NewGRPCMessageInboxClient(conn *grpc.ClientConn) MessageInboxClient {
return &grpcMessageInboxClient{client: activityv1.NewMessageInboxServiceClient(conn)}
}
// NewGRPCMessageActionConfirmClient builds an action-confirm client backed by activity-service.
func NewGRPCMessageActionConfirmClient(conn *grpc.ClientConn) MessageActionConfirmClient {
return &grpcMessageActionConfirmClient{client: activityv1.NewMessageActionConfirmServiceClient(conn)}
}
func (c *grpcMessageInboxClient) ListMessageTabs(ctx context.Context, req *activityv1.ListMessageTabsRequest) (*activityv1.ListMessageTabsResponse, error) {
return c.client.ListMessageTabs(ctx, req)
}
@ -44,3 +61,19 @@ func (c *grpcMessageInboxClient) MarkInboxSectionRead(ctx context.Context, req *
func (c *grpcMessageInboxClient) DeleteInboxMessage(ctx context.Context, req *activityv1.DeleteInboxMessageRequest) (*activityv1.DeleteInboxMessageResponse, error) {
return c.client.DeleteInboxMessage(ctx, req)
}
func (c *grpcMessageActionConfirmClient) AcceptActionConfirm(ctx context.Context, req *activityv1.AcceptActionConfirmRequest) (*activityv1.AcceptActionConfirmResponse, error) {
return c.client.AcceptActionConfirm(ctx, req)
}
func (c *grpcMessageActionConfirmClient) RejectActionConfirm(ctx context.Context, req *activityv1.RejectActionConfirmRequest) (*activityv1.RejectActionConfirmResponse, error) {
return c.client.RejectActionConfirm(ctx, req)
}
func (c *grpcMessageActionConfirmClient) BatchGetActionConfirmStatus(ctx context.Context, req *activityv1.BatchGetActionConfirmStatusRequest) (*activityv1.BatchGetActionConfirmStatusResponse, error) {
return c.client.BatchGetActionConfirmStatus(ctx, req)
}
func (c *grpcMessageActionConfirmClient) ListConversationActionConfirms(ctx context.Context, req *activityv1.ListConversationActionConfirmsRequest) (*activityv1.ListConversationActionConfirmsResponse, error) {
return c.client.ListConversationActionConfirms(ctx, req)
}

View File

@ -41,6 +41,7 @@ type UserProfileClient interface {
UpdateUserProfile(ctx context.Context, req *userv1.UpdateUserProfileRequest) (*userv1.UpdateUserProfileResponse, error)
UpdateUserProfileBackground(ctx context.Context, req *userv1.UpdateUserProfileBackgroundRequest) (*userv1.UpdateUserProfileBackgroundResponse, error)
ChangeUserCountry(ctx context.Context, req *userv1.ChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error)
AdminChangeUserCountry(ctx context.Context, req *userv1.AdminChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error)
CreateManagerUserBlock(ctx context.Context, req *userv1.CreateManagerUserBlockRequest) (*userv1.CreateManagerUserBlockResponse, error)
ListManagerUserBlocks(ctx context.Context, req *userv1.ListManagerUserBlocksRequest) (*userv1.ListManagerUserBlocksResponse, error)
UnblockManagerUser(ctx context.Context, req *userv1.UnblockManagerUserRequest) (*userv1.UnblockManagerUserResponse, error)
@ -338,6 +339,10 @@ func (c *grpcUserProfileClient) ChangeUserCountry(ctx context.Context, req *user
return c.client.ChangeUserCountry(ctx, req)
}
func (c *grpcUserProfileClient) AdminChangeUserCountry(ctx context.Context, req *userv1.AdminChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) {
return c.client.AdminChangeUserCountry(ctx, req)
}
func (c *grpcUserProfileClient) CreateManagerUserBlock(ctx context.Context, req *userv1.CreateManagerUserBlockRequest) (*userv1.CreateManagerUserBlockResponse, error) {
return c.client.CreateManagerUserBlock(ctx, req)
}

View File

@ -17,8 +17,12 @@ type WalletClient interface {
GetWalletValueSummary(ctx context.Context, req *walletv1.GetWalletValueSummaryRequest) (*walletv1.GetWalletValueSummaryResponse, error)
GetUserGiftWall(ctx context.Context, req *walletv1.GetUserGiftWallRequest) (*walletv1.GetUserGiftWallResponse, error)
ListRechargeProducts(ctx context.Context, req *walletv1.ListRechargeProductsRequest) (*walletv1.ListRechargeProductsResponse, error)
ListThirdPartyPaymentChannels(ctx context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error)
ListH5RechargeOptions(ctx context.Context, req *walletv1.H5RechargeOptionsRequest) (*walletv1.H5RechargeOptionsResponse, error)
CreateH5RechargeOrder(ctx context.Context, req *walletv1.CreateH5RechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error)
CreateTemporaryRechargeOrder(ctx context.Context, req *walletv1.CreateTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error)
GetTemporaryRechargeOrder(ctx context.Context, req *walletv1.GetTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error)
ListTemporaryRechargeOrders(ctx context.Context, req *walletv1.ListTemporaryRechargeOrdersRequest) (*walletv1.ListTemporaryRechargeOrdersResponse, error)
SubmitH5RechargeTx(ctx context.Context, req *walletv1.SubmitH5RechargeTxRequest) (*walletv1.H5RechargeOrderResponse, error)
GetH5RechargeOrder(ctx context.Context, req *walletv1.GetH5RechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error)
HandleMifapayNotify(ctx context.Context, req *walletv1.HandleMifapayNotifyRequest) (*walletv1.HandleMifapayNotifyResponse, error)
@ -92,6 +96,10 @@ func (c *grpcWalletClient) ListRechargeProducts(ctx context.Context, req *wallet
return c.client.ListRechargeProducts(ctx, req)
}
func (c *grpcWalletClient) ListThirdPartyPaymentChannels(ctx context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error) {
return c.client.ListThirdPartyPaymentChannels(ctx, req)
}
func (c *grpcWalletClient) ListH5RechargeOptions(ctx context.Context, req *walletv1.H5RechargeOptionsRequest) (*walletv1.H5RechargeOptionsResponse, error) {
return c.client.ListH5RechargeOptions(ctx, req)
}
@ -100,6 +108,18 @@ func (c *grpcWalletClient) CreateH5RechargeOrder(ctx context.Context, req *walle
return c.client.CreateH5RechargeOrder(ctx, req)
}
func (c *grpcWalletClient) CreateTemporaryRechargeOrder(ctx context.Context, req *walletv1.CreateTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) {
return c.client.CreateTemporaryRechargeOrder(ctx, req)
}
func (c *grpcWalletClient) GetTemporaryRechargeOrder(ctx context.Context, req *walletv1.GetTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) {
return c.client.GetTemporaryRechargeOrder(ctx, req)
}
func (c *grpcWalletClient) ListTemporaryRechargeOrders(ctx context.Context, req *walletv1.ListTemporaryRechargeOrdersRequest) (*walletv1.ListTemporaryRechargeOrdersResponse, error) {
return c.client.ListTemporaryRechargeOrders(ctx, req)
}
func (c *grpcWalletClient) SubmitH5RechargeTx(ctx context.Context, req *walletv1.SubmitH5RechargeTxRequest) (*walletv1.H5RechargeOrderResponse, error) {
return c.client.SubmitH5RechargeTx(ctx, req)
}

View File

@ -9,12 +9,13 @@ import (
)
type cpWeeklyRankStatusData struct {
Enabled bool `json:"enabled"`
Period cpWeeklyRankPeriodData `json:"period"`
Rewards []cpWeeklyRankRewardData `json:"rewards"`
Entries []cpWeeklyRankEntryData `json:"entries"`
ServerTimeMS int64 `json:"server_time_ms"`
RelationType string `json:"relation_type"`
Enabled bool `json:"enabled"`
Period cpWeeklyRankPeriodData `json:"period"`
Rewards []cpWeeklyRankRewardData `json:"rewards"`
Entries []cpWeeklyRankEntryData `json:"entries"`
PreviousEntries []cpWeeklyRankEntryData `json:"previous_entries"`
ServerTimeMS int64 `json:"server_time_ms"`
RelationType string `json:"relation_type"`
}
type cpWeeklyRankPeriodData struct {
@ -81,12 +82,13 @@ func (h *Handler) getCPWeeklyRankStatus(writer http.ResponseWriter, request *htt
func cpWeeklyRankStatusFromProto(resp *activityv1.GetCPWeeklyRankStatusResponse, groups map[int64]checkinResourceGroupData) cpWeeklyRankStatusData {
config := resp.GetConfig()
return cpWeeklyRankStatusData{
Enabled: config.GetEnabled(),
RelationType: config.GetRelationType(),
Period: cpWeeklyRankPeriodData{StartMS: resp.GetPeriodStartMs(), EndMS: resp.GetPeriodEndMs()},
Rewards: cpWeeklyRankRewardsFromProto(config.GetRewards(), groups),
Entries: cpWeeklyRankEntriesFromProto(resp.GetEntries()),
ServerTimeMS: resp.GetServerTimeMs(),
Enabled: config.GetEnabled(),
RelationType: config.GetRelationType(),
Period: cpWeeklyRankPeriodData{StartMS: resp.GetPeriodStartMs(), EndMS: resp.GetPeriodEndMs()},
Rewards: cpWeeklyRankRewardsFromProto(config.GetRewards(), groups),
Entries: cpWeeklyRankEntriesFromProto(resp.GetEntries()),
PreviousEntries: cpWeeklyRankEntriesFromProto(resp.GetPreviousEntries()),
ServerTimeMS: resp.GetServerTimeMs(),
}
}

View File

@ -141,6 +141,10 @@ func (f *fakeInviteActivityUserProfileClient) ChangeUserCountry(context.Context,
return &userv1.ChangeUserCountryResponse{}, nil
}
func (f *fakeInviteActivityUserProfileClient) AdminChangeUserCountry(context.Context, *userv1.AdminChangeUserCountryRequest) (*userv1.ChangeUserCountryResponse, error) {
return &userv1.ChangeUserCountryResponse{}, nil
}
func (f *fakeInviteActivityUserProfileClient) CreateManagerUserBlock(context.Context, *userv1.CreateManagerUserBlockRequest) (*userv1.CreateManagerUserBlockResponse, error) {
return &userv1.CreateManagerUserBlockResponse{}, nil
}

View File

@ -10,6 +10,8 @@ import (
"hyapp/services/gateway-service/internal/appconfig"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
userv1 "hyapp.local/api/proto/user/v1"
)
type appBootstrapBottomTab struct {
@ -128,13 +130,16 @@ func (h *Handler) listAppBanners(writer http.ResponseWriter, request *http.Reque
return
}
items, err := h.appConfigReader.ListBanners(request.Context(), appconfig.BannerQuery{
query := appconfig.BannerQuery{
AppCode: appcode.FromContext(request.Context()),
DisplayScope: displayScope,
Platform: httpkit.FirstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"),
RegionID: optionalInt64Query(request, "region_id"),
Country: httpkit.FirstQueryOrHeader(request, "country", "X-Country-Code", "X-App-Country"),
})
}
if !h.applyAuthenticatedBannerAudience(writer, request, &query) {
return
}
items, err := h.appConfigReader.ListBanners(request.Context(), query)
if err != nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
@ -143,6 +148,44 @@ func (h *Handler) listAppBanners(writer http.ResponseWriter, request *http.Reque
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
}
func (h *Handler) applyAuthenticatedBannerAudience(writer http.ResponseWriter, request *http.Request, query *appconfig.BannerQuery) bool {
viewerUserID := auth.UserIDFromContext(request.Context())
if viewerUserID == 0 {
// banner 是公开接口,但未登录请求不能用 region_id/country 伪装区域身份;空区域只会命中全局 banner。
return true
}
if h.userProfileClient == nil {
// 登录态区域必须以 user-service 的用户事实为准,缺少依赖时不能退回客户端传参。
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return false
}
resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: viewerUserID,
})
if err != nil {
// user-service 查询失败时返回上游错误,避免用客户端 region_id/country 打开错误区域的 banner。
httpkit.WriteRPCError(writer, request, err)
return false
}
user := resp.GetUser()
if user == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return false
}
if user.GetAppCode() != "" && appcode.Normalize(user.GetAppCode()) != query.AppCode {
// token 所属租户和用户资料租户不一致时拒绝返回区域配置,防止跨 App 读取 banner 策略。
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
return false
}
// 已登录用户的区域由后端用户资料覆盖;客户端保留 region_id/country 只做老版本参数兼容,不参与裁决。
query.RegionID = user.GetRegionId()
query.Country = strings.TrimSpace(user.GetCountry())
return true
}
// listSplashScreens 返回后台 APP配置/开屏配置 中当前可见的开屏列表;客户端按列表顺序取最高优先级配置展示。
func (h *Handler) listSplashScreens(writer http.ResponseWriter, request *http.Request) {
if h.appConfigReader == nil {

View File

@ -28,25 +28,28 @@ type ObjectUploader interface {
// Handler owns app platform endpoints such as bootstrap, config, upload and device token binding.
// It does not own user identity or resource semantics; those remain in their owner services/packages.
type Handler struct {
appConfigReader ConfigReader
userAuthClient client.UserAuthClient
userDeviceClient client.UserDeviceClient
objectUploader ObjectUploader
appConfigReader ConfigReader
userAuthClient client.UserAuthClient
userProfileClient client.UserProfileClient
userDeviceClient client.UserDeviceClient
objectUploader ObjectUploader
}
type Config struct {
AppConfigReader ConfigReader
UserAuthClient client.UserAuthClient
UserDeviceClient client.UserDeviceClient
ObjectUploader ObjectUploader
AppConfigReader ConfigReader
UserAuthClient client.UserAuthClient
UserProfileClient client.UserProfileClient
UserDeviceClient client.UserDeviceClient
ObjectUploader ObjectUploader
}
func New(config Config) *Handler {
return &Handler{
appConfigReader: config.AppConfigReader,
userAuthClient: config.UserAuthClient,
userDeviceClient: config.UserDeviceClient,
objectUploader: config.ObjectUploader,
appConfigReader: config.AppConfigReader,
userAuthClient: config.UserAuthClient,
userProfileClient: config.UserProfileClient,
userDeviceClient: config.UserDeviceClient,
objectUploader: config.ObjectUploader,
}
}

View File

@ -38,6 +38,7 @@ type Handler struct {
appRegistryClient client.AppRegistryClient
walletClient client.WalletClient
messageClient client.MessageInboxClient
messageActionClient client.MessageActionConfirmClient
taskClient client.TaskClient
growthLevelClient client.GrowthLevelClient
achievementClient client.AchievementClient
@ -213,6 +214,11 @@ func (h *Handler) SetMessageInboxClient(messageClient client.MessageInboxClient)
h.messageClient = messageClient
}
// SetMessageActionConfirmClient 注入 activity-service action-confirm client。
func (h *Handler) SetMessageActionConfirmClient(messageActionClient client.MessageActionConfirmClient) {
h.messageActionClient = messageActionClient
}
// SetTaskClient 注入 activity-service task client。
func (h *Handler) SetTaskClient(taskClient client.TaskClient) {
h.taskClient = taskClient

Some files were not shown because too many files have changed in this diff Show More