Compare commits
31 Commits
f453ede08b
...
bf2735d068
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf2735d068 | ||
|
|
35060cc1fc | ||
|
|
72ec2bfcc9 | ||
|
|
e385704a7d | ||
|
|
a93c3f24d1 | ||
|
|
7cd08d0967 | ||
|
|
9b4447e158 | ||
|
|
13f07d800f | ||
|
|
d57419c99e | ||
|
|
1e3d628f57 | ||
|
|
f8e34770d3 | ||
|
|
4028928123 | ||
|
|
6fd0f03895 | ||
|
|
9c547faf0e | ||
|
|
9a19267c6a | ||
|
|
b97e5f0011 | ||
|
|
ac6672f398 | ||
|
|
9c39d33bf2 | ||
|
|
dba6035fbe | ||
|
|
93250e5a7b | ||
|
|
059de783ac | ||
|
|
d51b735783 | ||
|
|
d947ebbfa3 | ||
|
|
3cf62395e0 | ||
|
|
ac767d1c7e | ||
|
|
f455df9192 | ||
|
|
1ac0cecf63 | ||
|
|
395470f3ba | ||
|
|
ebc99a097d | ||
|
|
6cfe45f73a | ||
|
|
ee86af4c14 |
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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
@ -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 返回扣费成功并落到房间后的状态结果。
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
@ -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);
|
||||
|
||||
@ -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
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
477
docs/flutter对接/通用确认消息Flutter对接.md
Normal file
477
docs/flutter对接/通用确认消息Flutter对接.md
Normal 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` 时不弹业务成功,只刷新状态后展示最终状态。
|
||||
422
docs/通用确认消息方案.md
Normal file
422
docs/通用确认消息方案.md
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
相关 IM:App 打开会话或回到前台时,收集当前页面内的 `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": ""
|
||||
}
|
||||
```
|
||||
|
||||
相关 IM:App 如果无法从本地消息列表快速收集 `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
63
pkg/messagemq/messages.go
Normal 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
|
||||
}
|
||||
@ -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"),
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -25,6 +25,7 @@ TOPICS=(
|
||||
"hyapp_robot_room_outbox"
|
||||
"hyapp_room_rocket_launch"
|
||||
"hyapp_user_outbox"
|
||||
"hyapp_message_action_outbox"
|
||||
"hyapp_game_outbox"
|
||||
)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
861
scripts/ops/refresh_robot_appearance.py
Executable file
861
scripts/ops/refresh_robot_appearance.py
Executable 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())
|
||||
@ -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),
|
||||
|
||||
@ -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` | 编辑礼物、资源绑定和价格 |
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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=
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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: "金币扣除"},
|
||||
|
||||
@ -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)
|
||||
|
||||
192
server/admin/internal/modules/cpweeklyrank/handler.go
Normal file
192
server/admin/internal/modules/cpweeklyrank/handler.go
Normal 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
|
||||
}
|
||||
16
server/admin/internal/modules/cpweeklyrank/routes.go
Normal file
16
server/admin/internal/modules/cpweeklyrank/routes.go
Normal 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)
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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")),
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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()) != "" {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
304
server/admin/internal/modules/resource/identity_auto_grant.go
Normal file
304
server/admin/internal/modules/resource/identity_auto_grant.go
Normal 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, ",")
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
61
server/admin/internal/modules/riskconfig/handler.go
Normal file
61
server/admin/internal/modules/riskconfig/handler.go
Normal 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)
|
||||
}
|
||||
16
server/admin/internal/modules/riskconfig/routes.go
Normal file
16
server/admin/internal/modules/riskconfig/routes.go
Normal 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)
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
54
server/admin/migrations/058_room_pin_navigation.sql
Normal file
54
server/admin/migrations/058_room_pin_navigation.sql
Normal 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';
|
||||
@ -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';
|
||||
46
server/admin/migrations/060_risk_config_navigation.sql
Normal file
46
server/admin/migrations/060_risk_config_navigation.sql
Normal 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';
|
||||
@ -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';
|
||||
57
server/admin/migrations/062_cp_weekly_rank_navigation.sql
Normal file
57
server/admin/migrations/062_cp_weekly_rank_navigation.sql
Normal 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';
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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';
|
||||
153
services/activity-service/internal/app/action_confirm_mq.go
Normal file
153
services/activity-service/internal/app/action_confirm_mq.go
Normal 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()
|
||||
}
|
||||
@ -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})
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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 != "" {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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]
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user