diff --git a/Makefile b/Makefile index 3ee565c2..6abd376f 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,10 @@ build: go build ./server/admin/... # `admin` 在本机直接启动后台管理后端;依赖默认从 server/admin/configs/config.yaml 读取。 +# 启动前自动杀掉占用 13100 端口的旧进程,避免 bind: address already in use。 +ADMIN_PORT ?= 13100 admin: + @pids=$$(lsof -ti tcp:$(ADMIN_PORT)) && [ -n "$$pids" ] && kill -9 $$pids 2>/dev/null || true cd server/admin && go run ./cmd/server -config $(ADMIN_CONFIG) # `admin-up` 先拉起后台本地依赖,再在本机启动后台管理后端。 @@ -50,6 +53,7 @@ admin-deps: # `admin-bootstrap` 显式执行后台初始化种子后启动服务。 admin-bootstrap: + @pids=$$(lsof -ti tcp:$(ADMIN_PORT)) && [ -n "$$pids" ] && kill -9 $$pids 2>/dev/null || true cd server/admin && go run ./cmd/server -config $(ADMIN_CONFIG) -bootstrap # `admin-test` 只跑后台管理后端测试。 diff --git a/api/proto/wallet/v1/wallet.pb.go b/api/proto/wallet/v1/wallet.pb.go index 82195b0c..e67719a0 100644 --- a/api/proto/wallet/v1/wallet.pb.go +++ b/api/proto/wallet/v1/wallet.pb.go @@ -591,6 +591,241 @@ func (x *AdminCreditAssetResponse) GetBalance() *AssetBalance { return nil } +// AdminCreditCoinSellerStockRequest 是后台给 active 币商专用库存入账的账务命令。 +type AdminCreditCoinSellerStockRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CommandId string `protobuf:"bytes,1,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"` + SellerUserId int64 `protobuf:"varint,2,opt,name=seller_user_id,json=sellerUserId,proto3" json:"seller_user_id,omitempty"` + StockType string `protobuf:"bytes,3,opt,name=stock_type,json=stockType,proto3" json:"stock_type,omitempty"` + CoinAmount int64 `protobuf:"varint,4,opt,name=coin_amount,json=coinAmount,proto3" json:"coin_amount,omitempty"` + PaidCurrencyCode string `protobuf:"bytes,5,opt,name=paid_currency_code,json=paidCurrencyCode,proto3" json:"paid_currency_code,omitempty"` + PaidAmountMicro int64 `protobuf:"varint,6,opt,name=paid_amount_micro,json=paidAmountMicro,proto3" json:"paid_amount_micro,omitempty"` + PaymentRef string `protobuf:"bytes,7,opt,name=payment_ref,json=paymentRef,proto3" json:"payment_ref,omitempty"` + EvidenceRef string `protobuf:"bytes,8,opt,name=evidence_ref,json=evidenceRef,proto3" json:"evidence_ref,omitempty"` + OperatorUserId int64 `protobuf:"varint,9,opt,name=operator_user_id,json=operatorUserId,proto3" json:"operator_user_id,omitempty"` + Reason string `protobuf:"bytes,10,opt,name=reason,proto3" json:"reason,omitempty"` + AppCode string `protobuf:"bytes,11,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` +} + +func (x *AdminCreditCoinSellerStockRequest) Reset() { + *x = AdminCreditCoinSellerStockRequest{} + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminCreditCoinSellerStockRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminCreditCoinSellerStockRequest) ProtoMessage() {} + +func (x *AdminCreditCoinSellerStockRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[7] + 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 AdminCreditCoinSellerStockRequest.ProtoReflect.Descriptor instead. +func (*AdminCreditCoinSellerStockRequest) Descriptor() ([]byte, []int) { + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{7} +} + +func (x *AdminCreditCoinSellerStockRequest) GetCommandId() string { + if x != nil { + return x.CommandId + } + return "" +} + +func (x *AdminCreditCoinSellerStockRequest) GetSellerUserId() int64 { + if x != nil { + return x.SellerUserId + } + return 0 +} + +func (x *AdminCreditCoinSellerStockRequest) GetStockType() string { + if x != nil { + return x.StockType + } + return "" +} + +func (x *AdminCreditCoinSellerStockRequest) GetCoinAmount() int64 { + if x != nil { + return x.CoinAmount + } + return 0 +} + +func (x *AdminCreditCoinSellerStockRequest) GetPaidCurrencyCode() string { + if x != nil { + return x.PaidCurrencyCode + } + return "" +} + +func (x *AdminCreditCoinSellerStockRequest) GetPaidAmountMicro() int64 { + if x != nil { + return x.PaidAmountMicro + } + return 0 +} + +func (x *AdminCreditCoinSellerStockRequest) GetPaymentRef() string { + if x != nil { + return x.PaymentRef + } + return "" +} + +func (x *AdminCreditCoinSellerStockRequest) GetEvidenceRef() string { + if x != nil { + return x.EvidenceRef + } + return "" +} + +func (x *AdminCreditCoinSellerStockRequest) GetOperatorUserId() int64 { + if x != nil { + return x.OperatorUserId + } + return 0 +} + +func (x *AdminCreditCoinSellerStockRequest) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *AdminCreditCoinSellerStockRequest) GetAppCode() string { + if x != nil { + return x.AppCode + } + return "" +} + +type AdminCreditCoinSellerStockResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TransactionId string `protobuf:"bytes,1,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` + SellerUserId int64 `protobuf:"varint,2,opt,name=seller_user_id,json=sellerUserId,proto3" json:"seller_user_id,omitempty"` + StockType string `protobuf:"bytes,3,opt,name=stock_type,json=stockType,proto3" json:"stock_type,omitempty"` + CoinAmount int64 `protobuf:"varint,4,opt,name=coin_amount,json=coinAmount,proto3" json:"coin_amount,omitempty"` + PaidCurrencyCode string `protobuf:"bytes,5,opt,name=paid_currency_code,json=paidCurrencyCode,proto3" json:"paid_currency_code,omitempty"` + PaidAmountMicro int64 `protobuf:"varint,6,opt,name=paid_amount_micro,json=paidAmountMicro,proto3" json:"paid_amount_micro,omitempty"` + CountsAsSellerRecharge bool `protobuf:"varint,7,opt,name=counts_as_seller_recharge,json=countsAsSellerRecharge,proto3" json:"counts_as_seller_recharge,omitempty"` + BalanceAfter int64 `protobuf:"varint,8,opt,name=balance_after,json=balanceAfter,proto3" json:"balance_after,omitempty"` + CreatedAtMs int64 `protobuf:"varint,9,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` +} + +func (x *AdminCreditCoinSellerStockResponse) Reset() { + *x = AdminCreditCoinSellerStockResponse{} + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminCreditCoinSellerStockResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminCreditCoinSellerStockResponse) ProtoMessage() {} + +func (x *AdminCreditCoinSellerStockResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[8] + 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 AdminCreditCoinSellerStockResponse.ProtoReflect.Descriptor instead. +func (*AdminCreditCoinSellerStockResponse) Descriptor() ([]byte, []int) { + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{8} +} + +func (x *AdminCreditCoinSellerStockResponse) GetTransactionId() string { + if x != nil { + return x.TransactionId + } + return "" +} + +func (x *AdminCreditCoinSellerStockResponse) GetSellerUserId() int64 { + if x != nil { + return x.SellerUserId + } + return 0 +} + +func (x *AdminCreditCoinSellerStockResponse) GetStockType() string { + if x != nil { + return x.StockType + } + return "" +} + +func (x *AdminCreditCoinSellerStockResponse) GetCoinAmount() int64 { + if x != nil { + return x.CoinAmount + } + return 0 +} + +func (x *AdminCreditCoinSellerStockResponse) GetPaidCurrencyCode() string { + if x != nil { + return x.PaidCurrencyCode + } + return "" +} + +func (x *AdminCreditCoinSellerStockResponse) GetPaidAmountMicro() int64 { + if x != nil { + return x.PaidAmountMicro + } + return 0 +} + +func (x *AdminCreditCoinSellerStockResponse) GetCountsAsSellerRecharge() bool { + if x != nil { + return x.CountsAsSellerRecharge + } + return false +} + +func (x *AdminCreditCoinSellerStockResponse) GetBalanceAfter() int64 { + if x != nil { + return x.BalanceAfter + } + return 0 +} + +func (x *AdminCreditCoinSellerStockResponse) GetCreatedAtMs() int64 { + if x != nil { + return x.CreatedAtMs + } + return 0 +} + // TransferCoinFromSellerRequest 是币商给玩家转普通金币的内部账务命令。 type TransferCoinFromSellerRequest struct { state protoimpl.MessageState @@ -610,7 +845,7 @@ type TransferCoinFromSellerRequest struct { func (x *TransferCoinFromSellerRequest) Reset() { *x = TransferCoinFromSellerRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[7] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -622,7 +857,7 @@ func (x *TransferCoinFromSellerRequest) String() string { func (*TransferCoinFromSellerRequest) ProtoMessage() {} func (x *TransferCoinFromSellerRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[7] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -635,7 +870,7 @@ func (x *TransferCoinFromSellerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TransferCoinFromSellerRequest.ProtoReflect.Descriptor instead. func (*TransferCoinFromSellerRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{7} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{9} } func (x *TransferCoinFromSellerRequest) GetCommandId() string { @@ -714,7 +949,7 @@ type TransferCoinFromSellerResponse struct { func (x *TransferCoinFromSellerResponse) Reset() { *x = TransferCoinFromSellerResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[8] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -726,7 +961,7 @@ func (x *TransferCoinFromSellerResponse) String() string { func (*TransferCoinFromSellerResponse) ProtoMessage() {} func (x *TransferCoinFromSellerResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[8] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -739,7 +974,7 @@ func (x *TransferCoinFromSellerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TransferCoinFromSellerResponse.ProtoReflect.Descriptor instead. func (*TransferCoinFromSellerResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{8} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{10} } func (x *TransferCoinFromSellerResponse) GetTransactionId() string { @@ -842,7 +1077,7 @@ type Resource struct { func (x *Resource) Reset() { *x = Resource{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[9] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -854,7 +1089,7 @@ func (x *Resource) String() string { func (*Resource) ProtoMessage() {} func (x *Resource) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[9] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -867,7 +1102,7 @@ func (x *Resource) ProtoReflect() protoreflect.Message { // Deprecated: Use Resource.ProtoReflect.Descriptor instead. func (*Resource) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{9} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{11} } func (x *Resource) GetAppCode() string { @@ -1031,7 +1266,7 @@ type ResourceGroupItem struct { func (x *ResourceGroupItem) Reset() { *x = ResourceGroupItem{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[10] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1043,7 +1278,7 @@ func (x *ResourceGroupItem) String() string { func (*ResourceGroupItem) ProtoMessage() {} func (x *ResourceGroupItem) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[10] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1056,7 +1291,7 @@ func (x *ResourceGroupItem) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceGroupItem.ProtoReflect.Descriptor instead. func (*ResourceGroupItem) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{10} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{12} } func (x *ResourceGroupItem) GetGroupItemId() int64 { @@ -1164,7 +1399,7 @@ type ResourceGroup struct { func (x *ResourceGroup) Reset() { *x = ResourceGroup{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[11] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1176,7 +1411,7 @@ func (x *ResourceGroup) String() string { func (*ResourceGroup) ProtoMessage() {} func (x *ResourceGroup) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[11] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1189,7 +1424,7 @@ func (x *ResourceGroup) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceGroup.ProtoReflect.Descriptor instead. func (*ResourceGroup) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{11} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{13} } func (x *ResourceGroup) GetAppCode() string { @@ -1307,7 +1542,7 @@ type GiftConfig struct { func (x *GiftConfig) Reset() { *x = GiftConfig{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[12] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1319,7 +1554,7 @@ func (x *GiftConfig) String() string { func (*GiftConfig) ProtoMessage() {} func (x *GiftConfig) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[12] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1332,7 +1567,7 @@ func (x *GiftConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use GiftConfig.ProtoReflect.Descriptor instead. func (*GiftConfig) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{12} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{14} } func (x *GiftConfig) GetAppCode() string { @@ -1511,7 +1746,7 @@ type UserResourceEntitlement struct { func (x *UserResourceEntitlement) Reset() { *x = UserResourceEntitlement{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[13] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1523,7 +1758,7 @@ func (x *UserResourceEntitlement) String() string { func (*UserResourceEntitlement) ProtoMessage() {} func (x *UserResourceEntitlement) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[13] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1536,7 +1771,7 @@ func (x *UserResourceEntitlement) ProtoReflect() protoreflect.Message { // Deprecated: Use UserResourceEntitlement.ProtoReflect.Descriptor instead. func (*UserResourceEntitlement) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{13} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{15} } func (x *UserResourceEntitlement) GetAppCode() string { @@ -1649,7 +1884,7 @@ type ResourceGrantItem struct { func (x *ResourceGrantItem) Reset() { *x = ResourceGrantItem{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[14] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1661,7 +1896,7 @@ func (x *ResourceGrantItem) String() string { func (*ResourceGrantItem) ProtoMessage() {} func (x *ResourceGrantItem) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[14] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1674,7 +1909,7 @@ func (x *ResourceGrantItem) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceGrantItem.ProtoReflect.Descriptor instead. func (*ResourceGrantItem) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{14} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{16} } func (x *ResourceGrantItem) GetGrantItemId() int64 { @@ -1769,7 +2004,7 @@ type ResourceGrant struct { func (x *ResourceGrant) Reset() { *x = ResourceGrant{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[15] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1781,7 +2016,7 @@ func (x *ResourceGrant) String() string { func (*ResourceGrant) ProtoMessage() {} func (x *ResourceGrant) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[15] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1794,7 +2029,7 @@ func (x *ResourceGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceGrant.ProtoReflect.Descriptor instead. func (*ResourceGrant) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{15} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{17} } func (x *ResourceGrant) GetAppCode() string { @@ -1904,7 +2139,7 @@ type ResourceGroupItemInput struct { func (x *ResourceGroupItemInput) Reset() { *x = ResourceGroupItemInput{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[16] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1916,7 +2151,7 @@ func (x *ResourceGroupItemInput) String() string { func (*ResourceGroupItemInput) ProtoMessage() {} func (x *ResourceGroupItemInput) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[16] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1929,7 +2164,7 @@ func (x *ResourceGroupItemInput) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceGroupItemInput.ProtoReflect.Descriptor instead. func (*ResourceGroupItemInput) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{16} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{18} } func (x *ResourceGroupItemInput) GetResourceId() int64 { @@ -1998,7 +2233,7 @@ type ListResourcesRequest struct { func (x *ListResourcesRequest) Reset() { *x = ListResourcesRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[17] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2010,7 +2245,7 @@ func (x *ListResourcesRequest) String() string { func (*ListResourcesRequest) ProtoMessage() {} func (x *ListResourcesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[17] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2023,7 +2258,7 @@ func (x *ListResourcesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListResourcesRequest.ProtoReflect.Descriptor instead. func (*ListResourcesRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{17} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{19} } func (x *ListResourcesRequest) GetRequestId() string { @@ -2093,7 +2328,7 @@ type ListResourcesResponse struct { func (x *ListResourcesResponse) Reset() { *x = ListResourcesResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[18] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2105,7 +2340,7 @@ func (x *ListResourcesResponse) String() string { func (*ListResourcesResponse) ProtoMessage() {} func (x *ListResourcesResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[18] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2118,7 +2353,7 @@ func (x *ListResourcesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListResourcesResponse.ProtoReflect.Descriptor instead. func (*ListResourcesResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{18} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{20} } func (x *ListResourcesResponse) GetResources() []*Resource { @@ -2147,7 +2382,7 @@ type GetResourceRequest struct { func (x *GetResourceRequest) Reset() { *x = GetResourceRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[19] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2159,7 +2394,7 @@ func (x *GetResourceRequest) String() string { func (*GetResourceRequest) ProtoMessage() {} func (x *GetResourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[19] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2172,7 +2407,7 @@ func (x *GetResourceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetResourceRequest.ProtoReflect.Descriptor instead. func (*GetResourceRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{19} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{21} } func (x *GetResourceRequest) GetRequestId() string { @@ -2206,7 +2441,7 @@ type GetResourceResponse struct { func (x *GetResourceResponse) Reset() { *x = GetResourceResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[20] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2218,7 +2453,7 @@ func (x *GetResourceResponse) String() string { func (*GetResourceResponse) ProtoMessage() {} func (x *GetResourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[20] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2231,7 +2466,7 @@ func (x *GetResourceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetResourceResponse.ProtoReflect.Descriptor instead. func (*GetResourceResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{20} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{22} } func (x *GetResourceResponse) GetResource() *Resource { @@ -2267,7 +2502,7 @@ type CreateResourceRequest struct { func (x *CreateResourceRequest) Reset() { *x = CreateResourceRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[21] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2279,7 +2514,7 @@ func (x *CreateResourceRequest) String() string { func (*CreateResourceRequest) ProtoMessage() {} func (x *CreateResourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[21] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2292,7 +2527,7 @@ func (x *CreateResourceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateResourceRequest.ProtoReflect.Descriptor instead. func (*CreateResourceRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{21} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{23} } func (x *CreateResourceRequest) GetRequestId() string { @@ -2441,7 +2676,7 @@ type UpdateResourceRequest struct { func (x *UpdateResourceRequest) Reset() { *x = UpdateResourceRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[22] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2453,7 +2688,7 @@ func (x *UpdateResourceRequest) String() string { func (*UpdateResourceRequest) ProtoMessage() {} func (x *UpdateResourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[22] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2466,7 +2701,7 @@ func (x *UpdateResourceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateResourceRequest.ProtoReflect.Descriptor instead. func (*UpdateResourceRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{22} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{24} } func (x *UpdateResourceRequest) GetRequestId() string { @@ -2609,7 +2844,7 @@ type SetResourceStatusRequest struct { func (x *SetResourceStatusRequest) Reset() { *x = SetResourceStatusRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[23] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2621,7 +2856,7 @@ func (x *SetResourceStatusRequest) String() string { func (*SetResourceStatusRequest) ProtoMessage() {} func (x *SetResourceStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[23] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2634,7 +2869,7 @@ func (x *SetResourceStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetResourceStatusRequest.ProtoReflect.Descriptor instead. func (*SetResourceStatusRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{23} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{25} } func (x *SetResourceStatusRequest) GetRequestId() string { @@ -2682,7 +2917,7 @@ type ResourceResponse struct { func (x *ResourceResponse) Reset() { *x = ResourceResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[24] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2694,7 +2929,7 @@ func (x *ResourceResponse) String() string { func (*ResourceResponse) ProtoMessage() {} func (x *ResourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[24] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2707,7 +2942,7 @@ func (x *ResourceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceResponse.ProtoReflect.Descriptor instead. func (*ResourceResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{24} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{26} } func (x *ResourceResponse) GetResource() *Resource { @@ -2733,7 +2968,7 @@ type ListResourceGroupsRequest struct { func (x *ListResourceGroupsRequest) Reset() { *x = ListResourceGroupsRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[25] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2745,7 +2980,7 @@ func (x *ListResourceGroupsRequest) String() string { func (*ListResourceGroupsRequest) ProtoMessage() {} func (x *ListResourceGroupsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[25] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2758,7 +2993,7 @@ func (x *ListResourceGroupsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListResourceGroupsRequest.ProtoReflect.Descriptor instead. func (*ListResourceGroupsRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{25} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{27} } func (x *ListResourceGroupsRequest) GetRequestId() string { @@ -2821,7 +3056,7 @@ type ListResourceGroupsResponse struct { func (x *ListResourceGroupsResponse) Reset() { *x = ListResourceGroupsResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[26] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2833,7 +3068,7 @@ func (x *ListResourceGroupsResponse) String() string { func (*ListResourceGroupsResponse) ProtoMessage() {} func (x *ListResourceGroupsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[26] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2846,7 +3081,7 @@ func (x *ListResourceGroupsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListResourceGroupsResponse.ProtoReflect.Descriptor instead. func (*ListResourceGroupsResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{26} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{28} } func (x *ListResourceGroupsResponse) GetGroups() []*ResourceGroup { @@ -2875,7 +3110,7 @@ type GetResourceGroupRequest struct { func (x *GetResourceGroupRequest) Reset() { *x = GetResourceGroupRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[27] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2887,7 +3122,7 @@ func (x *GetResourceGroupRequest) String() string { func (*GetResourceGroupRequest) ProtoMessage() {} func (x *GetResourceGroupRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[27] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2900,7 +3135,7 @@ func (x *GetResourceGroupRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetResourceGroupRequest.ProtoReflect.Descriptor instead. func (*GetResourceGroupRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{27} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{29} } func (x *GetResourceGroupRequest) GetRequestId() string { @@ -2934,7 +3169,7 @@ type GetResourceGroupResponse struct { func (x *GetResourceGroupResponse) Reset() { *x = GetResourceGroupResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[28] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2946,7 +3181,7 @@ func (x *GetResourceGroupResponse) String() string { func (*GetResourceGroupResponse) ProtoMessage() {} func (x *GetResourceGroupResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[28] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2959,7 +3194,7 @@ func (x *GetResourceGroupResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetResourceGroupResponse.ProtoReflect.Descriptor instead. func (*GetResourceGroupResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{28} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{30} } func (x *GetResourceGroupResponse) GetGroup() *ResourceGroup { @@ -2987,7 +3222,7 @@ type CreateResourceGroupRequest struct { func (x *CreateResourceGroupRequest) Reset() { *x = CreateResourceGroupRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[29] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2999,7 +3234,7 @@ func (x *CreateResourceGroupRequest) String() string { func (*CreateResourceGroupRequest) ProtoMessage() {} func (x *CreateResourceGroupRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[29] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3012,7 +3247,7 @@ func (x *CreateResourceGroupRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateResourceGroupRequest.ProtoReflect.Descriptor instead. func (*CreateResourceGroupRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{29} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{31} } func (x *CreateResourceGroupRequest) GetRequestId() string { @@ -3097,7 +3332,7 @@ type UpdateResourceGroupRequest struct { func (x *UpdateResourceGroupRequest) Reset() { *x = UpdateResourceGroupRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[30] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3109,7 +3344,7 @@ func (x *UpdateResourceGroupRequest) String() string { func (*UpdateResourceGroupRequest) ProtoMessage() {} func (x *UpdateResourceGroupRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[30] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3122,7 +3357,7 @@ func (x *UpdateResourceGroupRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateResourceGroupRequest.ProtoReflect.Descriptor instead. func (*UpdateResourceGroupRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{30} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{32} } func (x *UpdateResourceGroupRequest) GetRequestId() string { @@ -3209,7 +3444,7 @@ type SetResourceGroupStatusRequest struct { func (x *SetResourceGroupStatusRequest) Reset() { *x = SetResourceGroupStatusRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[31] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3221,7 +3456,7 @@ func (x *SetResourceGroupStatusRequest) String() string { func (*SetResourceGroupStatusRequest) ProtoMessage() {} func (x *SetResourceGroupStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[31] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3234,7 +3469,7 @@ func (x *SetResourceGroupStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetResourceGroupStatusRequest.ProtoReflect.Descriptor instead. func (*SetResourceGroupStatusRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{31} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{33} } func (x *SetResourceGroupStatusRequest) GetRequestId() string { @@ -3282,7 +3517,7 @@ type ResourceGroupResponse struct { func (x *ResourceGroupResponse) Reset() { *x = ResourceGroupResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[32] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3294,7 +3529,7 @@ func (x *ResourceGroupResponse) String() string { func (*ResourceGroupResponse) ProtoMessage() {} func (x *ResourceGroupResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[32] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3307,7 +3542,7 @@ func (x *ResourceGroupResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceGroupResponse.ProtoReflect.Descriptor instead. func (*ResourceGroupResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{32} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{34} } func (x *ResourceGroupResponse) GetGroup() *ResourceGroup { @@ -3335,7 +3570,7 @@ type ListGiftConfigsRequest struct { func (x *ListGiftConfigsRequest) Reset() { *x = ListGiftConfigsRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[33] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3347,7 +3582,7 @@ func (x *ListGiftConfigsRequest) String() string { func (*ListGiftConfigsRequest) ProtoMessage() {} func (x *ListGiftConfigsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[33] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3360,7 +3595,7 @@ func (x *ListGiftConfigsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListGiftConfigsRequest.ProtoReflect.Descriptor instead. func (*ListGiftConfigsRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{33} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{35} } func (x *ListGiftConfigsRequest) GetRequestId() string { @@ -3437,7 +3672,7 @@ type ListGiftConfigsResponse struct { func (x *ListGiftConfigsResponse) Reset() { *x = ListGiftConfigsResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[34] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3449,7 +3684,7 @@ func (x *ListGiftConfigsResponse) String() string { func (*ListGiftConfigsResponse) ProtoMessage() {} func (x *ListGiftConfigsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[34] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3462,7 +3697,7 @@ func (x *ListGiftConfigsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListGiftConfigsResponse.ProtoReflect.Descriptor instead. func (*ListGiftConfigsResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{34} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{36} } func (x *ListGiftConfigsResponse) GetGifts() []*GiftConfig { @@ -3508,7 +3743,7 @@ type CreateGiftConfigRequest struct { func (x *CreateGiftConfigRequest) Reset() { *x = CreateGiftConfigRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[35] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3520,7 +3755,7 @@ func (x *CreateGiftConfigRequest) String() string { func (*CreateGiftConfigRequest) ProtoMessage() {} func (x *CreateGiftConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[35] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3533,7 +3768,7 @@ func (x *CreateGiftConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateGiftConfigRequest.ProtoReflect.Descriptor instead. func (*CreateGiftConfigRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{35} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{37} } func (x *CreateGiftConfigRequest) GetRequestId() string { @@ -3705,7 +3940,7 @@ type UpdateGiftConfigRequest struct { func (x *UpdateGiftConfigRequest) Reset() { *x = UpdateGiftConfigRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[36] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3717,7 +3952,7 @@ func (x *UpdateGiftConfigRequest) String() string { func (*UpdateGiftConfigRequest) ProtoMessage() {} func (x *UpdateGiftConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[36] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3730,7 +3965,7 @@ func (x *UpdateGiftConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateGiftConfigRequest.ProtoReflect.Descriptor instead. func (*UpdateGiftConfigRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{36} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{38} } func (x *UpdateGiftConfigRequest) GetRequestId() string { @@ -3887,7 +4122,7 @@ type SetGiftConfigStatusRequest struct { func (x *SetGiftConfigStatusRequest) Reset() { *x = SetGiftConfigStatusRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[37] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3899,7 +4134,7 @@ func (x *SetGiftConfigStatusRequest) String() string { func (*SetGiftConfigStatusRequest) ProtoMessage() {} func (x *SetGiftConfigStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[37] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3912,7 +4147,7 @@ func (x *SetGiftConfigStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetGiftConfigStatusRequest.ProtoReflect.Descriptor instead. func (*SetGiftConfigStatusRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{37} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{39} } func (x *SetGiftConfigStatusRequest) GetRequestId() string { @@ -3960,7 +4195,7 @@ type GiftConfigResponse struct { func (x *GiftConfigResponse) Reset() { *x = GiftConfigResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[38] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3972,7 +4207,7 @@ func (x *GiftConfigResponse) String() string { func (*GiftConfigResponse) ProtoMessage() {} func (x *GiftConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[38] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3985,7 +4220,7 @@ func (x *GiftConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GiftConfigResponse.ProtoReflect.Descriptor instead. func (*GiftConfigResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{38} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{40} } func (x *GiftConfigResponse) GetGift() *GiftConfig { @@ -4013,7 +4248,7 @@ type GrantResourceRequest struct { func (x *GrantResourceRequest) Reset() { *x = GrantResourceRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[39] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4025,7 +4260,7 @@ func (x *GrantResourceRequest) String() string { func (*GrantResourceRequest) ProtoMessage() {} func (x *GrantResourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[39] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4038,7 +4273,7 @@ func (x *GrantResourceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GrantResourceRequest.ProtoReflect.Descriptor instead. func (*GrantResourceRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{39} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{41} } func (x *GrantResourceRequest) GetCommandId() string { @@ -4120,7 +4355,7 @@ type GrantResourceGroupRequest struct { func (x *GrantResourceGroupRequest) Reset() { *x = GrantResourceGroupRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[40] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4132,7 +4367,7 @@ func (x *GrantResourceGroupRequest) String() string { func (*GrantResourceGroupRequest) ProtoMessage() {} func (x *GrantResourceGroupRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[40] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4145,7 +4380,7 @@ func (x *GrantResourceGroupRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GrantResourceGroupRequest.ProtoReflect.Descriptor instead. func (*GrantResourceGroupRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{40} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{42} } func (x *GrantResourceGroupRequest) GetCommandId() string { @@ -4207,7 +4442,7 @@ type ResourceGrantResponse struct { func (x *ResourceGrantResponse) Reset() { *x = ResourceGrantResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[41] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4219,7 +4454,7 @@ func (x *ResourceGrantResponse) String() string { func (*ResourceGrantResponse) ProtoMessage() {} func (x *ResourceGrantResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[41] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4232,7 +4467,7 @@ func (x *ResourceGrantResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceGrantResponse.ProtoReflect.Descriptor instead. func (*ResourceGrantResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{41} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{43} } func (x *ResourceGrantResponse) GetGrant() *ResourceGrant { @@ -4256,7 +4491,7 @@ type ListUserResourcesRequest struct { func (x *ListUserResourcesRequest) Reset() { *x = ListUserResourcesRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[42] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4268,7 +4503,7 @@ func (x *ListUserResourcesRequest) String() string { func (*ListUserResourcesRequest) ProtoMessage() {} func (x *ListUserResourcesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[42] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4281,7 +4516,7 @@ func (x *ListUserResourcesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUserResourcesRequest.ProtoReflect.Descriptor instead. func (*ListUserResourcesRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{42} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{44} } func (x *ListUserResourcesRequest) GetRequestId() string { @@ -4329,7 +4564,7 @@ type ListUserResourcesResponse struct { func (x *ListUserResourcesResponse) Reset() { *x = ListUserResourcesResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[43] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4341,7 +4576,7 @@ func (x *ListUserResourcesResponse) String() string { func (*ListUserResourcesResponse) ProtoMessage() {} func (x *ListUserResourcesResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[43] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4354,7 +4589,7 @@ func (x *ListUserResourcesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUserResourcesResponse.ProtoReflect.Descriptor instead. func (*ListUserResourcesResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{43} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{45} } func (x *ListUserResourcesResponse) GetResources() []*UserResourceEntitlement { @@ -4378,7 +4613,7 @@ type EquipUserResourceRequest struct { func (x *EquipUserResourceRequest) Reset() { *x = EquipUserResourceRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[44] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4390,7 +4625,7 @@ func (x *EquipUserResourceRequest) String() string { func (*EquipUserResourceRequest) ProtoMessage() {} func (x *EquipUserResourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[44] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4403,7 +4638,7 @@ func (x *EquipUserResourceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EquipUserResourceRequest.ProtoReflect.Descriptor instead. func (*EquipUserResourceRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{44} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{46} } func (x *EquipUserResourceRequest) GetRequestId() string { @@ -4451,7 +4686,7 @@ type EquipUserResourceResponse struct { func (x *EquipUserResourceResponse) Reset() { *x = EquipUserResourceResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[45] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4463,7 +4698,7 @@ func (x *EquipUserResourceResponse) String() string { func (*EquipUserResourceResponse) ProtoMessage() {} func (x *EquipUserResourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[45] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4476,7 +4711,7 @@ func (x *EquipUserResourceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EquipUserResourceResponse.ProtoReflect.Descriptor instead. func (*EquipUserResourceResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{45} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{47} } func (x *EquipUserResourceResponse) GetResource() *UserResourceEntitlement { @@ -4501,7 +4736,7 @@ type ListResourceGrantsRequest struct { func (x *ListResourceGrantsRequest) Reset() { *x = ListResourceGrantsRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[46] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4513,7 +4748,7 @@ func (x *ListResourceGrantsRequest) String() string { func (*ListResourceGrantsRequest) ProtoMessage() {} func (x *ListResourceGrantsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[46] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4526,7 +4761,7 @@ func (x *ListResourceGrantsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListResourceGrantsRequest.ProtoReflect.Descriptor instead. func (*ListResourceGrantsRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{46} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{48} } func (x *ListResourceGrantsRequest) GetRequestId() string { @@ -4582,7 +4817,7 @@ type ListResourceGrantsResponse struct { func (x *ListResourceGrantsResponse) Reset() { *x = ListResourceGrantsResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[47] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4594,7 +4829,7 @@ func (x *ListResourceGrantsResponse) String() string { func (*ListResourceGrantsResponse) ProtoMessage() {} func (x *ListResourceGrantsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[47] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4607,7 +4842,7 @@ func (x *ListResourceGrantsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListResourceGrantsResponse.ProtoReflect.Descriptor instead. func (*ListResourceGrantsResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{47} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{49} } func (x *ListResourceGrantsResponse) GetGrants() []*ResourceGrant { @@ -4651,7 +4886,7 @@ type RechargeBill struct { func (x *RechargeBill) Reset() { *x = RechargeBill{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[48] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4663,7 +4898,7 @@ func (x *RechargeBill) String() string { func (*RechargeBill) ProtoMessage() {} func (x *RechargeBill) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[48] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4676,7 +4911,7 @@ func (x *RechargeBill) ProtoReflect() protoreflect.Message { // Deprecated: Use RechargeBill.ProtoReflect.Descriptor instead. func (*RechargeBill) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{48} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{50} } func (x *RechargeBill) GetAppCode() string { @@ -4826,7 +5061,7 @@ type ListRechargeBillsRequest struct { func (x *ListRechargeBillsRequest) Reset() { *x = ListRechargeBillsRequest{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[49] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4838,7 +5073,7 @@ func (x *ListRechargeBillsRequest) String() string { func (*ListRechargeBillsRequest) ProtoMessage() {} func (x *ListRechargeBillsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[49] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4851,7 +5086,7 @@ func (x *ListRechargeBillsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRechargeBillsRequest.ProtoReflect.Descriptor instead. func (*ListRechargeBillsRequest) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{49} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{51} } func (x *ListRechargeBillsRequest) GetRequestId() string { @@ -4949,7 +5184,7 @@ type ListRechargeBillsResponse struct { func (x *ListRechargeBillsResponse) Reset() { *x = ListRechargeBillsResponse{} - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[50] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4961,7 +5196,7 @@ func (x *ListRechargeBillsResponse) String() string { func (*ListRechargeBillsResponse) ProtoMessage() {} func (x *ListRechargeBillsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_wallet_v1_wallet_proto_msgTypes[50] + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4974,7 +5209,7 @@ func (x *ListRechargeBillsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRechargeBillsResponse.ProtoReflect.Descriptor instead. func (*ListRechargeBillsResponse) Descriptor() ([]byte, []int) { - return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{50} + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{52} } func (x *ListRechargeBillsResponse) GetBills() []*RechargeBill { @@ -5089,849 +5324,909 @@ var file_proto_wallet_v1_wallet_proto_rawDesc = []byte{ 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, - 0x65, 0x22, 0xa9, 0x02, 0x0a, 0x1d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x43, 0x6f, - 0x69, 0x6e, 0x46, 0x72, 0x6f, 0x6d, 0x53, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6c, 0x6c, - 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, 0x03, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, - 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, - 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x19, - 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x65, 0x6c, - 0x6c, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0e, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x6f, - 0x6e, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x65, - 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x94, 0x04, - 0x0a, 0x1e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x43, 0x6f, 0x69, 0x6e, 0x46, 0x72, - 0x6f, 0x6d, 0x53, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x65, 0x6c, 0x6c, 0x65, - 0x72, 0x5f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x42, 0x61, 0x6c, - 0x61, 0x6e, 0x63, 0x65, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x30, 0x0a, 0x14, 0x74, 0x61, 0x72, - 0x67, 0x65, 0x74, 0x5f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x66, 0x74, 0x65, - 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x42, - 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x61, - 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x6d, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, - 0x75, 0x73, 0x64, 0x5f, 0x6d, 0x69, 0x6e, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x10, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x55, 0x73, 0x64, 0x4d, 0x69, 0x6e, 0x6f, - 0x72, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x63, 0x75, - 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x14, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x43, 0x75, 0x72, 0x72, 0x65, - 0x6e, 0x63, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x72, 0x65, 0x63, 0x68, 0x61, - 0x72, 0x67, 0x65, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x10, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x49, 0x64, 0x12, 0x36, 0x0a, 0x17, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, - 0x65, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, - 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, - 0x1b, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x18, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x43, 0x6f, 0x69, 0x6e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x46, 0x0a, 0x20, - 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, - 0x75, 0x73, 0x64, 0x5f, 0x6d, 0x69, 0x6e, 0x6f, 0x72, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x1c, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, - 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x55, 0x73, 0x64, 0x4d, 0x69, 0x6e, 0x6f, 0x72, 0x41, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xc9, 0x05, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 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, 0x1f, 0x0a, 0x0b, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x23, 0x0a, - 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, - 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x61, 0x62, 0x6c, 0x65, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x61, 0x62, 0x6c, - 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, - 0x65, 0x67, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x67, 0x72, 0x61, 0x6e, 0x74, - 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, - 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x63, - 0x6f, 0x70, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, 0x61, 0x67, - 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, - 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x73, 0x73, 0x65, - 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, - 0x75, 0x72, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x76, 0x69, - 0x65, 0x77, 0x55, 0x72, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x6e, - 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x0f, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x73, 0x6f, 0x6e, 0x12, - 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x10, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x2b, - 0x0a, 0x12, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x5f, 0x75, 0x73, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x64, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x12, 0x75, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, - 0x64, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, - 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, - 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x14, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, - 0x22, 0xc7, 0x03, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x0d, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, - 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, - 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1a, 0x0a, - 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, - 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, - 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, - 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, - 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, - 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x74, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2a, - 0x0a, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, - 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x77, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xad, 0x03, 0x0a, 0x0d, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 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, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x43, 0x6f, 0x64, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x20, 0x0a, - 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x38, - 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, - 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x74, 0x65, - 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x55, - 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x12, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, - 0x5f, 0x62, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, - 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, - 0x5f, 0x6d, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xa9, 0x06, 0x0a, 0x0a, 0x47, - 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, + 0x65, 0x22, 0xa3, 0x03, 0x0a, 0x21, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x69, + 0x74, 0x43, 0x6f, 0x69, 0x6e, 0x53, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x53, 0x74, 0x6f, 0x63, 0x6b, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, + 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, + 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, + 0x73, 0x74, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x73, 0x74, 0x6f, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x63, + 0x6f, 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, + 0x70, 0x61, 0x69, 0x64, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x61, 0x69, 0x64, 0x43, 0x75, + 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x70, 0x61, + 0x69, 0x64, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x70, 0x61, 0x69, 0x64, 0x41, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x66, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x76, 0x69, 0x64, 0x65, + 0x6e, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, + 0x76, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x66, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, + 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, + 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x8f, 0x03, 0x0a, 0x22, 0x41, 0x64, 0x6d, 0x69, + 0x6e, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x69, 0x6e, 0x53, 0x65, 0x6c, 0x6c, 0x65, + 0x72, 0x53, 0x74, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, + 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x5f, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, + 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, + 0x74, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x73, 0x74, 0x6f, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, + 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x70, + 0x61, 0x69, 0x64, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x61, 0x69, 0x64, 0x43, 0x75, 0x72, + 0x72, 0x65, 0x6e, 0x63, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x70, 0x61, 0x69, + 0x64, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x70, 0x61, 0x69, 0x64, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x12, 0x39, 0x0a, 0x19, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5f, + 0x61, 0x73, 0x5f, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, + 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x41, 0x73, 0x53, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x52, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, + 0x12, 0x23, 0x0a, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x66, 0x74, 0x65, + 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, + 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xa9, 0x02, 0x0a, 0x1d, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x43, 0x6f, 0x69, 0x6e, 0x46, 0x72, 0x6f, 0x6d, 0x53, 0x65, + 0x6c, 0x6c, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, + 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6c, 0x6c, 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, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, + 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, + 0x65, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x67, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x73, 0x65, 0x6c, + 0x6c, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x67, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x94, 0x04, 0x0a, 0x1e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, + 0x65, 0x72, 0x43, 0x6f, 0x69, 0x6e, 0x46, 0x72, 0x6f, 0x6d, 0x53, 0x65, 0x6c, 0x6c, 0x65, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, + 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x30, 0x0a, 0x14, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, + 0x65, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, + 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x66, 0x74, 0x65, + 0x72, 0x12, 0x30, 0x0a, 0x14, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x62, 0x61, 0x6c, 0x61, + 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x12, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x66, + 0x74, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x72, + 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x75, 0x73, 0x64, 0x5f, 0x6d, 0x69, 0x6e, 0x6f, + 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, + 0x65, 0x55, 0x73, 0x64, 0x4d, 0x69, 0x6e, 0x6f, 0x72, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, 0x63, + 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x63, + 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x72, 0x65, 0x63, 0x68, 0x61, + 0x72, 0x67, 0x65, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, + 0x2c, 0x0a, 0x12, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x72, 0x65, 0x63, + 0x68, 0x61, 0x72, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x64, 0x12, 0x36, 0x0a, + 0x17, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, + 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x1b, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, + 0x65, 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x61, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x18, 0x72, 0x65, 0x63, 0x68, + 0x61, 0x72, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x69, 0x6e, 0x41, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x46, 0x0a, 0x20, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, + 0x5f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x75, 0x73, 0x64, 0x5f, 0x6d, 0x69, 0x6e, 0x6f, + 0x72, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x1c, + 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x55, 0x73, + 0x64, 0x4d, 0x69, 0x6e, 0x6f, 0x72, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xc9, 0x05, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 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, 0x17, 0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, + 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x67, + 0x72, 0x61, 0x6e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, + 0x67, 0x72, 0x61, 0x6e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x67, 0x72, 0x61, + 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, + 0x12, 0x2a, 0x0a, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x13, + 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, + 0x75, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, 0x61, 0x67, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, + 0x1b, 0x0a, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, + 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x55, 0x72, 0x6c, 0x12, 0x23, 0x0a, + 0x0d, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0e, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, + 0x72, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6a, + 0x73, 0x6f, 0x6e, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, + 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, + 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x62, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x55, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x12, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, + 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, + 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xc7, 0x03, 0x0a, 0x11, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x22, + 0x0a, 0x0d, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, + 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, - 0x12, 0x2b, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x65, - 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x23, 0x0a, - 0x0d, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x69, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x69, 0x6e, 0x50, 0x72, 0x69, 0x63, - 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, - 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x67, 0x69, - 0x66, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x68, 0x65, 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x09, 0x68, 0x65, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2b, 0x0a, 0x12, - 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, - 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x12, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, - 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1d, - 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x11, 0x20, 0x03, - 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x73, 0x12, 0x24, 0x0a, - 0x0e, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, - 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x67, 0x69, 0x66, 0x74, 0x54, 0x79, 0x70, 0x65, 0x43, - 0x6f, 0x64, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x61, 0x73, - 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, - 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x2a, 0x0a, 0x11, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x66, 0x72, 0x6f, - 0x6d, 0x5f, 0x6d, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x65, 0x66, 0x66, 0x65, - 0x63, 0x74, 0x69, 0x76, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x4d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, - 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6d, 0x73, 0x18, 0x15, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, - 0x6f, 0x4d, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, - 0x70, 0x65, 0x73, 0x18, 0x16, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x66, 0x66, 0x65, 0x63, - 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0xeb, 0x03, 0x0a, 0x17, 0x55, 0x73, 0x65, 0x72, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 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, 0x25, 0x0a, - 0x0e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1f, 0x0a, - 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x35, - 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1a, 0x0a, - 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x6d, - 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, - 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x66, 0x66, 0x65, - 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x41, 0x74, 0x4d, 0x73, - 0x12, 0x22, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x5f, 0x6d, - 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, - 0x41, 0x74, 0x4d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x67, - 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, - 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, - 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, - 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, - 0x41, 0x74, 0x4d, 0x73, 0x22, 0x86, 0x03, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x0d, 0x67, 0x72, - 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x12, 0x19, - 0x0a, 0x08, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, - 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4a, 0x73, 0x6f, 0x6e, - 0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1f, 0x0a, 0x0b, - 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12, 0x1f, 0x0a, - 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x32, - 0x0a, 0x15, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, - 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x74, 0x69, - 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xe1, 0x03, - 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 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, 0x19, 0x0a, 0x08, 0x67, 0x72, - 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, - 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 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, 0x21, 0x0a, 0x0c, 0x67, 0x72, - 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2c, 0x0a, - 0x12, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x67, 0x72, 0x61, 0x6e, 0x74, - 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x67, - 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x6a, - 0x65, 0x63, 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, 0x12, 0x16, 0x0a, - 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, - 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, - 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, - 0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x74, - 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, - 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0d, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, - 0x73, 0x22, 0x8e, 0x02, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1f, 0x0a, 0x0b, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, - 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, - 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, - 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, - 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x74, 0x65, - 0x6d, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x74, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x73, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x4d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, + 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, + 0x6d, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x74, 0x65, + 0x6d, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x74, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, - 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x22, 0xf9, 0x01, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, - 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, - 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, - 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, - 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x66, - 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0x6f, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, - 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, - 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x22, 0x4c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, - 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xdc, 0x04, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, - 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, - 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x1c, 0x0a, 0x09, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x09, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x25, 0x0a, - 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, - 0x74, 0x65, 0x67, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, - 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x2e, 0x0a, 0x13, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, - 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x77, - 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, - 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, - 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, 0x61, 0x67, 0x65, 0x53, 0x63, 0x6f, - 0x70, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x75, 0x72, 0x6c, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x55, 0x72, 0x6c, - 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x75, 0x72, 0x6c, 0x18, - 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x55, 0x72, - 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, - 0x72, 0x6c, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, - 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x11, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, - 0x65, 0x72, 0x49, 0x64, 0x22, 0xfd, 0x04, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, - 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, - 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, - 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x1c, 0x0a, 0x09, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x09, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x25, 0x0a, - 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, - 0x74, 0x65, 0x67, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, - 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x2e, 0x0a, 0x13, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, - 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x77, - 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, - 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, - 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x75, 0x73, 0x61, 0x67, 0x65, 0x53, 0x63, 0x6f, - 0x70, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x75, 0x72, 0x6c, - 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x55, 0x72, 0x6c, - 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x5f, 0x75, 0x72, 0x6c, 0x18, - 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x55, 0x72, - 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, - 0x72, 0x6c, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, - 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x12, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, - 0x65, 0x72, 0x49, 0x64, 0x22, 0xb7, 0x01, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, - 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, - 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, - 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x49, - 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, - 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xd9, 0x01, 0x0a, 0x19, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x6e, 0x74, 0x22, 0xad, 0x03, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, + 0x72, 0x6f, 0x75, 0x70, 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, + 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x72, + 0x6f, 0x75, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, + 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, + 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, + 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, + 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x12, 0x2b, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x5f, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2b, 0x0a, + 0x12, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x64, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, + 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, + 0x4d, 0x73, 0x22, 0xa9, 0x06, 0x0a, 0x0a, 0x47, 0x69, 0x66, 0x74, 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, 0x17, 0x0a, 0x07, + 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, + 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, + 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, + 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x73, + 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, + 0x69, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, + 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, + 0x63, 0x6f, 0x69, 0x6e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x67, 0x69, 0x66, + 0x74, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x67, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x41, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x68, 0x65, 0x61, 0x74, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x68, 0x65, 0x61, 0x74, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2b, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x62, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, + 0x64, 0x12, 0x2b, 0x0a, 0x12, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x5f, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x22, + 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, + 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, + 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, + 0x5f, 0x6d, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, + 0x5f, 0x69, 0x64, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x67, 0x69, + 0x6f, 0x6e, 0x49, 0x64, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x67, + 0x69, 0x66, 0x74, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x63, + 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x65, 0x66, 0x66, 0x65, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6d, 0x73, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x46, 0x72, 0x6f, + 0x6d, 0x4d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, + 0x5f, 0x74, 0x6f, 0x5f, 0x6d, 0x73, 0x18, 0x15, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x65, 0x66, + 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 0x6f, 0x4d, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x65, + 0x66, 0x66, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x16, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0b, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0xeb, + 0x03, 0x0a, 0x17, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x45, + 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 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, 0x25, 0x0a, 0x0e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x71, + 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x72, + 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61, 0x74, + 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x69, + 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0b, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x26, 0x0a, 0x0f, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, + 0x6e, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0x86, 0x03, 0x0a, + 0x11, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x74, + 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x0d, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x74, 0x65, 0x6d, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x74, + 0x49, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x49, + 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x64, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, + 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, + 0x6d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xe1, 0x03, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 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, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, + 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 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, 0x21, 0x0a, 0x0c, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x10, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x67, + 0x72, 0x61, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 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, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x28, 0x0a, + 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, + 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, + 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, + 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, + 0x6d, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0x8e, 0x02, 0x0a, 0x16, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x49, + 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x4d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, + 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x74, 0x65, 0x6d, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2a, + 0x0a, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x77, 0x61, + 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xf9, 0x01, 0x0a, 0x14, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, + 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, + 0x79, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, + 0x77, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, + 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, + 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, + 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x66, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x37, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0x6f, + 0x0a, 0x12, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, + 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x22, + 0x4c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xdc, 0x04, + 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, - 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, - 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x77, - 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, - 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, - 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, - 0x6e, 0x6c, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, - 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x6a, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, - 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x22, 0x6e, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, - 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, - 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, - 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, - 0x64, 0x22, 0x50, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, - 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x05, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x22, 0xcb, 0x02, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, - 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1d, 0x0a, 0x0a, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, - 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, - 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, - 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, - 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x70, 0x75, 0x74, - 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, - 0x64, 0x22, 0xe6, 0x02, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, - 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x63, - 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x43, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, - 0x72, 0x12, 0x3d, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x27, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x49, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xb6, 0x01, 0x0a, 0x1d, 0x53, - 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, - 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, - 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, - 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, - 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, - 0x72, 0x49, 0x64, 0x22, 0x4d, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x05, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, - 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x05, 0x67, 0x72, 0x6f, - 0x75, 0x70, 0x22, 0x98, 0x02, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x69, 0x66, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, - 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, - 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, - 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x72, - 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, - 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x6c, 0x74, - 0x65, 0x72, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0c, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x22, 0x62, 0x0a, - 0x17, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x05, 0x67, 0x69, 0x66, 0x74, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, - 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x67, 0x69, 0x66, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, - 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x22, 0xce, 0x05, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x69, 0x66, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, - 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 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, + 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x67, 0x72, 0x61, 0x6e, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x67, 0x72, 0x61, 0x6e, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x67, + 0x72, 0x61, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x2a, 0x0a, 0x11, + 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x73, 0x61, 0x67, + 0x65, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, + 0x75, 0x73, 0x61, 0x67, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x61, 0x73, 0x73, 0x65, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x65, 0x76, + 0x69, 0x65, 0x77, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, + 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x55, 0x72, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6e, 0x69, + 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x23, + 0x0a, 0x0d, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, + 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, + 0x73, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, + 0x72, 0x18, 0x10, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, + 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xfd, 0x04, 0x0a, + 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, - 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, - 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x11, - 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6a, 0x73, 0x6f, - 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x69, - 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x70, 0x72, 0x69, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, - 0x0a, 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x69, 0x6e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x2a, 0x0a, - 0x11, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x67, 0x69, 0x66, 0x74, 0x50, 0x6f, - 0x69, 0x6e, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x68, 0x65, 0x61, - 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x68, - 0x65, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x66, 0x66, 0x65, - 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x41, 0x74, 0x4d, 0x73, - 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, - 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x03, 0x52, 0x09, - 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x67, 0x69, 0x66, - 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x67, 0x69, 0x66, 0x74, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, - 0x2a, 0x0a, 0x11, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x68, 0x61, 0x72, - 0x67, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x65, - 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6d, 0x73, - 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, - 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x4d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x66, 0x66, 0x65, 0x63, - 0x74, 0x69, 0x76, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6d, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 0x6f, 0x4d, 0x73, 0x12, - 0x21, 0x0a, 0x0c, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, - 0x14, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, - 0x65, 0x73, 0x22, 0xce, 0x05, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x69, 0x66, - 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, - 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, - 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 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, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, - 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x2b, 0x0a, - 0x11, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6a, 0x73, - 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, - 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, - 0x69, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x70, 0x72, 0x69, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x69, 0x6e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x2a, - 0x0a, 0x11, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x61, 0x6d, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x67, 0x69, 0x66, 0x74, 0x50, - 0x6f, 0x69, 0x6e, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x68, 0x65, - 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, - 0x68, 0x65, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x66, 0x66, - 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0d, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x41, 0x74, 0x4d, - 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, - 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, - 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x03, 0x52, - 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x67, 0x69, - 0x66, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x10, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x67, 0x69, 0x66, 0x74, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x64, 0x65, - 0x12, 0x2a, 0x0a, 0x11, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, - 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x68, 0x61, - 0x72, 0x67, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x11, - 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6d, - 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, - 0x76, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x4d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x66, 0x66, 0x65, - 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x6d, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 0x6f, 0x4d, 0x73, - 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, - 0x18, 0x14, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x54, 0x79, - 0x70, 0x65, 0x73, 0x22, 0xb1, 0x01, 0x0a, 0x1a, 0x53, 0x65, 0x74, 0x47, 0x69, 0x66, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, - 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 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, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x28, 0x0a, - 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, - 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, - 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x45, 0x0a, 0x12, 0x47, 0x69, 0x66, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, - 0x04, 0x67, 0x69, 0x66, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, - 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x69, - 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x67, 0x69, 0x66, 0x74, 0x22, 0xb9, - 0x02, 0x0a, 0x14, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, - 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, - 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, - 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x28, 0x0a, - 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, - 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, - 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x67, 0x72, 0x61, 0x6e, 0x74, - 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x67, - 0x72, 0x61, 0x6e, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xfb, 0x01, 0x0a, 0x19, 0x47, - 0x72, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, - 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, - 0x64, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, 0x6f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, - 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x67, 0x72, 0x61, - 0x6e, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x4d, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x34, 0x0a, 0x05, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, - 0x52, 0x05, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x22, 0xb3, 0x01, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x17, - 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, - 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x63, 0x0a, - 0x19, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, - 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x18, 0x45, 0x71, 0x75, 0x69, 0x70, 0x55, 0x73, 0x65, 0x72, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x67, 0x72, 0x61, 0x6e, 0x74, + 0x61, 0x62, 0x6c, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x67, 0x72, 0x61, 0x6e, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, + 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x67, + 0x72, 0x61, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x2a, 0x0a, 0x11, + 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x73, 0x61, 0x67, + 0x65, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, + 0x75, 0x73, 0x61, 0x67, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x61, 0x73, 0x73, 0x65, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x65, 0x76, + 0x69, 0x65, 0x77, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, + 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x55, 0x72, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6e, 0x69, + 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x23, + 0x0a, 0x0d, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, + 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, + 0x73, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, + 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, + 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xb7, 0x01, 0x0a, + 0x18, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, + 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, + 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x28, 0x0a, 0x10, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x49, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, + 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x22, 0xd9, 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, - 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x61, 0x0a, 0x19, 0x45, 0x71, - 0x75, 0x69, 0x70, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xc4, 0x01, - 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, - 0x61, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, - 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, - 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, - 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, - 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, - 0x53, 0x69, 0x7a, 0x65, 0x22, 0x6a, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x36, 0x0a, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, - 0x6e, 0x74, 0x52, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, - 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, - 0x22, 0xa7, 0x05, 0x0a, 0x0c, 0x52, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x42, 0x69, 0x6c, - 0x6c, 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, 0x25, 0x0a, 0x0e, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, - 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x63, 0x68, 0x61, - 0x72, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, + 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x6a, 0x0a, + 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x06, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0x6e, 0x0a, 0x17, 0x47, 0x65, 0x74, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, + 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x22, 0x50, 0x0a, 0x18, 0x47, 0x65, 0x74, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, 0xcb, 0x02, 0x0a, 0x1a, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, + 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, + 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x43, + 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x21, 0x0a, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x66, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, - 0x65, 0x66, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, - 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, - 0x64, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x67, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x73, 0x65, 0x6c, - 0x6c, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x74, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x67, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, - 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x75, 0x72, - 0x72, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, - 0x0a, 0x0b, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x28, 0x0a, 0x10, 0x75, 0x73, 0x64, 0x5f, 0x6d, 0x69, 0x6e, 0x6f, 0x72, 0x5f, 0x61, 0x6d, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x75, 0x73, 0x64, 0x4d, 0x69, - 0x6e, 0x6f, 0x72, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x65, 0x78, 0x63, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x43, 0x6f, 0x69, 0x6e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x19, 0x65, - 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x5f, 0x75, 0x73, 0x64, 0x5f, 0x6d, 0x69, 0x6e, 0x6f, - 0x72, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x03, 0x52, 0x16, - 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x55, 0x73, 0x64, 0x4d, 0x69, 0x6e, 0x6f, 0x72, - 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xf4, 0x02, 0x0a, 0x18, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x42, 0x69, 0x6c, 0x6c, 0x73, + 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, + 0x12, 0x3d, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x27, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, + 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, + 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xe6, 0x02, 0x0a, 0x1a, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, + 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, + 0x64, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x1d, 0x0a, + 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, + 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, + 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x05, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x70, 0x75, + 0x74, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, + 0x49, 0x64, 0x22, 0xb6, 0x01, 0x0a, 0x1d, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, + 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x4d, 0x0a, 0x15, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, 0x98, 0x02, 0x0a, 0x16, 0x4c, + 0x69, 0x73, 0x74, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, + 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, + 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, + 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, + 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4f, + 0x6e, 0x6c, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, + 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x22, 0x62, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x69, 0x66, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x31, 0x0a, 0x05, 0x67, 0x69, 0x66, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x67, 0x69, + 0x66, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0xce, 0x05, 0x0a, 0x17, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 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, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, + 0x72, 0x64, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x10, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x73, 0x6f, + 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x69, 0x63, 0x65, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x70, + 0x72, 0x69, 0x63, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x69, 0x6e, + 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x70, 0x6f, + 0x69, 0x6e, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0f, 0x67, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x68, 0x65, 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x68, 0x65, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61, 0x74, + 0x5f, 0x6d, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, + 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x73, + 0x18, 0x0f, 0x20, 0x03, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x63, + 0x6f, 0x64, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x67, 0x69, 0x66, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x63, 0x68, 0x61, 0x72, 0x67, + 0x65, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x11, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0f, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, + 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6d, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, + 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x4d, 0x73, 0x12, + 0x26, 0x0a, 0x0f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x74, 0x6f, 0x5f, + 0x6d, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, + 0x69, 0x76, 0x65, 0x54, 0x6f, 0x4d, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x66, 0x66, 0x65, 0x63, + 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x65, + 0x66, 0x66, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0xce, 0x05, 0x0a, 0x17, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, + 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, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, + 0x72, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x11, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x10, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x73, + 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x69, 0x63, 0x65, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x5f, + 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x69, + 0x6e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0f, 0x67, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x41, 0x6d, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x68, 0x65, 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x68, 0x65, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61, + 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, + 0x63, 0x74, 0x69, 0x76, 0x65, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, + 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, + 0x64, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, + 0x63, 0x6f, 0x64, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x67, 0x69, 0x66, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x63, 0x68, 0x61, 0x72, + 0x67, 0x65, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x11, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, + 0x65, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6d, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x4d, 0x73, + 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x74, 0x6f, + 0x5f, 0x6d, 0x73, 0x18, 0x13, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, + 0x74, 0x69, 0x76, 0x65, 0x54, 0x6f, 0x4d, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x66, 0x66, 0x65, + 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, + 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0xb1, 0x01, 0x0a, 0x1a, + 0x53, 0x65, 0x74, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, + 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, + 0x43, 0x6f, 0x64, 0x65, 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, 0x16, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, + 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, + 0x45, 0x0a, 0x12, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x67, 0x69, 0x66, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x04, 0x67, 0x69, 0x66, 0x74, 0x22, 0xb9, 0x02, 0x0a, 0x14, 0x47, 0x72, 0x61, 0x6e, 0x74, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x19, + 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, + 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, + 0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1f, 0x0a, 0x0b, + 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12, 0x16, 0x0a, + 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, + 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, + 0x21, 0x0a, 0x0c, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x22, 0xfb, 0x01, 0x0a, 0x19, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, + 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, + 0x0c, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x22, 0x4d, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x05, 0x67, 0x72, 0x61, + 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x05, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x22, + 0xb3, 0x01, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, + 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, + 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, + 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, + 0x6e, 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, + 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x63, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, + 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, + 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x18, 0x45, + 0x71, 0x75, 0x69, 0x70, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, - 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, - 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x23, 0x0a, - 0x0d, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, - 0x79, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, - 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x61, 0x74, - 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1a, 0x0a, 0x09, 0x65, 0x6e, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, - 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x41, 0x74, 0x4d, 0x73, - 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, - 0x70, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, - 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, - 0x65, 0x22, 0x66, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, - 0x65, 0x42, 0x69, 0x6c, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, - 0x0a, 0x05, 0x62, 0x69, 0x6c, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, + 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x49, 0x64, 0x22, 0x61, 0x0a, 0x19, 0x45, 0x71, 0x75, 0x69, 0x70, 0x55, 0x73, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x44, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0xc4, 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x24, 0x0a, + 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, + 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x6a, 0x0a, 0x1a, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x06, 0x67, 0x72, + 0x61, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, + 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x06, 0x67, 0x72, 0x61, 0x6e, + 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0xa7, 0x05, 0x0a, 0x0c, 0x52, 0x65, 0x63, + 0x68, 0x61, 0x72, 0x67, 0x65, 0x42, 0x69, 0x6c, 0x6c, 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, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, + 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6c, + 0x6c, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x65, 0x6c, + 0x6c, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0e, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x65, + 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, + 0x09, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x08, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, + 0x63, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x61, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x63, 0x6f, 0x69, + 0x6e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x75, 0x73, 0x64, 0x5f, 0x6d, + 0x69, 0x6e, 0x6f, 0x72, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0e, 0x75, 0x73, 0x64, 0x4d, 0x69, 0x6e, 0x6f, 0x72, 0x41, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x30, 0x0a, 0x14, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x5f, 0x63, 0x6f, + 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x12, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x69, 0x6e, 0x41, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x19, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x5f, + 0x75, 0x73, 0x64, 0x5f, 0x6d, 0x69, 0x6e, 0x6f, 0x72, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x11, 0x20, 0x01, 0x28, 0x03, 0x52, 0x16, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x55, 0x73, 0x64, 0x4d, 0x69, 0x6e, 0x6f, 0x72, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x22, + 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, + 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, + 0x4d, 0x73, 0x22, 0xf4, 0x02, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x63, 0x68, 0x61, + 0x72, 0x67, 0x65, 0x42, 0x69, 0x6c, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x19, + 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, + 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6c, 0x6c, + 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x67, 0x69, + 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, + 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, + 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x0a, 0x0b, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1a, 0x0a, 0x09, + 0x65, 0x6e, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x07, 0x65, 0x6e, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, + 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x66, 0x0a, 0x19, 0x4c, 0x69, 0x73, + 0x74, 0x52, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x42, 0x69, 0x6c, 0x6c, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x05, 0x62, 0x69, 0x6c, 0x6c, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, + 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, + 0x42, 0x69, 0x6c, 0x6c, 0x52, 0x05, 0x62, 0x69, 0x6c, 0x6c, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x32, 0xb4, 0x14, 0x0a, 0x0d, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x09, 0x44, 0x65, 0x62, 0x69, 0x74, 0x47, 0x69, 0x66, 0x74, + 0x12, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, + 0x76, 0x31, 0x2e, 0x44, 0x65, 0x62, 0x69, 0x74, 0x47, 0x69, 0x66, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x62, 0x69, 0x74, 0x47, 0x69, 0x66, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x42, 0x61, + 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, + 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, + 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, + 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x67, 0x0a, 0x10, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, + 0x41, 0x73, 0x73, 0x65, 0x74, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, + 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x72, 0x65, + 0x64, 0x69, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x1a, 0x41, + 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x69, 0x6e, 0x53, 0x65, + 0x6c, 0x6c, 0x65, 0x72, 0x53, 0x74, 0x6f, 0x63, 0x6b, 0x12, 0x32, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x6d, 0x69, + 0x6e, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x69, 0x6e, 0x53, 0x65, 0x6c, 0x6c, 0x65, + 0x72, 0x53, 0x74, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x42, 0x69, 0x6c, 0x6c, 0x52, 0x05, 0x62, 0x69, - 0x6c, 0x6c, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x32, 0xac, 0x13, 0x0a, 0x0d, 0x57, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x09, 0x44, - 0x65, 0x62, 0x69, 0x74, 0x47, 0x69, 0x66, 0x74, 0x12, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, - 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x62, 0x69, 0x74, - 0x47, 0x69, 0x66, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79, - 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, - 0x62, 0x69, 0x74, 0x47, 0x69, 0x66, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x58, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x23, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, 0x10, 0x41, 0x64, 0x6d, - 0x69, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x12, 0x28, 0x2e, - 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, - 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, - 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, - 0x72, 0x65, 0x64, 0x69, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x43, 0x6f, 0x69, 0x6e, 0x53, + 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x53, 0x74, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x79, 0x0a, 0x16, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x43, 0x6f, 0x69, 0x6e, 0x46, 0x72, 0x6f, 0x6d, 0x53, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x12, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x54, @@ -6086,133 +6381,137 @@ func file_proto_wallet_v1_wallet_proto_rawDescGZIP() []byte { return file_proto_wallet_v1_wallet_proto_rawDescData } -var file_proto_wallet_v1_wallet_proto_msgTypes = make([]protoimpl.MessageInfo, 51) +var file_proto_wallet_v1_wallet_proto_msgTypes = make([]protoimpl.MessageInfo, 53) var file_proto_wallet_v1_wallet_proto_goTypes = []any{ - (*DebitGiftRequest)(nil), // 0: hyapp.wallet.v1.DebitGiftRequest - (*DebitGiftResponse)(nil), // 1: hyapp.wallet.v1.DebitGiftResponse - (*AssetBalance)(nil), // 2: hyapp.wallet.v1.AssetBalance - (*GetBalancesRequest)(nil), // 3: hyapp.wallet.v1.GetBalancesRequest - (*GetBalancesResponse)(nil), // 4: hyapp.wallet.v1.GetBalancesResponse - (*AdminCreditAssetRequest)(nil), // 5: hyapp.wallet.v1.AdminCreditAssetRequest - (*AdminCreditAssetResponse)(nil), // 6: hyapp.wallet.v1.AdminCreditAssetResponse - (*TransferCoinFromSellerRequest)(nil), // 7: hyapp.wallet.v1.TransferCoinFromSellerRequest - (*TransferCoinFromSellerResponse)(nil), // 8: hyapp.wallet.v1.TransferCoinFromSellerResponse - (*Resource)(nil), // 9: hyapp.wallet.v1.Resource - (*ResourceGroupItem)(nil), // 10: hyapp.wallet.v1.ResourceGroupItem - (*ResourceGroup)(nil), // 11: hyapp.wallet.v1.ResourceGroup - (*GiftConfig)(nil), // 12: hyapp.wallet.v1.GiftConfig - (*UserResourceEntitlement)(nil), // 13: hyapp.wallet.v1.UserResourceEntitlement - (*ResourceGrantItem)(nil), // 14: hyapp.wallet.v1.ResourceGrantItem - (*ResourceGrant)(nil), // 15: hyapp.wallet.v1.ResourceGrant - (*ResourceGroupItemInput)(nil), // 16: hyapp.wallet.v1.ResourceGroupItemInput - (*ListResourcesRequest)(nil), // 17: hyapp.wallet.v1.ListResourcesRequest - (*ListResourcesResponse)(nil), // 18: hyapp.wallet.v1.ListResourcesResponse - (*GetResourceRequest)(nil), // 19: hyapp.wallet.v1.GetResourceRequest - (*GetResourceResponse)(nil), // 20: hyapp.wallet.v1.GetResourceResponse - (*CreateResourceRequest)(nil), // 21: hyapp.wallet.v1.CreateResourceRequest - (*UpdateResourceRequest)(nil), // 22: hyapp.wallet.v1.UpdateResourceRequest - (*SetResourceStatusRequest)(nil), // 23: hyapp.wallet.v1.SetResourceStatusRequest - (*ResourceResponse)(nil), // 24: hyapp.wallet.v1.ResourceResponse - (*ListResourceGroupsRequest)(nil), // 25: hyapp.wallet.v1.ListResourceGroupsRequest - (*ListResourceGroupsResponse)(nil), // 26: hyapp.wallet.v1.ListResourceGroupsResponse - (*GetResourceGroupRequest)(nil), // 27: hyapp.wallet.v1.GetResourceGroupRequest - (*GetResourceGroupResponse)(nil), // 28: hyapp.wallet.v1.GetResourceGroupResponse - (*CreateResourceGroupRequest)(nil), // 29: hyapp.wallet.v1.CreateResourceGroupRequest - (*UpdateResourceGroupRequest)(nil), // 30: hyapp.wallet.v1.UpdateResourceGroupRequest - (*SetResourceGroupStatusRequest)(nil), // 31: hyapp.wallet.v1.SetResourceGroupStatusRequest - (*ResourceGroupResponse)(nil), // 32: hyapp.wallet.v1.ResourceGroupResponse - (*ListGiftConfigsRequest)(nil), // 33: hyapp.wallet.v1.ListGiftConfigsRequest - (*ListGiftConfigsResponse)(nil), // 34: hyapp.wallet.v1.ListGiftConfigsResponse - (*CreateGiftConfigRequest)(nil), // 35: hyapp.wallet.v1.CreateGiftConfigRequest - (*UpdateGiftConfigRequest)(nil), // 36: hyapp.wallet.v1.UpdateGiftConfigRequest - (*SetGiftConfigStatusRequest)(nil), // 37: hyapp.wallet.v1.SetGiftConfigStatusRequest - (*GiftConfigResponse)(nil), // 38: hyapp.wallet.v1.GiftConfigResponse - (*GrantResourceRequest)(nil), // 39: hyapp.wallet.v1.GrantResourceRequest - (*GrantResourceGroupRequest)(nil), // 40: hyapp.wallet.v1.GrantResourceGroupRequest - (*ResourceGrantResponse)(nil), // 41: hyapp.wallet.v1.ResourceGrantResponse - (*ListUserResourcesRequest)(nil), // 42: hyapp.wallet.v1.ListUserResourcesRequest - (*ListUserResourcesResponse)(nil), // 43: hyapp.wallet.v1.ListUserResourcesResponse - (*EquipUserResourceRequest)(nil), // 44: hyapp.wallet.v1.EquipUserResourceRequest - (*EquipUserResourceResponse)(nil), // 45: hyapp.wallet.v1.EquipUserResourceResponse - (*ListResourceGrantsRequest)(nil), // 46: hyapp.wallet.v1.ListResourceGrantsRequest - (*ListResourceGrantsResponse)(nil), // 47: hyapp.wallet.v1.ListResourceGrantsResponse - (*RechargeBill)(nil), // 48: hyapp.wallet.v1.RechargeBill - (*ListRechargeBillsRequest)(nil), // 49: hyapp.wallet.v1.ListRechargeBillsRequest - (*ListRechargeBillsResponse)(nil), // 50: hyapp.wallet.v1.ListRechargeBillsResponse + (*DebitGiftRequest)(nil), // 0: hyapp.wallet.v1.DebitGiftRequest + (*DebitGiftResponse)(nil), // 1: hyapp.wallet.v1.DebitGiftResponse + (*AssetBalance)(nil), // 2: hyapp.wallet.v1.AssetBalance + (*GetBalancesRequest)(nil), // 3: hyapp.wallet.v1.GetBalancesRequest + (*GetBalancesResponse)(nil), // 4: hyapp.wallet.v1.GetBalancesResponse + (*AdminCreditAssetRequest)(nil), // 5: hyapp.wallet.v1.AdminCreditAssetRequest + (*AdminCreditAssetResponse)(nil), // 6: hyapp.wallet.v1.AdminCreditAssetResponse + (*AdminCreditCoinSellerStockRequest)(nil), // 7: hyapp.wallet.v1.AdminCreditCoinSellerStockRequest + (*AdminCreditCoinSellerStockResponse)(nil), // 8: hyapp.wallet.v1.AdminCreditCoinSellerStockResponse + (*TransferCoinFromSellerRequest)(nil), // 9: hyapp.wallet.v1.TransferCoinFromSellerRequest + (*TransferCoinFromSellerResponse)(nil), // 10: hyapp.wallet.v1.TransferCoinFromSellerResponse + (*Resource)(nil), // 11: hyapp.wallet.v1.Resource + (*ResourceGroupItem)(nil), // 12: hyapp.wallet.v1.ResourceGroupItem + (*ResourceGroup)(nil), // 13: hyapp.wallet.v1.ResourceGroup + (*GiftConfig)(nil), // 14: hyapp.wallet.v1.GiftConfig + (*UserResourceEntitlement)(nil), // 15: hyapp.wallet.v1.UserResourceEntitlement + (*ResourceGrantItem)(nil), // 16: hyapp.wallet.v1.ResourceGrantItem + (*ResourceGrant)(nil), // 17: hyapp.wallet.v1.ResourceGrant + (*ResourceGroupItemInput)(nil), // 18: hyapp.wallet.v1.ResourceGroupItemInput + (*ListResourcesRequest)(nil), // 19: hyapp.wallet.v1.ListResourcesRequest + (*ListResourcesResponse)(nil), // 20: hyapp.wallet.v1.ListResourcesResponse + (*GetResourceRequest)(nil), // 21: hyapp.wallet.v1.GetResourceRequest + (*GetResourceResponse)(nil), // 22: hyapp.wallet.v1.GetResourceResponse + (*CreateResourceRequest)(nil), // 23: hyapp.wallet.v1.CreateResourceRequest + (*UpdateResourceRequest)(nil), // 24: hyapp.wallet.v1.UpdateResourceRequest + (*SetResourceStatusRequest)(nil), // 25: hyapp.wallet.v1.SetResourceStatusRequest + (*ResourceResponse)(nil), // 26: hyapp.wallet.v1.ResourceResponse + (*ListResourceGroupsRequest)(nil), // 27: hyapp.wallet.v1.ListResourceGroupsRequest + (*ListResourceGroupsResponse)(nil), // 28: hyapp.wallet.v1.ListResourceGroupsResponse + (*GetResourceGroupRequest)(nil), // 29: hyapp.wallet.v1.GetResourceGroupRequest + (*GetResourceGroupResponse)(nil), // 30: hyapp.wallet.v1.GetResourceGroupResponse + (*CreateResourceGroupRequest)(nil), // 31: hyapp.wallet.v1.CreateResourceGroupRequest + (*UpdateResourceGroupRequest)(nil), // 32: hyapp.wallet.v1.UpdateResourceGroupRequest + (*SetResourceGroupStatusRequest)(nil), // 33: hyapp.wallet.v1.SetResourceGroupStatusRequest + (*ResourceGroupResponse)(nil), // 34: hyapp.wallet.v1.ResourceGroupResponse + (*ListGiftConfigsRequest)(nil), // 35: hyapp.wallet.v1.ListGiftConfigsRequest + (*ListGiftConfigsResponse)(nil), // 36: hyapp.wallet.v1.ListGiftConfigsResponse + (*CreateGiftConfigRequest)(nil), // 37: hyapp.wallet.v1.CreateGiftConfigRequest + (*UpdateGiftConfigRequest)(nil), // 38: hyapp.wallet.v1.UpdateGiftConfigRequest + (*SetGiftConfigStatusRequest)(nil), // 39: hyapp.wallet.v1.SetGiftConfigStatusRequest + (*GiftConfigResponse)(nil), // 40: hyapp.wallet.v1.GiftConfigResponse + (*GrantResourceRequest)(nil), // 41: hyapp.wallet.v1.GrantResourceRequest + (*GrantResourceGroupRequest)(nil), // 42: hyapp.wallet.v1.GrantResourceGroupRequest + (*ResourceGrantResponse)(nil), // 43: hyapp.wallet.v1.ResourceGrantResponse + (*ListUserResourcesRequest)(nil), // 44: hyapp.wallet.v1.ListUserResourcesRequest + (*ListUserResourcesResponse)(nil), // 45: hyapp.wallet.v1.ListUserResourcesResponse + (*EquipUserResourceRequest)(nil), // 46: hyapp.wallet.v1.EquipUserResourceRequest + (*EquipUserResourceResponse)(nil), // 47: hyapp.wallet.v1.EquipUserResourceResponse + (*ListResourceGrantsRequest)(nil), // 48: hyapp.wallet.v1.ListResourceGrantsRequest + (*ListResourceGrantsResponse)(nil), // 49: hyapp.wallet.v1.ListResourceGrantsResponse + (*RechargeBill)(nil), // 50: hyapp.wallet.v1.RechargeBill + (*ListRechargeBillsRequest)(nil), // 51: hyapp.wallet.v1.ListRechargeBillsRequest + (*ListRechargeBillsResponse)(nil), // 52: hyapp.wallet.v1.ListRechargeBillsResponse } var file_proto_wallet_v1_wallet_proto_depIdxs = []int32{ 2, // 0: hyapp.wallet.v1.GetBalancesResponse.balances:type_name -> hyapp.wallet.v1.AssetBalance 2, // 1: hyapp.wallet.v1.AdminCreditAssetResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 9, // 2: hyapp.wallet.v1.ResourceGroupItem.resource:type_name -> hyapp.wallet.v1.Resource - 10, // 3: hyapp.wallet.v1.ResourceGroup.items:type_name -> hyapp.wallet.v1.ResourceGroupItem - 9, // 4: hyapp.wallet.v1.GiftConfig.resource:type_name -> hyapp.wallet.v1.Resource - 9, // 5: hyapp.wallet.v1.UserResourceEntitlement.resource:type_name -> hyapp.wallet.v1.Resource - 14, // 6: hyapp.wallet.v1.ResourceGrant.items:type_name -> hyapp.wallet.v1.ResourceGrantItem - 9, // 7: hyapp.wallet.v1.ListResourcesResponse.resources:type_name -> hyapp.wallet.v1.Resource - 9, // 8: hyapp.wallet.v1.GetResourceResponse.resource:type_name -> hyapp.wallet.v1.Resource - 9, // 9: hyapp.wallet.v1.ResourceResponse.resource:type_name -> hyapp.wallet.v1.Resource - 11, // 10: hyapp.wallet.v1.ListResourceGroupsResponse.groups:type_name -> hyapp.wallet.v1.ResourceGroup - 11, // 11: hyapp.wallet.v1.GetResourceGroupResponse.group:type_name -> hyapp.wallet.v1.ResourceGroup - 16, // 12: hyapp.wallet.v1.CreateResourceGroupRequest.items:type_name -> hyapp.wallet.v1.ResourceGroupItemInput - 16, // 13: hyapp.wallet.v1.UpdateResourceGroupRequest.items:type_name -> hyapp.wallet.v1.ResourceGroupItemInput - 11, // 14: hyapp.wallet.v1.ResourceGroupResponse.group:type_name -> hyapp.wallet.v1.ResourceGroup - 12, // 15: hyapp.wallet.v1.ListGiftConfigsResponse.gifts:type_name -> hyapp.wallet.v1.GiftConfig - 12, // 16: hyapp.wallet.v1.GiftConfigResponse.gift:type_name -> hyapp.wallet.v1.GiftConfig - 15, // 17: hyapp.wallet.v1.ResourceGrantResponse.grant:type_name -> hyapp.wallet.v1.ResourceGrant - 13, // 18: hyapp.wallet.v1.ListUserResourcesResponse.resources:type_name -> hyapp.wallet.v1.UserResourceEntitlement - 13, // 19: hyapp.wallet.v1.EquipUserResourceResponse.resource:type_name -> hyapp.wallet.v1.UserResourceEntitlement - 15, // 20: hyapp.wallet.v1.ListResourceGrantsResponse.grants:type_name -> hyapp.wallet.v1.ResourceGrant - 48, // 21: hyapp.wallet.v1.ListRechargeBillsResponse.bills:type_name -> hyapp.wallet.v1.RechargeBill + 11, // 2: hyapp.wallet.v1.ResourceGroupItem.resource:type_name -> hyapp.wallet.v1.Resource + 12, // 3: hyapp.wallet.v1.ResourceGroup.items:type_name -> hyapp.wallet.v1.ResourceGroupItem + 11, // 4: hyapp.wallet.v1.GiftConfig.resource:type_name -> hyapp.wallet.v1.Resource + 11, // 5: hyapp.wallet.v1.UserResourceEntitlement.resource:type_name -> hyapp.wallet.v1.Resource + 16, // 6: hyapp.wallet.v1.ResourceGrant.items:type_name -> hyapp.wallet.v1.ResourceGrantItem + 11, // 7: hyapp.wallet.v1.ListResourcesResponse.resources:type_name -> hyapp.wallet.v1.Resource + 11, // 8: hyapp.wallet.v1.GetResourceResponse.resource:type_name -> hyapp.wallet.v1.Resource + 11, // 9: hyapp.wallet.v1.ResourceResponse.resource:type_name -> hyapp.wallet.v1.Resource + 13, // 10: hyapp.wallet.v1.ListResourceGroupsResponse.groups:type_name -> hyapp.wallet.v1.ResourceGroup + 13, // 11: hyapp.wallet.v1.GetResourceGroupResponse.group:type_name -> hyapp.wallet.v1.ResourceGroup + 18, // 12: hyapp.wallet.v1.CreateResourceGroupRequest.items:type_name -> hyapp.wallet.v1.ResourceGroupItemInput + 18, // 13: hyapp.wallet.v1.UpdateResourceGroupRequest.items:type_name -> hyapp.wallet.v1.ResourceGroupItemInput + 13, // 14: hyapp.wallet.v1.ResourceGroupResponse.group:type_name -> hyapp.wallet.v1.ResourceGroup + 14, // 15: hyapp.wallet.v1.ListGiftConfigsResponse.gifts:type_name -> hyapp.wallet.v1.GiftConfig + 14, // 16: hyapp.wallet.v1.GiftConfigResponse.gift:type_name -> hyapp.wallet.v1.GiftConfig + 17, // 17: hyapp.wallet.v1.ResourceGrantResponse.grant:type_name -> hyapp.wallet.v1.ResourceGrant + 15, // 18: hyapp.wallet.v1.ListUserResourcesResponse.resources:type_name -> hyapp.wallet.v1.UserResourceEntitlement + 15, // 19: hyapp.wallet.v1.EquipUserResourceResponse.resource:type_name -> hyapp.wallet.v1.UserResourceEntitlement + 17, // 20: hyapp.wallet.v1.ListResourceGrantsResponse.grants:type_name -> hyapp.wallet.v1.ResourceGrant + 50, // 21: hyapp.wallet.v1.ListRechargeBillsResponse.bills:type_name -> hyapp.wallet.v1.RechargeBill 0, // 22: hyapp.wallet.v1.WalletService.DebitGift:input_type -> hyapp.wallet.v1.DebitGiftRequest 3, // 23: hyapp.wallet.v1.WalletService.GetBalances:input_type -> hyapp.wallet.v1.GetBalancesRequest 5, // 24: hyapp.wallet.v1.WalletService.AdminCreditAsset:input_type -> hyapp.wallet.v1.AdminCreditAssetRequest - 7, // 25: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:input_type -> hyapp.wallet.v1.TransferCoinFromSellerRequest - 17, // 26: hyapp.wallet.v1.WalletService.ListResources:input_type -> hyapp.wallet.v1.ListResourcesRequest - 19, // 27: hyapp.wallet.v1.WalletService.GetResource:input_type -> hyapp.wallet.v1.GetResourceRequest - 21, // 28: hyapp.wallet.v1.WalletService.CreateResource:input_type -> hyapp.wallet.v1.CreateResourceRequest - 22, // 29: hyapp.wallet.v1.WalletService.UpdateResource:input_type -> hyapp.wallet.v1.UpdateResourceRequest - 23, // 30: hyapp.wallet.v1.WalletService.SetResourceStatus:input_type -> hyapp.wallet.v1.SetResourceStatusRequest - 25, // 31: hyapp.wallet.v1.WalletService.ListResourceGroups:input_type -> hyapp.wallet.v1.ListResourceGroupsRequest - 27, // 32: hyapp.wallet.v1.WalletService.GetResourceGroup:input_type -> hyapp.wallet.v1.GetResourceGroupRequest - 29, // 33: hyapp.wallet.v1.WalletService.CreateResourceGroup:input_type -> hyapp.wallet.v1.CreateResourceGroupRequest - 30, // 34: hyapp.wallet.v1.WalletService.UpdateResourceGroup:input_type -> hyapp.wallet.v1.UpdateResourceGroupRequest - 31, // 35: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:input_type -> hyapp.wallet.v1.SetResourceGroupStatusRequest - 33, // 36: hyapp.wallet.v1.WalletService.ListGiftConfigs:input_type -> hyapp.wallet.v1.ListGiftConfigsRequest - 35, // 37: hyapp.wallet.v1.WalletService.CreateGiftConfig:input_type -> hyapp.wallet.v1.CreateGiftConfigRequest - 36, // 38: hyapp.wallet.v1.WalletService.UpdateGiftConfig:input_type -> hyapp.wallet.v1.UpdateGiftConfigRequest - 37, // 39: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:input_type -> hyapp.wallet.v1.SetGiftConfigStatusRequest - 39, // 40: hyapp.wallet.v1.WalletService.GrantResource:input_type -> hyapp.wallet.v1.GrantResourceRequest - 40, // 41: hyapp.wallet.v1.WalletService.GrantResourceGroup:input_type -> hyapp.wallet.v1.GrantResourceGroupRequest - 42, // 42: hyapp.wallet.v1.WalletService.ListUserResources:input_type -> hyapp.wallet.v1.ListUserResourcesRequest - 44, // 43: hyapp.wallet.v1.WalletService.EquipUserResource:input_type -> hyapp.wallet.v1.EquipUserResourceRequest - 46, // 44: hyapp.wallet.v1.WalletService.ListResourceGrants:input_type -> hyapp.wallet.v1.ListResourceGrantsRequest - 49, // 45: hyapp.wallet.v1.WalletService.ListRechargeBills:input_type -> hyapp.wallet.v1.ListRechargeBillsRequest - 1, // 46: hyapp.wallet.v1.WalletService.DebitGift:output_type -> hyapp.wallet.v1.DebitGiftResponse - 4, // 47: hyapp.wallet.v1.WalletService.GetBalances:output_type -> hyapp.wallet.v1.GetBalancesResponse - 6, // 48: hyapp.wallet.v1.WalletService.AdminCreditAsset:output_type -> hyapp.wallet.v1.AdminCreditAssetResponse - 8, // 49: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:output_type -> hyapp.wallet.v1.TransferCoinFromSellerResponse - 18, // 50: hyapp.wallet.v1.WalletService.ListResources:output_type -> hyapp.wallet.v1.ListResourcesResponse - 20, // 51: hyapp.wallet.v1.WalletService.GetResource:output_type -> hyapp.wallet.v1.GetResourceResponse - 24, // 52: hyapp.wallet.v1.WalletService.CreateResource:output_type -> hyapp.wallet.v1.ResourceResponse - 24, // 53: hyapp.wallet.v1.WalletService.UpdateResource:output_type -> hyapp.wallet.v1.ResourceResponse - 24, // 54: hyapp.wallet.v1.WalletService.SetResourceStatus:output_type -> hyapp.wallet.v1.ResourceResponse - 26, // 55: hyapp.wallet.v1.WalletService.ListResourceGroups:output_type -> hyapp.wallet.v1.ListResourceGroupsResponse - 28, // 56: hyapp.wallet.v1.WalletService.GetResourceGroup:output_type -> hyapp.wallet.v1.GetResourceGroupResponse - 32, // 57: hyapp.wallet.v1.WalletService.CreateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse - 32, // 58: hyapp.wallet.v1.WalletService.UpdateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse - 32, // 59: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:output_type -> hyapp.wallet.v1.ResourceGroupResponse - 34, // 60: hyapp.wallet.v1.WalletService.ListGiftConfigs:output_type -> hyapp.wallet.v1.ListGiftConfigsResponse - 38, // 61: hyapp.wallet.v1.WalletService.CreateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse - 38, // 62: hyapp.wallet.v1.WalletService.UpdateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse - 38, // 63: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:output_type -> hyapp.wallet.v1.GiftConfigResponse - 41, // 64: hyapp.wallet.v1.WalletService.GrantResource:output_type -> hyapp.wallet.v1.ResourceGrantResponse - 41, // 65: hyapp.wallet.v1.WalletService.GrantResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse - 43, // 66: hyapp.wallet.v1.WalletService.ListUserResources:output_type -> hyapp.wallet.v1.ListUserResourcesResponse - 45, // 67: hyapp.wallet.v1.WalletService.EquipUserResource:output_type -> hyapp.wallet.v1.EquipUserResourceResponse - 47, // 68: hyapp.wallet.v1.WalletService.ListResourceGrants:output_type -> hyapp.wallet.v1.ListResourceGrantsResponse - 50, // 69: hyapp.wallet.v1.WalletService.ListRechargeBills:output_type -> hyapp.wallet.v1.ListRechargeBillsResponse - 46, // [46:70] is the sub-list for method output_type - 22, // [22:46] is the sub-list for method input_type + 7, // 25: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:input_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockRequest + 9, // 26: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:input_type -> hyapp.wallet.v1.TransferCoinFromSellerRequest + 19, // 27: hyapp.wallet.v1.WalletService.ListResources:input_type -> hyapp.wallet.v1.ListResourcesRequest + 21, // 28: hyapp.wallet.v1.WalletService.GetResource:input_type -> hyapp.wallet.v1.GetResourceRequest + 23, // 29: hyapp.wallet.v1.WalletService.CreateResource:input_type -> hyapp.wallet.v1.CreateResourceRequest + 24, // 30: hyapp.wallet.v1.WalletService.UpdateResource:input_type -> hyapp.wallet.v1.UpdateResourceRequest + 25, // 31: hyapp.wallet.v1.WalletService.SetResourceStatus:input_type -> hyapp.wallet.v1.SetResourceStatusRequest + 27, // 32: hyapp.wallet.v1.WalletService.ListResourceGroups:input_type -> hyapp.wallet.v1.ListResourceGroupsRequest + 29, // 33: hyapp.wallet.v1.WalletService.GetResourceGroup:input_type -> hyapp.wallet.v1.GetResourceGroupRequest + 31, // 34: hyapp.wallet.v1.WalletService.CreateResourceGroup:input_type -> hyapp.wallet.v1.CreateResourceGroupRequest + 32, // 35: hyapp.wallet.v1.WalletService.UpdateResourceGroup:input_type -> hyapp.wallet.v1.UpdateResourceGroupRequest + 33, // 36: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:input_type -> hyapp.wallet.v1.SetResourceGroupStatusRequest + 35, // 37: hyapp.wallet.v1.WalletService.ListGiftConfigs:input_type -> hyapp.wallet.v1.ListGiftConfigsRequest + 37, // 38: hyapp.wallet.v1.WalletService.CreateGiftConfig:input_type -> hyapp.wallet.v1.CreateGiftConfigRequest + 38, // 39: hyapp.wallet.v1.WalletService.UpdateGiftConfig:input_type -> hyapp.wallet.v1.UpdateGiftConfigRequest + 39, // 40: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:input_type -> hyapp.wallet.v1.SetGiftConfigStatusRequest + 41, // 41: hyapp.wallet.v1.WalletService.GrantResource:input_type -> hyapp.wallet.v1.GrantResourceRequest + 42, // 42: hyapp.wallet.v1.WalletService.GrantResourceGroup:input_type -> hyapp.wallet.v1.GrantResourceGroupRequest + 44, // 43: hyapp.wallet.v1.WalletService.ListUserResources:input_type -> hyapp.wallet.v1.ListUserResourcesRequest + 46, // 44: hyapp.wallet.v1.WalletService.EquipUserResource:input_type -> hyapp.wallet.v1.EquipUserResourceRequest + 48, // 45: hyapp.wallet.v1.WalletService.ListResourceGrants:input_type -> hyapp.wallet.v1.ListResourceGrantsRequest + 51, // 46: hyapp.wallet.v1.WalletService.ListRechargeBills:input_type -> hyapp.wallet.v1.ListRechargeBillsRequest + 1, // 47: hyapp.wallet.v1.WalletService.DebitGift:output_type -> hyapp.wallet.v1.DebitGiftResponse + 4, // 48: hyapp.wallet.v1.WalletService.GetBalances:output_type -> hyapp.wallet.v1.GetBalancesResponse + 6, // 49: hyapp.wallet.v1.WalletService.AdminCreditAsset:output_type -> hyapp.wallet.v1.AdminCreditAssetResponse + 8, // 50: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:output_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockResponse + 10, // 51: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:output_type -> hyapp.wallet.v1.TransferCoinFromSellerResponse + 20, // 52: hyapp.wallet.v1.WalletService.ListResources:output_type -> hyapp.wallet.v1.ListResourcesResponse + 22, // 53: hyapp.wallet.v1.WalletService.GetResource:output_type -> hyapp.wallet.v1.GetResourceResponse + 26, // 54: hyapp.wallet.v1.WalletService.CreateResource:output_type -> hyapp.wallet.v1.ResourceResponse + 26, // 55: hyapp.wallet.v1.WalletService.UpdateResource:output_type -> hyapp.wallet.v1.ResourceResponse + 26, // 56: hyapp.wallet.v1.WalletService.SetResourceStatus:output_type -> hyapp.wallet.v1.ResourceResponse + 28, // 57: hyapp.wallet.v1.WalletService.ListResourceGroups:output_type -> hyapp.wallet.v1.ListResourceGroupsResponse + 30, // 58: hyapp.wallet.v1.WalletService.GetResourceGroup:output_type -> hyapp.wallet.v1.GetResourceGroupResponse + 34, // 59: hyapp.wallet.v1.WalletService.CreateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 34, // 60: hyapp.wallet.v1.WalletService.UpdateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 34, // 61: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 36, // 62: hyapp.wallet.v1.WalletService.ListGiftConfigs:output_type -> hyapp.wallet.v1.ListGiftConfigsResponse + 40, // 63: hyapp.wallet.v1.WalletService.CreateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse + 40, // 64: hyapp.wallet.v1.WalletService.UpdateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse + 40, // 65: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:output_type -> hyapp.wallet.v1.GiftConfigResponse + 43, // 66: hyapp.wallet.v1.WalletService.GrantResource:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 43, // 67: hyapp.wallet.v1.WalletService.GrantResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 45, // 68: hyapp.wallet.v1.WalletService.ListUserResources:output_type -> hyapp.wallet.v1.ListUserResourcesResponse + 47, // 69: hyapp.wallet.v1.WalletService.EquipUserResource:output_type -> hyapp.wallet.v1.EquipUserResourceResponse + 49, // 70: hyapp.wallet.v1.WalletService.ListResourceGrants:output_type -> hyapp.wallet.v1.ListResourceGrantsResponse + 52, // 71: hyapp.wallet.v1.WalletService.ListRechargeBills:output_type -> hyapp.wallet.v1.ListRechargeBillsResponse + 47, // [47:72] is the sub-list for method output_type + 22, // [22:47] is the sub-list for method input_type 22, // [22:22] is the sub-list for extension type_name 22, // [22:22] is the sub-list for extension extendee 0, // [0:22] is the sub-list for field type_name @@ -6229,7 +6528,7 @@ func file_proto_wallet_v1_wallet_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_wallet_v1_wallet_proto_rawDesc, NumEnums: 0, - NumMessages: 51, + NumMessages: 53, NumExtensions: 0, NumServices: 1, }, diff --git a/api/proto/wallet/v1/wallet.proto b/api/proto/wallet/v1/wallet.proto index c47d43f3..304c8276 100644 --- a/api/proto/wallet/v1/wallet.proto +++ b/api/proto/wallet/v1/wallet.proto @@ -71,6 +71,33 @@ message AdminCreditAssetResponse { AssetBalance balance = 2; } +// AdminCreditCoinSellerStockRequest 是后台给 active 币商专用库存入账的账务命令。 +message AdminCreditCoinSellerStockRequest { + string command_id = 1; + int64 seller_user_id = 2; + string stock_type = 3; + int64 coin_amount = 4; + string paid_currency_code = 5; + int64 paid_amount_micro = 6; + string payment_ref = 7; + string evidence_ref = 8; + int64 operator_user_id = 9; + string reason = 10; + string app_code = 11; +} + +message AdminCreditCoinSellerStockResponse { + string transaction_id = 1; + int64 seller_user_id = 2; + string stock_type = 3; + int64 coin_amount = 4; + string paid_currency_code = 5; + int64 paid_amount_micro = 6; + bool counts_as_seller_recharge = 7; + int64 balance_after = 8; + int64 created_at_ms = 9; +} + // TransferCoinFromSellerRequest 是币商给玩家转普通金币的内部账务命令。 message TransferCoinFromSellerRequest { string command_id = 1; @@ -558,6 +585,7 @@ service WalletService { rpc DebitGift(DebitGiftRequest) returns (DebitGiftResponse); rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse); rpc AdminCreditAsset(AdminCreditAssetRequest) returns (AdminCreditAssetResponse); + rpc AdminCreditCoinSellerStock(AdminCreditCoinSellerStockRequest) returns (AdminCreditCoinSellerStockResponse); rpc TransferCoinFromSeller(TransferCoinFromSellerRequest) returns (TransferCoinFromSellerResponse); rpc ListResources(ListResourcesRequest) returns (ListResourcesResponse); rpc GetResource(GetResourceRequest) returns (GetResourceResponse); diff --git a/api/proto/wallet/v1/wallet_grpc.pb.go b/api/proto/wallet/v1/wallet_grpc.pb.go index 6992d341..b51d983d 100644 --- a/api/proto/wallet/v1/wallet_grpc.pb.go +++ b/api/proto/wallet/v1/wallet_grpc.pb.go @@ -19,30 +19,31 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - WalletService_DebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitGift" - WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances" - WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset" - WalletService_TransferCoinFromSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferCoinFromSeller" - WalletService_ListResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResources" - WalletService_GetResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResource" - WalletService_CreateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResource" - WalletService_UpdateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResource" - WalletService_SetResourceStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceStatus" - WalletService_ListResourceGroups_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGroups" - WalletService_GetResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResourceGroup" - WalletService_CreateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResourceGroup" - WalletService_UpdateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResourceGroup" - WalletService_SetResourceGroupStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceGroupStatus" - WalletService_ListGiftConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftConfigs" - WalletService_CreateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateGiftConfig" - WalletService_UpdateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateGiftConfig" - WalletService_SetGiftConfigStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetGiftConfigStatus" - WalletService_GrantResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResource" - WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup" - WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources" - WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource" - WalletService_ListResourceGrants_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGrants" - WalletService_ListRechargeBills_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeBills" + WalletService_DebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitGift" + WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances" + WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset" + WalletService_AdminCreditCoinSellerStock_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditCoinSellerStock" + WalletService_TransferCoinFromSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferCoinFromSeller" + WalletService_ListResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResources" + WalletService_GetResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResource" + WalletService_CreateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResource" + WalletService_UpdateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResource" + WalletService_SetResourceStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceStatus" + WalletService_ListResourceGroups_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGroups" + WalletService_GetResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResourceGroup" + WalletService_CreateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResourceGroup" + WalletService_UpdateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResourceGroup" + WalletService_SetResourceGroupStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceGroupStatus" + WalletService_ListGiftConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftConfigs" + WalletService_CreateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateGiftConfig" + WalletService_UpdateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateGiftConfig" + WalletService_SetGiftConfigStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetGiftConfigStatus" + WalletService_GrantResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResource" + WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup" + WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources" + WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource" + WalletService_ListResourceGrants_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGrants" + WalletService_ListRechargeBills_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeBills" ) // WalletServiceClient is the client API for WalletService service. @@ -54,6 +55,7 @@ type WalletServiceClient interface { DebitGift(ctx context.Context, in *DebitGiftRequest, opts ...grpc.CallOption) (*DebitGiftResponse, error) GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error) AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error) + AdminCreditCoinSellerStock(ctx context.Context, in *AdminCreditCoinSellerStockRequest, opts ...grpc.CallOption) (*AdminCreditCoinSellerStockResponse, error) TransferCoinFromSeller(ctx context.Context, in *TransferCoinFromSellerRequest, opts ...grpc.CallOption) (*TransferCoinFromSellerResponse, error) ListResources(ctx context.Context, in *ListResourcesRequest, opts ...grpc.CallOption) (*ListResourcesResponse, error) GetResource(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*GetResourceResponse, error) @@ -115,6 +117,16 @@ func (c *walletServiceClient) AdminCreditAsset(ctx context.Context, in *AdminCre return out, nil } +func (c *walletServiceClient) AdminCreditCoinSellerStock(ctx context.Context, in *AdminCreditCoinSellerStockRequest, opts ...grpc.CallOption) (*AdminCreditCoinSellerStockResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AdminCreditCoinSellerStockResponse) + err := c.cc.Invoke(ctx, WalletService_AdminCreditCoinSellerStock_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *walletServiceClient) TransferCoinFromSeller(ctx context.Context, in *TransferCoinFromSellerRequest, opts ...grpc.CallOption) (*TransferCoinFromSellerResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(TransferCoinFromSellerResponse) @@ -334,6 +346,7 @@ type WalletServiceServer interface { DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error) GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error) AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error) + AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error) TransferCoinFromSeller(context.Context, *TransferCoinFromSellerRequest) (*TransferCoinFromSellerResponse, error) ListResources(context.Context, *ListResourcesRequest) (*ListResourcesResponse, error) GetResource(context.Context, *GetResourceRequest) (*GetResourceResponse, error) @@ -374,6 +387,9 @@ func (UnimplementedWalletServiceServer) GetBalances(context.Context, *GetBalance func (UnimplementedWalletServiceServer) AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AdminCreditAsset not implemented") } +func (UnimplementedWalletServiceServer) AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AdminCreditCoinSellerStock not implemented") +} func (UnimplementedWalletServiceServer) TransferCoinFromSeller(context.Context, *TransferCoinFromSellerRequest) (*TransferCoinFromSellerResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method TransferCoinFromSeller not implemented") } @@ -512,6 +528,24 @@ func _WalletService_AdminCreditAsset_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _WalletService_AdminCreditCoinSellerStock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AdminCreditCoinSellerStockRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletServiceServer).AdminCreditCoinSellerStock(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WalletService_AdminCreditCoinSellerStock_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletServiceServer).AdminCreditCoinSellerStock(ctx, req.(*AdminCreditCoinSellerStockRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _WalletService_TransferCoinFromSeller_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(TransferCoinFromSellerRequest) if err := dec(in); err != nil { @@ -909,6 +943,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{ MethodName: "AdminCreditAsset", Handler: _WalletService_AdminCreditAsset_Handler, }, + { + MethodName: "AdminCreditCoinSellerStock", + Handler: _WalletService_AdminCreditCoinSellerStock_Handler, + }, { MethodName: "TransferCoinFromSeller", Handler: _WalletService_TransferCoinFromSeller_Handler, diff --git a/docs/api-endpoints.md b/docs/api-endpoints.md new file mode 100644 index 00000000..4cff0a76 --- /dev/null +++ b/docs/api-endpoints.md @@ -0,0 +1,264 @@ +# API 清单 + +本清单由当前仓库的 OpenAPI/路由合约生成。浏览器测试页只直接调用 HTTP JSON 接口;内部 gRPC 路径是 protobuf 合约引用,不代表公网 HTTP endpoint。 + +## Gateway HTTP + +`/api/v1/**` 业务接口返回 `{code,message,request_id,data}` envelope;`/healthz/**` 不套 envelope。 + +| Method | Path | Tag | Operation | Summary | +| --- | --- | --- | --- | --- | +| GET | `/healthz/live` | health | `gatewayLive` | 进程存活探测 | +| GET | `/healthz/ready` | health | `gatewayReady` | 接流量就绪探测 | +| POST | `/api/v1/auth/account/login` | auth | `loginAccountPassword` | 账号密码登录 | +| POST | `/api/v1/auth/third-party/login` | auth | `loginThirdParty` | 三方登录或注册 | +| POST | `/api/v1/auth/password/set` | auth | `setPassword` | 已登录用户首次设置密码 | +| POST | `/api/v1/auth/token/refresh` | auth | `refreshToken` | 刷新 token | +| POST | `/api/v1/auth/logout` | auth | `logout` | 登出并失效 refresh session | +| GET | `/api/v1/countries` | countries | `listRegistrationCountries` | 获取 App 注册页可选国家 | +| GET | `/api/v1/app/bootstrap` | app-config | `getAppBootstrap` | 获取 App 启动配置摘要 | +| GET | `/api/v1/app/h5-links` | app-config | `listAppH5Links` | 获取 App H5 入口地址 | +| GET | `/api/v1/im/usersig` | tencent-im | `issueTencentIMUserSig` | 获取腾讯云 IM UserSig | +| POST | `/api/v1/rtc/token` | tencent-rtc | `issueTencentRTCToken` | 获取腾讯 RTC 进房 UserSig | +| POST | `/api/v1/tencent-rtc/callback` | tencent-rtc | `handleTencentRTCCallback` | 腾讯 RTC 服务端事件回调 | +| POST | `/api/v1/files/upload` | files | `uploadFile` | 上传 App 通用文件 | +| POST | `/api/v1/files/avatar/upload` | files | `uploadAvatar` | 上传用户头像 | +| POST | `/api/v1/devices/push-token` | devices | `bindPushToken` | 绑定设备推送 token | +| DELETE | `/api/v1/devices/push-token` | devices | `deletePushToken` | 删除设备推送 token | +| POST | `/api/v1/tencent-im/callback` | tencent-im | `handleTencentIMCallback` | 腾讯云 IM 服务端回调 | +| GET | `/api/v1/users/by-display-user-id/{display_user_id}` | users | `resolveDisplayUserID` | 通过当前展示短号解析用户 | +| GET | `/api/v1/users/profiles:batch` | users | `batchUserProfiles` | 批量查询会话展示资料 | +| GET | `/api/v1/users/me/identity` | users | `getMyIdentity` | 查询当前用户展示短号状态 | +| GET | `/api/v1/users/me/host-identity` | users | `getMyHostIdentity` | 查询当前用户 Host/Agency/BD 身份 | +| GET | `/api/v1/users/me/overview` | users | `getMyOverview` | 我的页首屏聚合摘要 | +| POST | `/api/v1/users/me/onboarding/complete` | users | `completeMyOnboarding` | 完成注册页必填资料 | +| POST | `/api/v1/users/me/profile/update` | users | `updateMyProfile` | 修改当前用户基础资料 | +| POST | `/api/v1/users/me/country/change` | users | `changeMyCountry` | 修改当前用户国家 | +| POST | `/api/v1/users/me/display-id/change` | users | `changeMyDisplayUserID` | 修改当前用户默认短号 | +| POST | `/api/v1/users/me/display-id/pretty/apply` | users | `applyMyPrettyDisplayUserID` | 申请临时靓号 | +| GET | `/api/v1/rooms/current` | rooms | `getCurrentRoom` | 查询当前可恢复房间 | +| GET | `/api/v1/rooms/snapshot` | rooms | `getRoomSnapshot` | 查询房间完整快照 | +| POST | `/api/v1/rooms/create` | rooms | `createRoom` | 创建房间 | +| POST | `/api/v1/rooms/join` | rooms | `joinRoom` | 加入房间 | +| POST | `/api/v1/rooms/heartbeat` | rooms | `roomHeartbeat` | 刷新房间业务 presence | +| POST | `/api/v1/rooms/leave` | rooms | `leaveRoom` | 离开房间 | +| POST | `/api/v1/rooms/mic/up` | rooms | `micUp` | 上麦 | +| POST | `/api/v1/rooms/mic/down` | rooms | `micDown` | 下麦 | +| POST | `/api/v1/rooms/mic/change` | rooms | `changeMicSeat` | 调整麦位 | +| POST | `/api/v1/rooms/mic/publishing/confirm` | rooms | `confirmMicPublishing` | 确认 RTC 音频发布成功 | +| POST | `/api/v1/rooms/mic/lock` | rooms | `setMicSeatLock` | 锁定或解锁麦位 | +| POST | `/api/v1/rooms/chat/enabled` | rooms | `setChatEnabled` | 开启或关闭房间公屏 | +| POST | `/api/v1/rooms/admin/set` | rooms | `setRoomAdmin` | 添加或移除房间管理员 | +| POST | `/api/v1/rooms/host/transfer` | rooms | `transferRoomHost` | 转移房间主持人 | +| POST | `/api/v1/rooms/user/mute` | rooms | `muteUser` | 设置房间内禁言 | +| POST | `/api/v1/rooms/user/kick` | rooms | `kickUser` | 踢出房间用户 | +| POST | `/api/v1/rooms/user/unban` | rooms | `unbanUser` | 解除房间 ban | +| POST | `/api/v1/rooms/gift/send` | rooms | `sendGift` | 发送礼物 | +| GET | `/api/v1/resources` | resources | `listResources` | App 资源列表 | +| GET | `/api/v1/resource-groups/{group_id}` | resources | `getResourceGroup` | App 资源组详情 | +| GET | `/api/v1/gifts` | resources | `listGifts` | App 礼物列表 | +| GET | `/api/v1/users/me/resources` | resources | `listMyResources` | 我的资源权益 | +| POST | `/api/v1/users/me/resources/{resource_id}/equip` | resources | `equipMyResource` | 佩戴我的资源 | +| GET | `/api/v1/messages/tabs` | messages | `listMessageTabs` | 消息 tab 分区摘要 | +| GET | `/api/v1/messages` | messages | `listInboxMessages` | system/activity 消息列表 | +| POST | `/api/v1/messages/read-all` | messages | `markInboxSectionRead` | 标记分区全部已读 | +| POST | `/api/v1/messages/{message_id}/read` | messages | `markInboxMessageRead` | 标记单条消息已读 | +| DELETE | `/api/v1/messages/{message_id}` | messages | `deleteInboxMessage` | 删除单条消息 | +| POST | `/api/v1/wallet/coin-seller/transfer` | wallet | `transferCoinFromSeller` | 币商给玩家转金币 | + +## Admin HTTP + +后台接口实际路径包含 `/api/v1` 前缀,鉴权使用后台登录 token。 + +| Method | Path | Tag | Operation | Summary | +| --- | --- | --- | --- | --- | +| GET | `/api/v1/admin/agencies` | admin | `listAgencies` | listAgencies | +| POST | `/api/v1/admin/agencies` | admin | `createAgency` | createAgency | +| POST | `/api/v1/admin/agencies/{agency_id}/close` | admin | `closeAgency` | closeAgency | +| POST | `/api/v1/admin/agencies/{agency_id}/join-enabled` | admin | `setAgencyJoinEnabled` | setAgencyJoinEnabled | +| GET | `/api/v1/admin/app-config/banners` | admin | `listBanners` | listBanners | +| POST | `/api/v1/admin/app-config/banners` | admin | `createBanner` | createBanner | +| DELETE | `/api/v1/admin/app-config/banners/{banner_id}` | admin | `deleteBanner` | deleteBanner | +| PUT | `/api/v1/admin/app-config/banners/{banner_id}` | admin | `updateBanner` | updateBanner | +| GET | `/api/v1/admin/app-config/h5-links` | admin | `listH5Links` | listH5Links | +| PUT | `/api/v1/admin/app-config/h5-links` | admin | `updateH5Links` | updateH5Links | +| GET | `/api/v1/admin/apps` | admin | `listApps` | listApps | +| GET | `/api/v1/admin/bd-leaders` | admin | `listBDLeaders` | listBDLeaders | +| POST | `/api/v1/admin/bd-leaders` | admin | `createBDLeader` | createBDLeader | +| PATCH | `/api/v1/admin/bd-leaders/{user_id}/status` | admin | `setBDLeaderStatus` | setBDLeaderStatus | +| GET | `/api/v1/admin/bds` | admin | `listBDs` | listBDs | +| POST | `/api/v1/admin/bds` | admin | `createBD` | createBD | +| PATCH | `/api/v1/admin/bds/{user_id}/status` | admin | `setBDStatus` | setBDStatus | +| GET | `/api/v1/admin/coin-sellers` | admin | `listCoinSellers` | listCoinSellers | +| POST | `/api/v1/admin/coin-sellers` | admin | `createCoinSeller` | createCoinSeller | +| PATCH | `/api/v1/admin/coin-sellers/{user_id}/status` | admin | `setCoinSellerStatus` | setCoinSellerStatus | +| POST | `/api/v1/admin/coin-sellers/{user_id}/stock-credits` | admin | `creditCoinSellerStock` | creditCoinSellerStock | +| GET | `/api/v1/admin/countries` | admin | `listCountries` | listCountries | +| POST | `/api/v1/admin/countries` | admin | `createCountry` | createCountry | +| DELETE | `/api/v1/admin/countries/{country_id}` | admin | `deleteCountry` | deleteCountry | +| GET | `/api/v1/admin/countries/{country_id}` | admin | `getCountry` | getCountry | +| PATCH | `/api/v1/admin/countries/{country_id}` | admin | `updateCountry` | updateCountry | +| POST | `/api/v1/admin/countries/{country_id}/disable` | admin | `disableCountry` | disableCountry | +| POST | `/api/v1/admin/countries/{country_id}/enable` | admin | `enableCountry` | enableCountry | +| POST | `/api/v1/admin/files/image/upload` | admin | `uploadImage` | uploadImage | +| POST | `/api/v1/admin/files/upload` | admin | `uploadFile` | uploadFile | +| GET | `/api/v1/admin/gifts` | admin | `listGifts` | listGifts | +| POST | `/api/v1/admin/gifts` | admin | `createGift` | createGift | +| PUT | `/api/v1/admin/gifts/{gift_id}` | admin | `updateGift` | updateGift | +| POST | `/api/v1/admin/gifts/{gift_id}/disable` | admin | `disableGift` | disableGift | +| POST | `/api/v1/admin/gifts/{gift_id}/enable` | admin | `enableGift` | enableGift | +| GET | `/api/v1/admin/hosts` | admin | `listHosts` | listHosts | +| GET | `/api/v1/admin/payment/recharge-bills` | admin | `listRechargeBills` | listRechargeBills | +| GET | `/api/v1/admin/regions` | admin | `listRegions` | listRegions | +| POST | `/api/v1/admin/regions` | admin | `createRegion` | createRegion | +| DELETE | `/api/v1/admin/regions/{region_id}` | admin | `disableRegion` | disableRegion | +| GET | `/api/v1/admin/regions/{region_id}` | admin | `getRegion` | getRegion | +| PATCH | `/api/v1/admin/regions/{region_id}` | admin | `updateRegion` | updateRegion | +| PUT | `/api/v1/admin/regions/{region_id}/countries` | admin | `replaceRegionCountries` | replaceRegionCountries | +| POST | `/api/v1/admin/regions/{region_id}/enable` | admin | `enableRegion` | enableRegion | +| GET | `/api/v1/admin/resource-grants` | admin | `listResourceGrants` | listResourceGrants | +| POST | `/api/v1/admin/resource-grants/group` | admin | `grantResourceGroup` | grantResourceGroup | +| POST | `/api/v1/admin/resource-grants/resource` | admin | `grantResource` | grantResource | +| GET | `/api/v1/admin/resource-groups` | admin | `listResourceGroups` | listResourceGroups | +| POST | `/api/v1/admin/resource-groups` | admin | `createResourceGroup` | createResourceGroup | +| GET | `/api/v1/admin/resource-groups/{group_id}` | admin | `getResourceGroup` | getResourceGroup | +| PUT | `/api/v1/admin/resource-groups/{group_id}` | admin | `updateResourceGroup` | updateResourceGroup | +| POST | `/api/v1/admin/resource-groups/{group_id}/disable` | admin | `disableResourceGroup` | disableResourceGroup | +| POST | `/api/v1/admin/resource-groups/{group_id}/enable` | admin | `enableResourceGroup` | enableResourceGroup | +| PUT | `/api/v1/admin/resource-groups/{group_id}/items` | admin | `updateResourceGroupItems` | updateResourceGroupItems | +| GET | `/api/v1/admin/resources` | admin | `listResources` | listResources | +| POST | `/api/v1/admin/resources` | admin | `createResource` | createResource | +| GET | `/api/v1/admin/resources/{resource_id}` | admin | `getResource` | getResource | +| PUT | `/api/v1/admin/resources/{resource_id}` | admin | `updateResource` | updateResource | +| POST | `/api/v1/admin/resources/{resource_id}/disable` | admin | `disableResource` | disableResource | +| POST | `/api/v1/admin/resources/{resource_id}/enable` | admin | `enableResource` | enableResource | +| GET | `/api/v1/admin/rooms` | admin | `listRooms` | listRooms | +| DELETE | `/api/v1/admin/rooms/{room_id}` | admin | `deleteRoom` | deleteRoom | +| PATCH | `/api/v1/admin/rooms/{room_id}` | admin | `updateRoom` | updateRoom | +| GET | `/api/v1/app/users` | admin | `appListUsers` | appListUsers | +| GET | `/api/v1/app/users/{id}` | admin | `appGetUser` | appGetUser | +| PATCH | `/api/v1/app/users/{id}` | admin | `appUpdateUser` | appUpdateUser | +| POST | `/api/v1/app/users/{id}/ban` | admin | `appBanUser` | appBanUser | +| POST | `/api/v1/app/users/{id}/password` | admin | `appSetPassword` | appSetPassword | +| POST | `/api/v1/app/users/{id}/unban` | admin | `appUnbanUser` | appUnbanUser | +| POST | `/api/v1/auth/change-password` | admin | `changePassword` | changePassword | +| POST | `/api/v1/auth/login` | admin | `login` | login | +| POST | `/api/v1/auth/logout` | admin | `logout` | logout | +| GET | `/api/v1/auth/me` | admin | `me` | me | +| POST | `/api/v1/auth/refresh` | admin | `refresh` | refresh | +| GET | `/api/v1/dashboard/overview` | admin | `dashboardOverview` | dashboardOverview | +| POST | `/api/v1/exports/users` | admin | `createUserExportJob` | createUserExportJob | +| GET | `/api/v1/jobs` | admin | `listJobs` | listJobs | +| GET | `/api/v1/jobs/{id}/artifact` | admin | `downloadJobArtifact` | downloadJobArtifact | +| POST | `/api/v1/jobs/{id}/cancel` | admin | `cancelJob` | cancelJob | +| GET | `/api/v1/logs/login` | admin | `listLoginLogs` | listLoginLogs | +| GET | `/api/v1/logs/login/export` | admin | `exportLoginLogs` | exportLoginLogs | +| GET | `/api/v1/logs/operations` | admin | `listOperationLogs` | listOperationLogs | +| GET | `/api/v1/logs/operations/export` | admin | `exportOperationLogs` | exportOperationLogs | +| GET | `/api/v1/navigation/menus` | admin | `navigationMenus` | navigationMenus | +| GET | `/api/v1/notifications` | admin | `listNotifications` | listNotifications | +| DELETE | `/api/v1/notifications/{id}` | admin | `deleteNotification` | deleteNotification | +| PATCH | `/api/v1/notifications/{id}/read` | admin | `markNotificationRead` | markNotificationRead | +| PATCH | `/api/v1/notifications/read-all` | admin | `markAllNotificationsRead` | markAllNotificationsRead | +| GET | `/api/v1/permissions` | admin | `listPermissions` | listPermissions | +| POST | `/api/v1/permissions` | admin | `createPermission` | createPermission | +| DELETE | `/api/v1/permissions/{id}` | admin | `deletePermission` | deletePermission | +| PATCH | `/api/v1/permissions/{id}` | admin | `updatePermission` | updatePermission | +| POST | `/api/v1/permissions/sync` | admin | `syncPermissions` | syncPermissions | +| GET | `/api/v1/roles` | admin | `listRoles` | listRoles | +| POST | `/api/v1/roles` | admin | `createRole` | createRole | +| DELETE | `/api/v1/roles/{id}` | admin | `deleteRole` | deleteRole | +| PATCH | `/api/v1/roles/{id}` | admin | `updateRole` | updateRole | +| GET | `/api/v1/roles/{id}/data-scopes` | admin | `getRoleDataScopes` | getRoleDataScopes | +| PUT | `/api/v1/roles/{id}/data-scopes` | admin | `replaceRoleDataScopes` | replaceRoleDataScopes | +| PUT | `/api/v1/roles/{id}/permissions` | admin | `replaceRolePermissions` | replaceRolePermissions | +| GET | `/api/v1/search` | admin | `search` | search | +| GET | `/api/v1/system/menus` | admin | `listSystemMenus` | listSystemMenus | +| POST | `/api/v1/system/menus` | admin | `createMenu` | createMenu | +| DELETE | `/api/v1/system/menus/{id}` | admin | `deleteMenu` | deleteMenu | +| PATCH | `/api/v1/system/menus/{id}` | admin | `updateMenu` | updateMenu | +| PATCH | `/api/v1/system/menus/{id}/visible` | admin | `updateMenuVisible` | updateMenuVisible | +| PUT | `/api/v1/system/menus/sort` | admin | `sortMenus` | sortMenus | +| GET | `/api/v1/users` | admin | `listUsers` | listUsers | +| POST | `/api/v1/users` | admin | `createUser` | createUser | +| GET | `/api/v1/users/{id}` | admin | `getUser` | getUser | +| PATCH | `/api/v1/users/{id}` | admin | `updateUser` | updateUser | +| POST | `/api/v1/users/{id}/reset-password` | admin | `resetUserPassword` | resetUserPassword | +| PATCH | `/api/v1/users/{id}/status` | admin | `updateUserStatus` | updateUserStatus | +| POST | `/api/v1/users/batch/status` | admin | `batchUpdateUserStatus` | batchUpdateUserStatus | +| GET | `/api/v1/users/export` | admin | `exportUsers` | exportUsers | + +## Internal gRPC/OpenAPI Reference + +这些路径用于记录 gRPC full method,不能直接用浏览器 fetch 调用。 + +| Method | Path | Tag | Operation | Summary | +| --- | --- | --- | --- | --- | +| POST | `/hyapp.user.v1.AuthService/LoginPassword` | auth | `userAuthLoginPassword` | 短号密码登录 | +| POST | `/hyapp.user.v1.AuthService/LoginThirdParty` | auth | `userAuthLoginThirdParty` | 三方登录或注册 | +| POST | `/hyapp.user.v1.AuthService/SetPassword` | auth | `userAuthSetPassword` | 首次设置密码 | +| POST | `/hyapp.user.v1.AuthService/RefreshToken` | auth | `userAuthRefreshToken` | 刷新 token | +| POST | `/hyapp.user.v1.AuthService/Logout` | auth | `userAuthLogout` | 登出并失效 refresh session | +| POST | `/hyapp.user.v1.UserService/GetUser` | users | `userGetUser` | 查询单个用户主状态 | +| POST | `/hyapp.user.v1.UserService/BatchGetUsers` | users | `userBatchGetUsers` | 批量查询用户主状态 | +| POST | `/hyapp.user.v1.UserService/ListUserIDs` | users | `userListUserIDs` | 后台任务按 user_id 游标查询目标用户 | +| POST | `/hyapp.user.v1.UserService/UpdateUserProfile` | users | `userUpdateUserProfile` | 修改用户基础资料 | +| POST | `/hyapp.user.v1.UserService/ChangeUserCountry` | users | `userChangeUserCountry` | 修改用户国家 | +| POST | `/hyapp.user.v1.UserService/CompleteOnboarding` | users | `userCompleteOnboarding` | 完成注册页必填资料 | +| POST | `/hyapp.user.v1.UserDeviceService/BindPushToken` | user-devices | `userBindPushToken` | 绑定当前登录用户的系统推送 token | +| POST | `/hyapp.user.v1.UserDeviceService/DeletePushToken` | user-devices | `userDeletePushToken` | 失效当前登录用户的系统推送 token | +| POST | `/hyapp.user.v1.CountryAdminService/CreateCountry` | country-admin | `userCreateCountry` | 创建国家主数据 | +| POST | `/hyapp.user.v1.CountryAdminService/ListCountries` | country-admin | `userListCountries` | 查询国家主数据 | +| POST | `/hyapp.user.v1.CountryAdminService/UpdateCountry` | country-admin | `userUpdateCountry` | 修改国家展示字段 | +| POST | `/hyapp.user.v1.CountryAdminService/EnableCountry` | country-admin | `userEnableCountry` | 开放国家给 App 选择 | +| POST | `/hyapp.user.v1.CountryAdminService/DisableCountry` | country-admin | `userDisableCountry` | 关闭国家 App 可选状态 | +| POST | `/hyapp.user.v1.CountryQueryService/ListRegistrationCountries` | country-query | `userListRegistrationCountries` | 查询 App 注册页开放国家列表 | +| POST | `/hyapp.user.v1.RegionAdminService/CreateRegion` | region-admin | `userCreateRegion` | 创建区域并配置国家列表 | +| POST | `/hyapp.user.v1.RegionAdminService/ListRegions` | region-admin | `userListRegions` | 查询区域列表 | +| POST | `/hyapp.user.v1.RegionAdminService/GetRegion` | region-admin | `userGetRegion` | 查询单个区域 | +| POST | `/hyapp.user.v1.RegionAdminService/UpdateRegion` | region-admin | `userUpdateRegion` | 修改区域展示字段 | +| POST | `/hyapp.user.v1.RegionAdminService/ReplaceRegionCountries` | region-admin | `userReplaceRegionCountries` | 替换区域国家列表 | +| POST | `/hyapp.user.v1.RegionAdminService/DisableRegion` | region-admin | `userDisableRegion` | 停用区域 | +| POST | `/hyapp.user.v1.UserIdentityService/GetUserIdentity` | user-identity | `userGetUserIdentity` | 按 user_id 查询当前展示短号 | +| POST | `/hyapp.user.v1.UserIdentityService/ResolveDisplayUserID` | user-identity | `userResolveDisplayUserID` | 通过当前展示短号解析用户 | +| POST | `/hyapp.user.v1.UserIdentityService/ChangeDisplayUserID` | user-identity | `userChangeDisplayUserID` | 修改默认展示短号 | +| POST | `/hyapp.user.v1.UserIdentityService/ApplyPrettyDisplayUserID` | user-identity | `userApplyPrettyDisplayUserID` | 申请临时靓号 | +| POST | `/hyapp.user.v1.UserIdentityService/ExpirePrettyDisplayUserID` | user-identity | `userExpirePrettyDisplayUserID` | 主动触发靓号过期恢复 | +| POST | `/grpc.health.v1.Health/Check` | health | `userHealthCheck` | 标准 gRPC health check | +| POST | `/grpc.health.v1.Health/Watch` | health | `userHealthWatch` | 标准 gRPC health watch | +| POST | `/hyapp.room.v1.RoomCommandService/CreateRoom` | room-command | `roomCreateRoom` | 创建房间 | +| POST | `/hyapp.room.v1.RoomCommandService/JoinRoom` | room-command | `roomJoinRoom` | 加入房间业务 presence | +| POST | `/hyapp.room.v1.RoomCommandService/LeaveRoom` | room-command | `roomLeaveRoom` | 离开房间业务 presence | +| POST | `/hyapp.room.v1.RoomCommandService/MicUp` | room-command | `roomMicUp` | 上麦 | +| POST | `/hyapp.room.v1.RoomCommandService/MicDown` | room-command | `roomMicDown` | 下麦 | +| POST | `/hyapp.room.v1.RoomCommandService/ChangeMicSeat` | room-command | `roomChangeMicSeat` | 调整麦位 | +| POST | `/hyapp.room.v1.RoomCommandService/ConfirmMicPublishing` | room-command | `roomConfirmMicPublishing` | 确认 RTC 音频发布成功 | +| POST | `/hyapp.room.v1.RoomCommandService/SetMicSeatLock` | room-command | `roomSetMicSeatLock` | 锁定或解锁麦位 | +| POST | `/hyapp.room.v1.RoomCommandService/SetChatEnabled` | room-command | `roomSetChatEnabled` | 开启或关闭公屏 | +| POST | `/hyapp.room.v1.RoomCommandService/SetRoomAdmin` | room-command | `roomSetRoomAdmin` | 添加或移除房间管理员 | +| POST | `/hyapp.room.v1.RoomCommandService/TransferRoomHost` | room-command | `roomTransferRoomHost` | 转移主持人 | +| POST | `/hyapp.room.v1.RoomCommandService/MuteUser` | room-command | `roomMuteUser` | 设置房间内禁言 | +| POST | `/hyapp.room.v1.RoomCommandService/KickUser` | room-command | `roomKickUser` | 踢出房间用户 | +| POST | `/hyapp.room.v1.RoomCommandService/UnbanUser` | room-command | `roomUnbanUser` | 解除房间 ban | +| POST | `/hyapp.room.v1.RoomCommandService/SendGift` | room-command | `roomSendGift` | 发送礼物 | +| POST | `/hyapp.room.v1.RoomGuardService/CheckSpeakPermission` | room-guard | `roomCheckSpeakPermission` | 发言前守卫 | +| POST | `/hyapp.room.v1.RoomGuardService/VerifyRoomPresence` | room-guard | `roomVerifyRoomPresence` | 入群前 presence 守卫 | +| POST | `/hyapp.room.v1.RoomQueryService/GetCurrentRoom` | room-query | `roomGetCurrentRoom` | 查询用户当前可恢复房间 | +| POST | `/hyapp.room.v1.RoomQueryService/GetRoomSnapshot` | room-query | `roomGetRoomSnapshot` | 查询房间完整快照 | +| POST | `/grpc.health.v1.Health/Check` | health | `roomHealthCheck` | 标准 gRPC health check | +| POST | `/grpc.health.v1.Health/Watch` | health | `roomHealthWatch` | 标准 gRPC health watch | +| POST | `/hyapp.wallet.v1.WalletService/DebitGift` | wallet | `walletDebitGift` | 送礼扣费 | +| POST | `/grpc.health.v1.Health/Check` | health | `walletHealthCheck` | 标准 gRPC health check | +| POST | `/grpc.health.v1.Health/Watch` | health | `walletHealthWatch` | 标准 gRPC health watch | +| POST | `/hyapp.activity.v1.ActivityService/PingActivity` | activity | `activityPingActivity` | 活动服务连通性探测 | +| POST | `/hyapp.activity.v1.ActivityService/GetActivityStatus` | activity | `activityGetActivityStatus` | 查询活动状态 | +| POST | `/hyapp.activity.v1.MessageInboxService/ListMessageTabs` | messages | `activityListMessageTabs` | 查询 App 消息 tab 摘要 | +| POST | `/hyapp.activity.v1.MessageInboxService/ListInboxMessages` | messages | `activityListInboxMessages` | 分页查询 system/activity inbox | +| POST | `/hyapp.activity.v1.MessageInboxService/MarkInboxMessageRead` | messages | `activityMarkInboxMessageRead` | 标记单条 inbox 消息已读 | +| POST | `/hyapp.activity.v1.MessageInboxService/MarkInboxSectionRead` | messages | `activityMarkInboxSectionRead` | 标记一个消息分区全部已读 | +| POST | `/hyapp.activity.v1.MessageInboxService/DeleteInboxMessage` | messages | `activityDeleteInboxMessage` | 删除当前用户可见 inbox 消息 | +| POST | `/hyapp.activity.v1.MessageInboxService/CreateInboxMessage` | messages | `activityCreateInboxMessage` | 生产者创建单用户 inbox 消息 | +| POST | `/hyapp.activity.v1.MessageInboxService/CreateFanoutJob` | messages | `activityCreateFanoutJob` | 创建后台 fanout 任务 | +| POST | `/grpc.health.v1.Health/Check` | health | `activityHealthCheck` | 标准 gRPC health check | +| POST | `/grpc.health.v1.Health/Watch` | health | `activityHealthWatch` | 标准 gRPC health watch | + diff --git a/docs/api-test-flows.md b/docs/api-test-flows.md new file mode 100644 index 00000000..0e09f098 --- /dev/null +++ b/docs/api-test-flows.md @@ -0,0 +1,88 @@ +# API 测试链路 + +本文件记录 `docs/api-test-harness.html` 内置的流程化链路。HTML 只直接执行 HTTP API;腾讯云 IM/TRTC SDK 动作作为客户端步骤列出,不伪造成后端接口。 + +## 01 三方注册/登录 -> 资料 -> 国家 -> token 生命周期 + +1. `GET /healthz/ready`:确认 gateway 可接流量。 +2. `GET /api/v1/countries`:确认注册页国家主数据可读。 +3. `POST /api/v1/auth/third-party/login`:使用真实 Firebase/Google ID token 登录或注册,写入 username/avatar/gender/birth/country/device/source 等注册字段。 +4. `GET /api/v1/users/me/identity`:确认 user_id/display_user_id 映射。 +5. `GET /api/v1/users/me/overview`:确认我的页聚合数据。 +6. `POST /api/v1/users/me/profile/update`:修改用户名、头像、生日。 +7. `POST /api/v1/users/me/country/change`:修改国家;同一用户一个月只允许一次,重复执行应返回冲突。 +8. `POST /api/v1/auth/password/set`:首次设置密码。 +9. `POST /api/v1/auth/token/refresh`:刷新 access token。 +10. `POST /api/v1/auth/logout`:失效 refresh session。 + +## 02 密码登录 -> refresh -> logout + +1. `POST /api/v1/auth/account/login`:账号是当前展示短号。 +2. `GET /api/v1/users/me/identity`:验证登录态。 +3. `POST /api/v1/auth/token/refresh`:验证 refresh 轮换。 +4. `POST /api/v1/auth/logout`:验证 session 失效。 + +## 03 房间 -> IM -> RTC -> MicUp -> 发流确认 -> 下麦离房 + +1. `POST /api/v1/rooms/create`:创建房间,成功后回填 `room_id`。 +2. `POST /api/v1/rooms/join`:加入业务 presence。 +3. `GET /api/v1/rooms/current`:确认当前可恢复房间。 +4. `GET /api/v1/im/usersig`:获取 IM `sdk_app_id/user_id/user_sig`。 +5. 客户端 IM SDK:`TIM.create + login`,房间群 ID 使用 `room_id`。 +6. `POST /api/v1/rtc/token`:获取 RTC `sdk_app_id/rtc_user_id/user_sig/str_room_id`。 +7. 客户端 TRTC SDK:使用 `str_room_id` 进房。 +8. `POST /api/v1/rooms/mic/up`:麦位进入 `pending_publish`,回填 `mic_session_id/room_version`。 +9. 客户端 TRTC SDK:`switchRole(anchor) + startLocalAudio`。 +10. `POST /api/v1/rooms/mic/publishing/confirm`:带 `mic_session_id/room_version/event_time_ms` 确认发布。 +11. `GET /api/v1/rooms/snapshot`:确认麦位进入 `publishing`。 +12. `POST /api/v1/rooms/heartbeat`:刷新业务 presence。 +13. `POST /api/v1/rooms/mic/down`:下麦。 +14. `POST /api/v1/rooms/leave`:离房。 + +## 04 MicUp 未确认 -> publish_timeout 自动释放验证 + +1. `POST /api/v1/rooms/create` +2. `POST /api/v1/rooms/join` +3. `POST /api/v1/rtc/token` +4. `POST /api/v1/rooms/mic/up` +5. 不调用 `ConfirmMicPublishing`,等待 `publish_deadline_ms` 或服务配置 deadline。 +6. `GET /api/v1/rooms/current` +7. `GET /api/v1/rooms/snapshot`:确认麦位释放或状态转移符合 `publish_timeout`。 + +## 05 App 消息 tab/inbox 读写链路 + +1. `GET /api/v1/messages/tabs` +2. `GET /api/v1/messages` +3. `POST /api/v1/messages/{message_id}/read` +4. `POST /api/v1/messages/read-all` +5. `DELETE /api/v1/messages/{message_id}` + +## 06 资源/礼物/送礼/币商转账链路 + +1. `GET /api/v1/resources` +2. `GET /api/v1/resource-groups/{group_id}` +3. `GET /api/v1/gifts` +4. `GET /api/v1/users/me/resources` +5. `POST /api/v1/users/me/resources/{resource_id}/equip` +6. `POST /api/v1/rooms/gift/send` +7. `POST /api/v1/wallet/coin-seller/transfer` + +## 07 后台国家/区域配置链路 + +1. `POST /api/v1/auth/login` +2. `GET /api/v1/auth/me` +3. `GET /api/v1/admin/countries` +4. `POST /api/v1/admin/countries` +5. `POST /api/v1/admin/countries/{country_id}/enable` +6. `GET /api/v1/admin/regions` +7. `POST /api/v1/admin/regions` +8. `PUT /api/v1/admin/regions/{region_id}/countries` +9. `POST /api/v1/admin/regions/{region_id}/enable` + +## 08 后台币商库存入账 -> App 币商转账 + +1. `POST /api/v1/auth/login` +2. `GET /api/v1/admin/coin-sellers` +3. `POST /api/v1/admin/coin-sellers/{user_id}/stock-credits` +4. `POST /api/v1/wallet/coin-seller/transfer` +5. `GET /api/v1/admin/payment/recharge-bills` diff --git a/docs/api-test-harness.html b/docs/api-test-harness.html new file mode 100644 index 00000000..3647cd5b --- /dev/null +++ b/docs/api-test-harness.html @@ -0,0 +1,2794 @@ + + + + + + HY API Flow Harness + + + +
+

HY API Flow Harness

+

页面只保留接口调用器形态:流程链路把 API、客户端 SDK 动作、人工校验按顺序列清楚;可逐步执行,也可执行当前链路里的 API 步骤。

+
+ +
+
+

调用配置

+
建议在仓库根目录执行 python3 -m http.server 19090,然后访问 http://localhost:19090/docs/api-test-harness.html。所有业务接口仍以 gateway/admin 实际 CORS 配置为准。
+
+ + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+ +
+

流程化接口调用器

+
+ + + +
+
+
+
+ +
+

全部接口调用器

+
+ + + + +
+
+
+
+ +
+

日志

+

+    
+
+ + + + diff --git a/docs/coin-seller-stock-credit-architecture.md b/docs/coin-seller-stock-credit-architecture.md new file mode 100644 index 00000000..149b17fd --- /dev/null +++ b/docs/coin-seller-stock-credit-architecture.md @@ -0,0 +1,422 @@ +# 币商进货与金币补偿架构设计 + +## 目标 + +币商管理列表增加“加金币”按钮。运营点击后打开“币商进货”弹窗,给币商的专用金币账户 `COIN_SELLER_COIN` 增加库存。 + +表单字段: + +- `充值金币`:增加到币商库存的金币数量,必填。 +- `类型`:`USDT进货` / `金币补偿`,必填。 +- `充值金额`:币商线下支付给平台的 USDT 金额,仅 `USDT进货` 必填。 + +业务口径: + +- `USDT进货`:平台收到币商线下 USDT 后,后台给币商发放 `COIN_SELLER_COIN` 库存;这笔记录计入币商进货/充值统计。 +- `金币补偿`:平台因为异常、活动、人工修正等原因给币商补库存;没有充值金额,不计入币商进货/充值统计。 +- 两种类型都不能给币商发放 `USD_BALANCE`,也不能直接影响玩家普通 `COIN`。 + +## 服务边界 + +| 模块 | 职责 | +| --- | --- | +| `server/admin` | 币商列表、按钮权限、弹窗表单、后台审计、调用 user-service 校验币商身份、调用 wallet-service 发放库存 | +| `user-service` | 保存 `coin_seller_profiles` 身份事实,判断目标用户是否是有效币商 | +| `wallet-service` | 拥有 `COIN_SELLER_COIN` 账户、钱包流水、币商进货记录、幂等和 outbox | +| `gateway-service` | 不参与后台币商进货;App 币商给玩家转金币仍走现有 `/api/v1/wallet/coin-seller/transfer` | + +边界规则: + +- `server/admin` 不能直接改 `wallet_accounts`。 +- `user-service` 不能保存币商余额。 +- `wallet-service` 不判断后台菜单权限,但必须保存完整账务流水和进货记录。 +- 币商库存入账必须走钱包命令,不能用通用 SQL 修余额。 + +## 后台交互 + +### 币商列表 + +现有币商列表继续展示: + +- 用户 ID / 短号 +- 昵称 / 头像 +- 区域 +- 币商状态 +- 币商库存余额 `merchantBalance` +- 创建时间 / 更新时间 + +新增行操作: + +| 按钮 | 权限 | 状态 | +| --- | --- | --- | +| `加金币` | `coin-seller:stock-credit` | 仅 active 币商可点击;disabled 币商按钮置灰 | + +如果后续产品要求 disabled 币商也能补偿库存,必须新增单独权限或二次确认,不在首版默认放开。 + +### 弹窗表单 + +弹窗标题:`币商进货` + +只读信息: + +- 币商用户 ID +- 币商短号 +- 昵称 +- 当前库存余额 +- 区域 + +可编辑字段: + +| 字段 | 类型 | 规则 | +| --- | --- | --- | +| `type` | 单选 | `usdt_purchase` / `coin_compensation` | +| `coin_amount` | 正整数 | 两种类型都必填,表示增加的 `COIN_SELLER_COIN` | +| `recharge_amount` | 金额 | 仅 `usdt_purchase` 展示并必填;`coin_compensation` 不展示也不提交 | +| `payment_ref` | 文本 | `usdt_purchase` 必填,建议填链上 tx hash、收款单号或财务凭证号 | +| `evidence_ref` | 文本 | 凭证附件或后台上传后的文件 ID;首版建议两种类型都必填 | +| `reason` | 文本 | 必填,写清楚进货或补偿原因 | +| `command_id` | 文本 | 前端生成或 admin-server 生成,用于幂等 | + +金额存储规则: + +- `recharge_amount` 前端可以展示小数,例如 `120.50`。 +- 服务端统一转成 `paid_amount_micro`,USDT 按 6 位精度保存。 +- `coin_compensation` 必须满足 `paid_currency_code=''` 且 `paid_amount_micro=0`,如果客户端传了金额,服务端直接拒绝。 + +## 账务模型 + +币商库存仍然是钱包资产: + +- 资产类型:`COIN_SELLER_COIN` +- 账户表:`wallet_accounts` +- 流水表:`wallet_transactions` +- 分录表:`wallet_entries` +- 事件表:`wallet_outbox` + +新增专门的币商进货明细表,避免从通用钱包 metadata 里做统计。 + +```sql +coin_seller_stock_records( + app_code VARCHAR(32) NOT NULL DEFAULT 'lalu', + stock_id VARCHAR(96) NOT NULL, + transaction_id VARCHAR(96) NOT NULL, + command_id VARCHAR(128) NOT NULL, + seller_user_id BIGINT NOT NULL, + stock_type VARCHAR(32) NOT NULL, + counts_as_seller_recharge BOOLEAN NOT NULL, + coin_amount BIGINT NOT NULL, + paid_currency_code VARCHAR(8) NOT NULL DEFAULT '', + paid_amount_micro BIGINT NOT NULL DEFAULT 0, + payment_ref VARCHAR(128) NULL, + evidence_ref VARCHAR(256) NOT NULL DEFAULT '', + operator_user_id BIGINT NOT NULL, + reason VARCHAR(512) NOT NULL DEFAULT '', + balance_after BIGINT NOT NULL, + metadata_json JSON NULL, + created_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, stock_id), + UNIQUE KEY uk_coin_seller_stock_command (app_code, command_id), + UNIQUE KEY uk_coin_seller_stock_payment_ref (app_code, stock_type, payment_ref), + KEY idx_coin_seller_stock_seller_time (app_code, seller_user_id, created_at_ms), + KEY idx_coin_seller_stock_type_time (app_code, stock_type, created_at_ms) +); +``` + +字段语义: + +- `stock_type='usdt_purchase'`:`counts_as_seller_recharge=true`,`paid_currency_code='USDT'`,`paid_amount_micro>0`。 +- `stock_type='coin_compensation'`:`counts_as_seller_recharge=false`,`paid_currency_code=''`,`paid_amount_micro=0`。 +- `coin_amount` 是发给币商的库存金币,不是玩家充值金币。 +- `payment_ref` 对 USDT 进货必须唯一,防止同一笔线下收款重复入账。 +- `balance_after` 保存账后币商库存,后台详情不用再回放钱包分录。 + +注意:这张表不是 `wallet_recharge_records`。`wallet_recharge_records` 只记录币商给玩家转金币后形成的“玩家充值事实”。币商自己进货应该单独统计,不能混到玩家充值记录里。 + +## 业务类型 + +钱包交易 `biz_type` 建议拆成两个值: + +| `biz_type` | 说明 | 是否计入币商充值统计 | +| --- | --- | --- | +| `coin_seller_stock_purchase` | USDT 进货,平台收到币商付款后发放库存 | 是 | +| `coin_seller_coin_compensation` | 人工金币补偿、异常修复、活动补库存 | 否 | + +不要复用 `manual_credit`,因为通用后台入账无法表达: + +- 是否是币商库存 +- 是否是 USDT 进货 +- 是否计入币商充值统计 +- 线下付款凭证是否唯一 +- 金币补偿没有充值金额这个约束 + +## 主链路 + +```mermaid +sequenceDiagram + autonumber + participant Admin as Admin Web + participant AdminSrv as server/admin + participant User as user-service + participant Wallet as wallet-service + participant DB as wallet MySQL + participant Audit as admin_operation_logs + + Admin->>AdminSrv: POST /admin/coin-sellers/{user_id}/stock-credits + AdminSrv->>AdminSrv: 校验 coin-seller:stock-credit 权限和表单 + AdminSrv->>User: GetCoinSellerProfile(user_id) + User-->>AdminSrv: active coin_seller profile + AdminSrv->>Wallet: AdminCreditCoinSellerStock(command) + Wallet->>DB: BEGIN + Wallet->>DB: 按 command_id / payment_ref 做幂等和重复收款校验 + Wallet->>DB: SELECT wallet_accounts FOR UPDATE + Wallet->>DB: 增加 COIN_SELLER_COIN + Wallet->>DB: 写 wallet_transactions / wallet_entries + Wallet->>DB: 写 coin_seller_stock_records + Wallet->>DB: 写 wallet_outbox + Wallet->>DB: COMMIT + AdminSrv->>Audit: 写后台操作审计 + AdminSrv-->>Admin: 返回 transaction_id 和账后余额 +``` + +失败边界: + +- user-service 判断不是 active 币商时,不调用 wallet-service。 +- wallet-service 入账失败时,不写成功审计;可以写失败审计或错误日志。 +- 钱包事务提交成功但后台审计写失败时,必须记录告警;后续需要审计补偿任务,不能静默吞掉。 + +## Admin API + +新增后台接口: + +```http +POST /admin/coin-sellers/{user_id}/stock-credits +``` + +权限: + +```text +coin-seller:stock-credit +``` + +请求: + +```json +{ + "commandId": "admin-coin-seller-stock-20260509-001", + "type": "usdt_purchase", + "coinAmount": 8000000, + "rechargeAmount": "100.000000", + "paymentRef": "usdt-tx-hash-or-offline-receipt", + "evidenceRef": "oss://admin-evidence/receipt.png", + "reason": "币商 USDT 进货" +} +``` + +金币补偿请求: + +```json +{ + "commandId": "admin-coin-seller-comp-20260509-001", + "type": "coin_compensation", + "coinAmount": 50000, + "evidenceRef": "ticket-12345", + "reason": "上次转账异常补偿" +} +``` + +响应: + +```json +{ + "transactionId": "lalu_admin_coin_seller_stock_...", + "sellerUserId": "163001", + "stockType": "usdt_purchase", + "coinAmount": 8000000, + "paidCurrencyCode": "USDT", + "paidAmountMicro": 100000000, + "countsAsSellerRecharge": true, + "balanceAfter": 12800000, + "createdAtMs": 1760000000000 +} +``` + +校验: + +- `user_id` 必须是 active `coin_seller`。 +- `coinAmount > 0`。 +- `type=usdt_purchase` 时,`rechargeAmount > 0`、`paymentRef` 必填、`paid_currency_code=USDT`。 +- `type=coin_compensation` 时,不能提交 `rechargeAmount`,`paid_amount_micro` 固定为 0。 +- `reason` 必填。 +- `evidenceRef` 首版建议必填,方便财务和客服追踪。 + +## gRPC 契约建议 + +在 `wallet.v1.WalletService` 增加后台命令: + +```protobuf +message AdminCreditCoinSellerStockRequest { + string command_id = 1; + int64 seller_user_id = 2; + string stock_type = 3; + int64 coin_amount = 4; + string paid_currency_code = 5; + int64 paid_amount_micro = 6; + string payment_ref = 7; + string evidence_ref = 8; + int64 operator_user_id = 9; + string reason = 10; + string app_code = 11; +} + +message AdminCreditCoinSellerStockResponse { + string transaction_id = 1; + int64 seller_user_id = 2; + string stock_type = 3; + int64 coin_amount = 4; + string paid_currency_code = 5; + int64 paid_amount_micro = 6; + bool counts_as_seller_recharge = 7; + int64 balance_after = 8; + int64 created_at_ms = 9; +} +``` + +`server/admin` 负责在调用前通过 `user-service` 校验币商身份;`wallet-service` 负责校验金额、类型、幂等、重复 `payment_ref` 和钱包事务。 + +## 统计口径 + +币商库存相关统计从 `coin_seller_stock_records` 读取: + +| 指标 | 查询条件 | +| --- | --- | +| 币商累计 USDT 进货金币 | `stock_type='usdt_purchase'` | +| 币商累计 USDT 进货金额 | `counts_as_seller_recharge=true`,汇总 `paid_amount_micro` | +| 币商累计金币补偿 | `stock_type='coin_compensation'` | +| 当前库存余额 | `wallet_accounts.asset_type='COIN_SELLER_COIN'` | +| 币商给玩家充值金币 | 现有 `wallet_recharge_records` 或 `biz_type='coin_seller_transfer'` | + +金币补偿不计入币商充值统计的落地方式: + +- 表单不展示 `充值金额`。 +- API 不接受 `rechargeAmount`。 +- 数据库存 `paid_amount_micro=0`。 +- `counts_as_seller_recharge=false`。 +- 统计 SQL 必须带 `counts_as_seller_recharge=true` 或 `stock_type='usdt_purchase'`。 + +## 列表性能 + +币商列表不要每行实时做大范围流水汇总。 + +首版建议: + +- 列表只展示当前库存余额 `merchantBalance`,继续按当前实现从 `wallet_accounts` 按当页 user_id 批量查询。 +- 如果产品要展示累计进货金额、累计补偿金币、最近进货时间,只对当前页 user_id 从 `coin_seller_stock_records` 做 group by。 +- 如果后续需要按累计进货金额排序或筛选,再增加 `coin_seller_stock_stats` 投影表。 + +可选投影表: + +```sql +coin_seller_stock_stats( + app_code VARCHAR(32) NOT NULL DEFAULT 'lalu', + seller_user_id BIGINT NOT NULL, + purchase_coin_total BIGINT NOT NULL DEFAULT 0, + purchase_paid_usdt_micro_total BIGINT NOT NULL DEFAULT 0, + compensation_coin_total BIGINT NOT NULL DEFAULT 0, + last_stock_at_ms BIGINT NOT NULL DEFAULT 0, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, seller_user_id) +); +``` + +投影更新可以和钱包事务同事务完成,也可以由 wallet outbox 异步更新。首版如果列表不展示累计值,不需要这张表。 + +## Outbox 事件 + +钱包事务成功后写出: + +- `WalletCoinSellerStockPurchased` +- `WalletCoinSellerCoinCompensated` +- `WalletBalanceChanged` + +事件 payload: + +- `seller_user_id` +- `stock_type` +- `coin_amount` +- `paid_currency_code` +- `paid_amount_micro` +- `counts_as_seller_recharge` +- `transaction_id` +- `operator_user_id` +- `created_at_ms` + +这些事件可用于后台通知、财务报表、风控告警和统计投影。 + +## 权限和审计 + +新增权限: + +| Permission | Kind | Usage | +| --- | --- | --- | +| `coin-seller:stock-credit` | `button` | 币商进货、金币补偿 | + +审计 action: + +| Action | Resource | 说明 | +| --- | --- | --- | +| `coin-seller-stock-credit` | `coin_seller_stock_records` | USDT 进货或金币补偿成功 | + +审计 detail 必须包含: + +- `command_id` +- `seller_user_id` +- `stock_type` +- `coin_amount` +- `paid_currency_code` +- `paid_amount_micro` +- `payment_ref` +- `transaction_id` +- `reason` + +USDT 进货金额属于财务口径,不能只放前端日志。 + +## 错误码建议 + +纳入 `pkg/xerr` catalog: + +| 业务错误 | gRPC code | 说明 | +| --- | --- | --- | +| `COIN_SELLER_NOT_ACTIVE` | `FailedPrecondition` | 目标用户不是 active 币商 | +| `COIN_SELLER_STOCK_TYPE_INVALID` | `InvalidArgument` | 类型不是 USDT 进货或金币补偿 | +| `COIN_SELLER_STOCK_AMOUNT_INVALID` | `InvalidArgument` | 金币数或 USDT 金额不合法 | +| `COIN_SELLER_PAYMENT_REF_DUPLICATED` | `AlreadyExists` | 同一 USDT 凭证已入账 | +| `IDEMPOTENCY_CONFLICT` | `AlreadyExists` | command_id 重复但 payload 不同 | + +客户端展示文案由 admin 前端根据业务 code 转换,不从内部错误字符串拼 UI。 + +## 实现顺序 + +1. 增加权限种子:`coin-seller:stock-credit`,币商列表行操作按权限展示。 +2. 扩展 `api/proto/wallet/v1/wallet.proto`:新增 `AdminCreditCoinSellerStock` 请求和响应。 +3. 增加 wallet MySQL 表 `coin_seller_stock_records`。 +4. 在 `wallet-service` 实现专用命令:校验类型、金额、payment_ref、幂等,锁定 `COIN_SELLER_COIN` 账户并入账。 +5. 在 `server/admin` 增加接口:校验 active 币商身份,调用 wallet-service,成功后写 `admin_operation_logs`。 +6. 后台页面币商列表增加“加金币”按钮和弹窗表单。 +7. 增加列表成功后的刷新策略:成功后刷新当前行余额或重新拉取当前页。 +8. 如需累计统计,再增加当前页聚合查询或 `coin_seller_stock_stats` 投影。 + +## 测试重点 + +必须覆盖: + +- active 币商 USDT 进货成功,`COIN_SELLER_COIN` 增加,`counts_as_seller_recharge=true`。 +- USDT 进货缺少 `rechargeAmount` 或 `paymentRef` 被拒绝。 +- 同一个 `paymentRef` 不能重复进货。 +- 金币补偿成功,`COIN_SELLER_COIN` 增加,`paid_amount_micro=0`,`counts_as_seller_recharge=false`。 +- 金币补偿传入 `rechargeAmount` 被拒绝。 +- disabled 或不存在的币商不能加金币。 +- 相同 `command_id` 和相同 payload 重试返回原交易结果。 +- 相同 `command_id` 但 payload 不同返回幂等冲突。 +- wallet 事务失败时不写进货记录、不改余额。 +- admin 审计记录包含 transaction_id、类型、金币数、USDT 金额和操作人。 + diff --git a/docs/logging-infrastructure-architecture.md b/docs/logging-infrastructure-architecture.md new file mode 100644 index 00000000..3bcb3000 --- /dev/null +++ b/docs/logging-infrastructure-architecture.md @@ -0,0 +1,447 @@ +# 日志基础设施整理方案 + +## 目标 + +当前服务已经有基础日志能力:HTTP/gRPC 边界日志在 `pkg/logx`,服务启动和 worker 使用标准库 `log` 输出。但生产环境需要把日志整理成稳定、可检索、可告警、可控成本的基础设施。 + +目标: + +- 所有服务统一输出 JSON 结构化日志到 stdout/stderr。 +- 业务代码不直接依赖腾讯云 CLS SDK。 +- 部署层采集容器 stdout/stderr 到腾讯云 CLS。 +- 日志字段统一,支持按 `request_id`、`command_id`、`user_id`、`room_id`、`transaction_id` 追踪。 +- 敏感字段默认脱敏,避免 token、密码、支付凭证、UserSig、SecretKey 进入日志。 +- 保留后台审计日志和业务账务流水,不用运行日志替代审计和账务事实。 + +非目标: + +- 首版不做完整链路追踪系统。 +- 首版不引入重型日志框架。 +- 首版不在代码里直连 CLS API 写日志。 +- 首版不采集客户端 SDK 日志;客户端日志和崩溃分析后续单独设计。 + +## 当前现状 + +已有能力: + +- `gateway-service` HTTP 入口接入 `logx.HTTPMiddleware`。 +- `room-service`、`wallet-service`、`user-service`、`activity-service` gRPC server 接入 `logx.UnaryServerInterceptor`。 +- `logx` 会记录请求、响应、状态码、错误 reason、耗时和 `request_id`。 +- `logx` 已经对 `password`、`token`、`secret`、`credential`、`user_sig`、`authorization` 等字段做脱敏。 +- 健康检查不会进入高频 access log。 + +主要问题: + +- App 后端服务已接入 `logx.Init`,默认输出 stdout JSON;`log.Printf` 会被兜底包成 `stdlog` JSON。 +- 核心 worker、RTC callback、room outbox 已改为结构化字段日志。 +- admin-server 是独立 Go module,按项目边界在 `server/admin/internal/platform/logging` 接入同一 stdout JSON 口径,不 import `hyapp/pkg/logx`。 +- access log 默认不记录 request/response body;本地可通过配置临时打开,生产需要继续按接口和环境控制体积。 +- 没有部署层日志采集配置,也没有腾讯云 CLS logset/topic 规划。 + +## 总体架构 + +```mermaid +flowchart LR + S1["gateway-service"] --> STDOUT["stdout/stderr JSON logs"] + S2["room-service"] --> STDOUT + S3["wallet-service"] --> STDOUT + S4["user-service"] --> STDOUT + S5["activity-service"] --> STDOUT + S6["hyapp-admin-server"] --> STDOUT + + STDOUT --> Runtime["Container Runtime"] + Runtime --> Collector["TKE Log Collector / CLS LogListener"] + Collector --> CLS["Tencent Cloud CLS"] + CLS --> Search["检索与排障"] + CLS --> Dashboard["仪表盘"] + CLS --> Alarm["告警策略"] +``` + +核心决策: + +- 服务只负责输出标准 JSON 日志。 +- 日志采集由部署层负责。 +- 本地开发继续用 stdout,`docker compose logs` 可查看。 +- 生产环境通过腾讯云容器日志采集或 CLS LogListener 采集 stdout。 +- 后台审计继续写 `admin_operation_logs`,钱包账务继续写 `wallet_transactions`、`wallet_entries`,不能用运行日志替代。 + +## 日志等级 + +统一使用: + +| Level | 使用场景 | +| --- | --- | +| `debug` | 本地调试,生产默认关闭 | +| `info` | 正常业务事件、access log、worker 成功摘要 | +| `warn` | 可恢复异常、降级、重试、外部依赖短暂失败 | +| `error` | 请求失败、事务失败、外部依赖不可用、worker 批次失败 | + +配置项: + +```yaml +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 +``` + +本地可以打开 body: + +```yaml +log: + level: debug + format: text + include_request_body: true + include_response_body: true + max_payload_bytes: 8192 +``` + +生产默认不记录完整 request/response body,只记录关键字段和错误摘要。 + +## 统一字段规范 + +每条日志都应该尽量包含基础字段: + +| 字段 | 说明 | +| --- | --- | +| `time` | 日志时间 | +| `level` | `debug/info/warn/error` | +| `msg` | 短事件名,例如 `http_access`、`grpc_access`、`wallet_transaction_failed` | +| `env` | `local/dev/staging/prod` | +| `app_code` | 多 App 租户标识 | +| `service` | 服务名 | +| `version` | 镜像版本或 git sha | +| `node_id` | 当前服务节点 | +| `request_id` | 请求追踪 ID | + +业务字段按场景补充: + +| 场景 | 推荐字段 | +| --- | --- | +| HTTP | `method`、`path`、`status`、`duration_ms`、`client_ip` | +| gRPC | `grpc_method`、`grpc_code`、`reason`、`duration_ms` | +| 房间 | `room_id`、`room_version`、`command_id`、`user_id` | +| 钱包 | `transaction_id`、`command_id`、`user_id`、`asset_type`、`biz_type` | +| 礼物 | `room_id`、`sender_user_id`、`target_user_id`、`gift_id`、`gift_count` | +| 币商 | `seller_user_id`、`target_user_id`、`amount`、`transaction_id` | +| worker | `worker`、`worker_id`、`batch_size`、`retry_count` | +| 外部依赖 | `provider`、`endpoint`、`external_code`、`duration_ms` | + +字段命名固定使用 snake_case,和 protobuf/mysql 字段风格一致,方便 CLS 查询。 + +## `pkg/logx` 改造 + +`pkg/logx` 应该从“access log middleware”升级成“统一日志入口”。 + +建议新增: + +```go +type Config struct { + Service string + Env string + Version string + NodeID string + Level string + Format string + IncludeRequestBody bool + IncludeResponseBody bool + MaxPayloadBytes int +} + +func Init(cfg Config) error +func Logger(ctx context.Context) *slog.Logger +func With(ctx context.Context, attrs ...slog.Attr) context.Context +func Info(ctx context.Context, msg string, attrs ...slog.Attr) +func Warn(ctx context.Context, msg string, attrs ...slog.Attr) +func Error(ctx context.Context, msg string, err error, attrs ...slog.Attr) +``` + +改造原则: + +- 服务启动时调用 `logx.Init` 设置全局 logger。 +- `log.Printf` 逐步替换成 `logx.Info/Warn/Error` 或 `slog` 结构化字段。 +- HTTP/gRPC middleware 自动把 `request_id`、`service`、`app_code` 写入日志上下文。 +- `max_payload_bytes` 从配置读取,不写死。 +- 生产默认不记录完整 body;只有错误或 debug 环境才允许记录脱敏 body。 + +## Access Log 策略 + +HTTP access log: + +- 成功请求记录一条 `info`。 +- 4xx 记录 `warn` 或 `info`,取决于业务错误类型。 +- 5xx 记录 `error`。 +- healthcheck 不记录。 +- request/response body 默认关闭。 + +gRPC access log: + +- `codes.OK` 记录 `info`。 +- 业务前置失败,例如 `InvalidArgument`、`FailedPrecondition`,记录 `warn` 或 `info`。 +- `Internal`、`Unavailable`、`DeadlineExceeded` 记录 `error`。 +- `grpc.health.v1.Health` 不记录。 + +建议生产日志示例: + +```json +{ + "time": "2026-05-09T12:00:00.000Z", + "level": "INFO", + "msg": "grpc_access", + "env": "prod", + "service": "wallet-service", + "request_id": "req-123", + "app_code": "lalu", + "grpc_method": "/hyapp.wallet.v1.WalletService/TransferCoinFromSeller", + "grpc_code": "OK", + "reason": "", + "duration_ms": 18, + "command_id": "cmd-456", + "seller_user_id": 1001, + "target_user_id": 2002, + "transaction_id": "wtx-789" +} +``` + +## 敏感字段规则 + +禁止进入日志: + +- 明文密码 +- Firebase ID token +- Google/Apple provider credential +- refresh token / access token +- 腾讯云 SecretId / SecretKey +- Tencent IM UserSig +- RTC token +- 支付 provider receipt 原文 +- USDT 完整链上凭证如包含敏感内部备注时,最多记录摘要 +- 身份证件、银行卡、提现收款账号全文 + +可以记录: + +- `request_id` +- `command_id` +- `transaction_id` +- `room_id` +- `user_id` +- `provider` +- provider 错误码 +- token/receipt 的 hash 摘要,前提是确实需要排障 + +`logx` 的脱敏 key 列表需要继续集中维护,禁止业务代码自己拼接敏感原文。 + +## 腾讯云 CLS 接入方案 + +推荐方案:stdout 采集到 CLS。 + +### 方案 A:TKE 容器标准输出采集 + +如果服务部署在腾讯云 TKE: + +1. 每个服务输出 JSON 到 stdout/stderr。 +2. TKE 开启日志采集。 +3. 采集目标选择容器标准输出。 +4. 解析方式选择 JSON。 +5. 不同环境写入不同 CLS topic。 + +推荐 topic: + +| 环境 | Logset | Topic | +| --- | --- | --- | +| dev | `hyapp-dev` | `service-logs` | +| staging | `hyapp-staging` | `service-logs` | +| prod | `hyapp-prod` | `service-logs` | +| prod | `hyapp-prod` | `admin-audit-runtime` | +| prod | `hyapp-prod` | `wallet-security` | + +首版可以所有服务进同一个 `service-logs` topic,通过 `service` 字段过滤。等日志量变大,再把 `gateway-service`、`wallet-service`、`room-service` 拆 topic。 + +### 方案 B:CVM / Docker 部署使用 CLS LogListener + +如果服务部署在 CVM Docker: + +1. 服务仍输出 JSON 到 stdout。 +2. Docker 日志落到宿主机默认 json log 文件。 +3. 安装 CLS LogListener。 +4. LogListener 采集容器日志文件或指定目录。 +5. CLS 端按 JSON 解析。 + +不建议在每个 Go 服务里集成 CLS SDK。直连 SDK 会把业务进程和日志平台绑定,CLS 抖动时还可能影响业务路径。 + +## CLS 索引字段 + +CLS topic 需要开启键值索引。 + +建议索引字段: + +- `level` +- `env` +- `service` +- `app_code` +- `request_id` +- `command_id` +- `user_id` +- `room_id` +- `transaction_id` +- `biz_type` +- `grpc_code` +- `reason` +- `status` +- `path` +- `worker` +- `event_type` + +全文索引可以保留,但排障优先用字段检索,避免只靠模糊搜索。 + +## 告警策略 + +首版告警不要太多,先覆盖最关键的错误和积压。 + +建议: + +| 告警 | 条件 | +| --- | --- | +| 服务 5xx 增多 | `service=gateway-service AND status>=500` 5 分钟超过阈值 | +| gRPC Internal | `grpc_code=Internal` 5 分钟超过阈值 | +| 钱包失败 | `service=wallet-service AND level=ERROR` 1 分钟超过阈值 | +| room outbox 积压 | worker 日志出现 `status=batch_failed` 或 retryable 数量持续增长 | +| 腾讯云 IM/RTC 回调异常 | `provider=tencent AND level>=WARN` 超过阈值 | +| 用户登录失败突增 | `msg=auth_failed` 或 `reason=AUTH_FAILED` 超过阈值 | + +告警通知: + +- 开发阶段:企业微信/飞书群。 +- 生产阶段:按服务 owner 分组。 +- 钱包和支付相关 error 单独告警,不能淹没在普通业务 error 里。 + +## 保留周期和成本控制 + +建议: + +| 环境 | 保留时间 | +| --- | --- | +| local | 不上传 CLS | +| dev | 3-7 天 | +| staging | 7-14 天 | +| prod 普通服务日志 | 30 天 | +| prod 钱包/支付安全日志 | 90-180 天,按合规要求调整 | + +成本控制: + +- 生产关闭 request/response body。 +- 高频成功日志只保留摘要。 +- healthcheck 不采集或不输出。 +- 大 payload 截断。 +- 对重复 worker 成功日志做批次汇总,不逐条刷屏。 +- 对外部 callback 原始 body 只在 debug 或白名单场景短期开启。 + +## 和审计日志的关系 + +运行日志和审计日志是两套东西。 + +运行日志: + +- 用于排障、告警、性能分析。 +- 存在 CLS。 +- 可以按保留周期清理。 + +审计日志: + +- 用于追责、后台操作记录、财务追踪。 +- 存在业务数据库,例如 `admin_operation_logs`。 +- 必须有操作者、动作、资源、结果、IP、原因。 +- 不能因为 CLS 采集失败而丢失。 + +钱包账务流水: + +- 存在 `wallet_transactions`、`wallet_entries`、`wallet_recharge_records` 等表。 +- 是账务事实来源。 +- 运行日志只辅助定位,不能替代账务表。 + +## 阶段划分 + +第一阶段是服务内日志标准化,目标是让所有进程稳定输出结构化 JSON 到 stdout/stderr。当前第一阶段已经完成到可进入联调的状态: + +- App 后端服务接入 `logx.Init`。 +- `admin-server` 因为是独立 Go module,使用 `server/admin/internal/platform/logging` 接入。 +- HTTP/gRPC access log 支持统一字段、脱敏、body 开关、payload 截断和错误等级。 +- `log.Printf` 类 worker 日志已改成结构化日志或被 stdout JSON 兜底。 +- `logx` 已补充 gRPC 常用追踪字段提取;即使生产关闭 request body,也能在 access log 中检索 `command_id`、`user_id`、`room_id`、`transaction_id` 等字段。 +- 启动完成后的进程级失败会按 `ERROR` 结构化输出,不再落成普通 INFO `stdlog`。 + +第二阶段是腾讯云 CLS 部署接入,目标是把第一阶段的 stdout JSON 真正采集、索引、查询和告警: + +1. 在腾讯云 CLS 创建 logset/topic,按环境隔离 `dev`、`staging`、`prod`。 +2. 配置 TKE 容器标准输出采集,或在 CVM/Docker 部署 CLS LogListener。 +3. CLS topic 使用 JSON 解析,开启字段索引。 +4. 建立基础查询模板:按 `request_id` 查链路,按 `service` 看错误,按 `transaction_id` 查钱包,按 `room_id` 查房间。 +5. 建立第一批 dashboard:服务错误、接口耗时、gRPC 错误、wallet 失败、room outbox 重试、登录失败。 +6. 建立第一批告警:gateway 5xx、gRPC Internal/Unavailable、wallet ERROR、room outbox 重试异常、腾讯云回调异常。 +7. 验证保留周期和采集成本,生产默认不上传 request/response body。 + +第三阶段再做观测增强,包括 OpenTelemetry trace、Prometheus 指标、慢请求日志、安全日志 topic、客户端日志关联。 + +## 实施顺序 + +1. 已增加统一日志配置结构:`log.level`、`log.format`、`log.include_request_body`、`log.include_response_body`、`log.max_payload_bytes`。 +2. 已扩展 `pkg/logx.Init`,服务启动时统一设置 JSON/text handler、level、基础字段。 +3. 已给 gateway、room、user、wallet、activity 接入 `logx.Init`;admin-server 通过独立 module 内的 `platform/logging` 接入。 +4. 已改造 `logx.HTTPMiddleware` 和 `logx.UnaryServerInterceptor`,按配置控制 body、截断大小和错误等级。 +5. 已替换核心路径的 `log.Printf`: + - room outbox worker + - user region rebuild worker + - user mic time worker + - invite recharge worker + - Tencent RTC callback +6. 本地验证 JSON 日志格式:`docker compose logs gateway-service` 能看到结构化 JSON。 +7. 生产部署层接入腾讯云 CLS stdout 采集。 +8. 在 CLS 配置 JSON 解析和字段索引。 +9. 建立基础 dashboard:错误率、接口耗时、钱包失败、room outbox 重试、登录失败。 +10. 建立首批告警策略。 + +## 测试和验收 + +代码侧: + +- `logx` 单测覆盖脱敏字段。 +- `logx` 单测覆盖 payload 截断。 +- HTTP middleware 单测覆盖 success、4xx、5xx 日志级别。 +- gRPC interceptor 单测覆盖 OK、业务错误、Internal。 +- 配置解析单测覆盖默认值和非法 log level。 + +本地验收: + +```bash +docker compose up -d +docker compose logs gateway-service +docker compose logs wallet-service +``` + +验收标准: + +- 日志是 JSON 或本地配置指定的 text。 +- 每条 access log 有 `service`、`request_id`、`duration_ms`。 +- 敏感字段显示为 `***REDACTED***`。 +- healthcheck 不刷屏。 +- 生产配置下 request/response body 不输出。 + +CLS 验收: + +- 能按 `request_id` 查到 gateway 和下游服务日志。 +- 能按 `service=wallet-service AND transaction_id=...` 查到钱包交易日志。 +- 能按 `room_id` 查房间命令相关日志。 +- 能按 `level=ERROR` 聚合最近 5 分钟错误。 +- 告警能正确触发并发送到通知渠道。 + +## 后续演进 + +日志整理完成后,再考虑: + +- OpenTelemetry trace,把 `trace_id/span_id` 加入日志字段。 +- Prometheus 指标和日志告警分工,错误率和延迟优先走 metrics。 +- 慢请求日志,按 `duration_ms` 阈值记录额外字段。 +- 安全日志 topic,把支付、提现、币商、管理员高危操作单独留存。 +- 客户端日志和崩溃分析接入,和服务端 `request_id` 或 `session_id` 关联。 diff --git a/docs/service-foundation-technical-design.md b/docs/service-foundation-technical-design.md index a4d76943..8a65356a 100644 --- a/docs/service-foundation-technical-design.md +++ b/docs/service-foundation-technical-design.md @@ -181,7 +181,7 @@ Activity 专用 reason: ### Error Implementation Boundary -建议扩展 `pkg/xerr`,但领域层仍不能 import gRPC 包。 +错误码统一由 `pkg/xerr` 的 catalog 注册,领域层仍不能 import gRPC 包。 目标结构: @@ -192,7 +192,15 @@ type Error struct { } ``` -`transport/grpc` 负责把 `xerr.Error` 转成 gRPC status,并附带 `google.rpc.ErrorInfo`。 +`transport/grpc` 负责把 `xerr.Error` 转成 gRPC status,并附带 `google.rpc.ErrorInfo`。gRPC code 保持少量 canonical code;业务码只放在 `ErrorInfo.reason` 和 HTTP envelope `code`。 + +新增业务错误码时必须同时在 `xerr` catalog 注册: + +- gRPC canonical code。 +- HTTP status。 +- App 可见 public code。 +- App 可见 public message。 +- 是否可重试。 不要在 service 层直接返回 `status.Error(...)`。 diff --git a/docs/user-mic-online-time-architecture.md b/docs/user-mic-online-time-architecture.md index 9129ffbc..4ce5979f 100644 --- a/docs/user-mic-online-time-architecture.md +++ b/docs/user-mic-online-time-architecture.md @@ -270,22 +270,29 @@ user_mic_event_consumption( ## Open Session Compensation -需要一个补偿 worker 处理异常 open session: +user-service 已内置 open session compensation worker,用于兜底关闭 room-service down 事件缺失导致的异常 open session。补偿只修正用户麦时长事实,不反写 Room Cell,也不向客户端发送房间控制消息。 | Case | Action | | --- | --- | -| `pending_publish` 超过 room-service publish timeout | 等 room-service `publish_timeout` down 事件;若长期没有事件,按异常关闭 | -| `publishing` 长时间没有 down,但用户 presence 已离开 | 使用 presence/room close 事实补偿关闭 | -| 房间关闭但仍有 open session | 以 `RoomClosed.occurred_at_ms` 关闭 | -| 消费者停机后恢复 | 从 `room_outbox` 或 MQ 位点继续消费;必要时用未结束 session 扫描补偿 | +| `pending_publish` 超过 `pending_publish_max_age_sec` | 关闭 session,`end_reason=compensated_publish_timeout`,`mic_online_ms=0` | +| `publishing` 超过 `publishing_session_max_age_sec` | 关闭 session,`end_reason=compensated_publishing_max_window`,按确认发流后到截断点累计 `mic_online_ms` | +| 消费者停机后恢复 | room outbox worker 继续从位点扫描;补偿 worker 扫描 MySQL open session 兜底 | -补偿关闭必须写 `end_reason=compensated_*`,不能伪装成用户主动下麦。 +补偿扫描使用 MySQL `FOR UPDATE SKIP LOCKED` 锁定候选 session。单批 claim、关闭、daily/lifetime 聚合和 outbox 写入在同一个事务内完成,避免关闭成功但派生事件丢失。 -补偿必须设置最大可累计窗口,避免房间事件缺失时把异常 open session 算成超长在线。首版建议按房间关闭时间、presence 离开时间或 `publish_deadline_ms` 截断;仍无法确认时只关闭 session 并告警,不增加 `mic_online_ms`。 +补偿关闭必须写 `end_reason=compensated_*`,不能伪装成用户主动下麦。补偿必须设置最大可累计窗口,避免房间事件缺失时把异常 open session 算成超长在线。当前默认配置: + +| Config | Default | Meaning | +| --- | ---: | --- | +| `compensation_enabled` | `true` | 是否启动异常 session 补偿 worker | +| `compensation_interval_sec` | `60` | 没有积压时两轮扫描间隔 | +| `compensation_batch_size` | `100` | 单轮最多关闭 session 数 | +| `pending_publish_max_age_sec` | `120` | pending 未确认发流的最大保留窗口 | +| `publishing_session_max_age_sec` | `43200` | publishing session 最大可累计窗口,默认 12 小时 | ## Derived Events And Consumers -用户麦时长消费者写入 MySQL 后,可以通过 user-service outbox 产出派生事件,供 host stats、任务、活动和 BI 消费。派生事件不能替代基础事实表,消费者失败不影响 `user_mic_sessions`、daily 和 lifetime 事实。 +用户麦时长消费者关闭 session 时,在同一个 MySQL 事务内写入 user-service `user_outbox` 派生事件,供 host stats、任务、活动和 BI 消费。派生事件不能替代基础事实表,消费者失败不影响 `user_mic_sessions`、daily 和 lifetime 事实。 | Event | Trigger | Consumer | | --- | --- | --- | @@ -293,6 +300,18 @@ user_mic_event_consumption( | `UserMicDailyStatsUpdated` | daily stats 有增量 | task progress、growth system | | `UserMicLifetimeStatsUpdated` | lifetime stats 有增量 | lifetime task cache | +事件幂等键: + +| Event | `event_id` | +| --- | --- | +| `UserMicSessionClosed` | `UserMicSessionClosed:{mic_session_id}` | +| `UserMicDailyStatsUpdated` | `UserMicDailyStatsUpdated:{mic_session_id}:{stat_date}` | +| `UserMicLifetimeStatsUpdated` | `UserMicLifetimeStatsUpdated:{mic_session_id}` | + +`UserMicSessionClosed` payload 至少包含 `app_code`、`user_id`、`room_id`、`mic_session_id`、`seat_occupied_ms`、`mic_online_ms`、`seat_started_at_ms`、`ended_at_ms`、`end_reason`、`closed_event_id`。 + +daily/lifetime update payload 使用 delta 语义,包含 `seat_occupied_delta_ms`、`mic_online_delta_ms`、`session_count_delta` 和 `updated_at_ms`;daily 事件额外包含 `stat_date`、`room_count_delta`。 + host/Agency/BD 统计应该优先消费 `UserMicSessionClosed` 或读取 user mic facts 派生关系快照,不应该再直接消费 `RoomMicChanged` 重算同一套用户时长。 ## Task Usage @@ -389,8 +408,8 @@ GET /api/v1/users/me/mic-stats?from_ms=...&to_ms=... 4. 已实现 session 状态机:`up -> publish_confirmed -> down`,支持 `change` 不切断。 5. 已实现 daily/lifetime 聚合,跨 UTC 日切分。 6. 已增加内部查询 RPC `GetUserMicLifetimeStats`,供任务、主播、后台读取 lifetime 聚合。 -7. 下一步增加补偿 worker,关闭异常 open session。 -8. 下一步增加 user mic outbox,产出 `UserMicSessionClosed`、`UserMicDailyStatsUpdated`、`UserMicLifetimeStatsUpdated`。 +7. 已增加补偿 worker,关闭异常 open session。 +8. 已增加 user mic outbox,产出 `UserMicSessionClosed`、`UserMicDailyStatsUpdated`、`UserMicLifetimeStatsUpdated`。 9. 任务系统接入 `user_mic_lifetime_stats`,不要重复消费 room 事件。 10. host stats 从 user mic facts 或 user mic outbox 派生,不直接重复处理 Room Cell 麦位事件。 diff --git a/docs/vip-system-architecture.md b/docs/vip-system-architecture.md new file mode 100644 index 00000000..66fc6b0c --- /dev/null +++ b/docs/vip-system-architecture.md @@ -0,0 +1,461 @@ +# VIP 系统架构设计 + +## 目标 + +VIP 是用户可直接用金币购买的限时会员权益。每个 VIP 等级都可以单独购买,单次购买有效期固定 7 天,不同等级价格不同。购买成功后,系统根据后台配置的资源组发放头像框、座驾、气泡、徽章、礼物等权益。 + +首版只做当前业务真实需要的能力: + +- 用户可以购买任意有效 VIP 等级。 +- 用户可以从低等级升级到高等级。 +- 用户当前 VIP 未过期时,不能购买比当前等级低的 VIP。 +- 同等级重复购买按续期处理。 +- VIP 扣金币、会员状态变更、资源组发放必须在 `wallet-service` 内形成一致账务闭环。 +- 后台只配置 VIP 等级、金币价格、有效期和赠送资源组;客户端不能传价格或资源明细。 + +## 服务边界 + +VIP 属于 `wallet-service` 的账务和权益域,不放在 `user-service`。 + +原因: + +- 购买 VIP 必须先扣金币,扣费成功才允许生成 VIP 状态和权益。 +- 购买 VIP 会发放后台资源组,资源组、资源权益和钱包资产已经归 `wallet-service` 管。 +- 如果 VIP 状态放在 `user-service`,扣费、会员状态、资源发放会跨服务,需要分布式事务或补偿,复杂度和失败窗口都更大。 + +服务职责: + +- `gateway-service`:提供 App HTTP API,做鉴权、参数校验、request_id/command_id 透传,调用 `wallet-service`。 +- `wallet-service`:拥有 VIP 等级配置、用户 VIP 状态、VIP 购买订单、金币扣费、资源组发放、VIP 事件 outbox。 +- `user-service`:只在用户资料聚合时读取 VIP 摘要,不拥有 VIP 状态。 +- `room-service`:需要展示 VIP 标识时读取用户资料/VIP 投影,不参与 VIP 购买主链路。 +- `server/admin`:后台配置 VIP 等级和资源组,具体后台接口单独设计。 + +## 核心模型 + +### VIP 等级 + +`vip_levels` + +| 字段 | 说明 | +| -------------------------- | -------------------------------------------------- | +| `id` | 内部主键 | +| `level` | VIP 等级,数字越大等级越高 | +| `name` | 展示名称,例如 VIP1、VIP2 | +| `coin_price` | 购买该等级需要消耗的金币数 | +| `duration_ms` | 有效期,首版固定 `7 * 24 * 60 * 60 * 1000` | +| `reward_resource_group_id` | 购买成功后发放的资源组 | +| `status` | `draft` / `active` / `disabled` | +| `sort_order` | App 展示排序 | +| `display_config_json` | 展示配置快照,例如颜色、权益说明、角标,不参与结算 | +| `created_at_ms` | 创建时间 | +| `updated_at_ms` | 更新时间 | + +约束: + +- `level` 唯一。 +- `coin_price > 0`。 +- `duration_ms` 首版必须等于 7 天,不允许后台临时改成其它值。 +- `active` 状态必须绑定有效资源组。 + +### 用户 VIP 当前状态 + +`user_vip_memberships` + +| 字段 | 说明 | +| --------------- | ---------------------------- | +| `user_id` | 用户 ID,唯一 | +| `level` | 当前等级 | +| `status` | `active` / `expired` | +| `started_at_ms` | 当前等级周期开始时间 | +| `expires_at_ms` | 当前 VIP 到期时间 | +| `last_order_id` | 最近一次购买订单 | +| `version` | 乐观版本,配合行锁防并发覆盖 | +| `created_at_ms` | 创建时间 | +| `updated_at_ms` | 更新时间 | + +读取时以 `expires_at_ms > now` 判断是否有效,`status` 只作为后台检索和事件处理的辅助状态。这样即使过期任务延迟,App 也不会把过期 VIP 当成有效权益。 + +### VIP 购买订单 + +`vip_purchase_orders` + +| 字段 | 说明 | +| ---------------------------- | --------------------------------- | +| `order_id` | VIP 订单号 | +| `command_id` | 客户端生成的幂等命令 ID | +| `user_id` | 购买用户 | +| `target_level` | 目标 VIP 等级 | +| `coin_price_snapshot` | 下单时价格快照 | +| `duration_ms_snapshot` | 下单时有效期快照 | +| `resource_group_id_snapshot` | 下单时资源组快照 | +| `previous_level` | 购买前有效等级,无有效 VIP 则为 0 | +| `previous_expires_at_ms` | 购买前到期时间 | +| `new_level` | 购买后等级 | +| `new_expires_at_ms` | 购买后到期时间 | +| `wallet_transaction_id` | 金币扣费交易 ID | +| `resource_grant_id` | 资源组发放记录 ID | +| `status` | `succeeded` / `failed` | +| `failure_reason` | 失败原因 | +| `request_hash` | 幂等请求摘要 | +| `created_at_ms` | 创建时间 | +| `updated_at_ms` | 更新时间 | + +约束: + +- `(user_id, command_id)` 唯一。 +- 相同 `command_id` + 相同 `request_hash` 返回原订单结果。 +- 相同 `command_id` + 不同 `request_hash` 返回幂等冲突。 + +### VIP 变更历史 + +`user_vip_history` + +记录每次购买、续期、升级、过期,供客服、后台账单、风控排查使用。历史表不参与 App 主链路强查询。 + +核心字段: + +- `history_id` +- `user_id` +- `event_type`: `purchase` / `renew` / `upgrade` / `expire` +- `from_level` +- `to_level` +- `from_expires_at_ms` +- `to_expires_at_ms` +- `order_id` +- `created_at_ms` + +## 购买和升级规则 + +定义: + +- 当前有效 VIP:`membership.expires_at_ms > now && membership.level > 0` +- 目标等级:用户本次购买的 `vip_level` + +规则: + +| 场景 | 处理 | +| ---------------------- | ------------------------------------------------------------------------ | +| 无有效 VIP | 可以购买任意 active 等级,`expires_at = now + 7d` | +| 有效 VIP,购买同等级 | 续期,`expires_at = current_expires_at + 7d` | +| 有效 VIP,购买更高等级 | 立即升级,`level = target_level`,`expires_at = current_expires_at + 7d` | +| 有效 VIP,购买更低等级 | 拒绝,返回 `VIP_DOWNGRADE_NOT_ALLOWED` | +| VIP 已过期 | 按无有效 VIP 处理,可以购买任意 active 等级 | + +首版升级采用“保留剩余时间并升级”的策略:如果用户 VIP1 还剩 3 天,又购买 VIP3,则 VIP3 到期时间为当前 VIP1 到期时间再加 7 天。这个规则实现简单、用户感知清晰,也避免升级时损失已付费时长。 + +如果后续业务希望“补差价升级”或“升级只生效 7 天”,只需要扩展 VIP 等级的升级策略,不改变主链路表结构。 + +## 资源组发放规则 + +购买 VIP 成功后,`wallet-service` 根据 `vip_levels.reward_resource_group_id` 发放资源组。 + +资源组继续复用现有资源组能力: + +- 头像框:`avatar_frame` +- 座驾:`vehicle` +- 聊天气泡:`chat_bubble` +- 徽章:`badge` +- 飘屏:`floating_screen` +- 礼物:`gift` +- 金币等钱包资产:仅在明确配置时发放,默认不建议 VIP 购买返金币,避免账务绕圈。 + +VIP 资源组需要增加一个明确约束: + +- VIP 专属装扮类资源默认与 VIP 到期时间对齐。 +- 资源组 item 如果配置了独立 `duration_ms`,后台必须能看到它和 VIP 7 天周期的差异。 +- 首版推荐新增 `duration_policy = inherit_vip_expiry`,购买 VIP 时将资源权益到期时间直接设置为 `new_expires_at_ms`。 + +续期和升级时: + +- 同一资源重复发放时,使用资源权益的 `extend_expiry` 或 `align_to_expiry` 策略,避免重复插入导致用户出现多个同资源有效权益。 +- 升级后只发放目标等级配置的资源组;低等级已获得的资源不立即删除,到期后自然失效。 +- App 展示和自动佩戴时按用户当前有效资源、资源等级和佩戴状态决定,不从 VIP 等级反推资源。 + +## 主链路流程 + +```mermaid +sequenceDiagram + autonumber + participant App + participant Gateway as gateway-service + participant Wallet as wallet-service + participant DB as Wallet MySQL + participant Outbox as Wallet Outbox + + App->>Gateway: POST /api/v1/vip/purchase {level, command_id} + Gateway->>Wallet: PurchaseVip(user_id, level, command_id, request_meta) + Wallet->>DB: BEGIN + Wallet->>DB: 查询并锁定 vip_levels(level) + Wallet->>DB: 查询并锁定 user_vip_memberships(user_id) + Wallet->>DB: 校验不能降级、计算 new_expires_at_ms + Wallet->>DB: 锁定用户 COIN 钱包账户 + Wallet->>DB: 扣减金币,写 wallet_transaction / wallet_entries + Wallet->>DB: 发放 VIP 资源组,写 resource_entitlements + Wallet->>DB: upsert user_vip_memberships + Wallet->>DB: 写 vip_purchase_orders / user_vip_history + Wallet->>Outbox: 写 VipPurchased / VipUpgraded / VipRenewed + Wallet->>DB: COMMIT + Wallet-->>Gateway: 返回当前 VIP、扣费、权益摘要 + Gateway-->>App: {code,message,request_id,data} +``` + +关键点: + +- 金币扣费、VIP 状态、资源组发放必须在同一个 `wallet-service` 数据库事务中完成。 +- 客户端只传 `level` 和 `command_id`,价格和资源组完全由服务端配置决定。 +- 扣费成功但资源发放失败不能提交事务;否则用户会付费但拿不到权益。 +- 资源发放成功但 VIP 状态更新失败也不能提交事务;否则权益和会员状态会不一致。 + +## App 一级接口 + +### 查询 VIP 等级 + +`GET /api/v1/vip/levels` + +返回: + +```json +{ + "current_vip": { + "level": 2, + "name": "VIP2", + "expires_at_ms": 1760000000000, + "remaining_ms": 604800000 + }, + "levels": [ + { + "level": 1, + "name": "VIP1", + "coin_price": 1000, + "duration_ms": 604800000, + "can_purchase": true, + "purchase_block_reason": "", + "benefits": [ + { + "resource_type": "avatar_frame", + "resource_id": "vip1_frame", + "name": "VIP1 Avatar Frame", + "duration_policy": "inherit_vip_expiry" + } + ] + } + ] +} +``` + +说明: + +- `can_purchase=false` 通常表示当前有效 VIP 等级高于该等级。 +- App 可以展示所有等级,但购买按钮状态以服务端返回为准。 + +### 查询我的 VIP + +`GET /api/v1/vip/me` + +返回: + +```json +{ + "level": 2, + "name": "VIP2", + "status": "active", + "expires_at_ms": 1760000000000, + "remaining_ms": 604800000, + "renewable": true, + "upgradeable_levels": [3, 4, 5] +} +``` + +说明: + +- 底部“我的”页面只需要拿 VIP 摘要,不要拉完整资源列表。 +- 完整装扮资源仍从资源/背包接口查询。 + +### 购买 VIP + +`POST /api/v1/vip/purchase` + +请求: + +```json +{ + "level": 3, + "command_id": "client-generated-id" +} +``` + +返回: + +```json +{ + "order_id": "vip_order_123", + "event_type": "upgrade", + "coin_spent": 3000, + "vip": { + "level": 3, + "name": "VIP3", + "expires_at_ms": 1760604800000 + }, + "granted_resources": [ + { + "resource_type": "vehicle", + "resource_id": "vip3_vehicle", + "expires_at_ms": 1760604800000 + } + ] +} +``` + +## gRPC 契约建议 + +新增 `wallet.v1.VipService`: + +```protobuf +service VipService { + rpc ListVipLevels(ListVipLevelsRequest) returns (ListVipLevelsResponse); + rpc GetMyVip(GetMyVipRequest) returns (GetMyVipResponse); + rpc PurchaseVip(PurchaseVipRequest) returns (PurchaseVipResponse); +} +``` + +核心字段: + +- `RequestMeta meta` +- `int64 user_id` +- `int32 level` +- `string command_id` +- `int64 now_ms` 不由客户端传,由服务端生成;测试可通过 clock 注入。 + +错误码建议纳入 `pkg/xerr` catalog: + +- `VIP_LEVEL_NOT_FOUND` +- `VIP_LEVEL_DISABLED` +- `VIP_DOWNGRADE_NOT_ALLOWED` +- `VIP_PRICE_CHANGED`,仅在未来支持客户端价格确认时使用 +- `WALLET_INSUFFICIENT_BALANCE` +- `IDEMPOTENCY_CONFLICT` + +gRPC status 仍保持少量通用 code: + +- 参数错误:`InvalidArgument` +- 余额不足、不能降级:`FailedPrecondition` +- 等级不存在或禁用:`NotFound` / `FailedPrecondition` +- 幂等冲突:`AlreadyExists` +- 内部事务失败:`Internal` + +业务错误放在 `google.rpc.ErrorInfo.reason`。 + +## 并发和幂等 + +同一用户可能快速点击多次购买或同时升级,必须按用户串行化处理。 + +事务内锁顺序固定: + +1. 锁 `vip_levels(level)`,读取配置快照。 +2. 锁 `user_vip_memberships(user_id)`。 +3. 锁用户 `COIN` 钱包账户。 +4. 写订单、流水、权益、会员状态。 + +如果 `user_vip_memberships` 不存在,先插入一行占位再锁,或者用用户级 advisory lock。不要只依赖应用内 mutex,因为多实例部署时无效。 + +幂等规则: + +- `command_id` 是购买命令幂等键,不使用 `request_id`。 +- 已成功订单重复请求直接返回原结果。 +- 已失败订单是否允许重试取决于失败类型:余额不足可以用新 `command_id` 重试;系统错误如果事务未提交,可以继续用原 `command_id` 重试。 +- 订单内保存价格、资源组、有效期快照,后台改配置不影响已成功订单解释。 + +## VIP 过期 + +VIP 是否有效以 `expires_at_ms` 为准。过期处理分两层: + +- App 查询时实时判断,过期立即返回 `status=expired`。 +- 后台定时任务扫描过期会员,写 `user_vip_history(expire)` 和 `VipExpired` outbox,用于消息通知、缓存清理和搜索投影更新。 + +资源权益也以自己的 `expires_at_ms` 判断有效性。如果使用 `inherit_vip_expiry`,VIP 到期后头像框、座驾等权益自然失效,不需要额外删除。 + +## Outbox 事件 + +`wallet-service` 写出以下事件: + +- `VipPurchased` +- `VipRenewed` +- `VipUpgraded` +- `VipExpired` + +事件内容: + +- `event_id` +- `user_id` +- `order_id` +- `from_level` +- `to_level` +- `expires_at_ms` +- `coin_spent` +- `resource_group_id` +- `occurred_at_ms` + +消费者: + +- `user-service` 可消费后更新用户资料投影,便于列表页快速展示 VIP 标识。 +- `room-service` 如需房间内展示 VIP 角标,可消费投影事件或通过用户资料聚合读取,不接入购买主链路。 +- `activity-service` 可用于 VIP 活动任务统计。 + +## 缓存策略 + +VIP 配置可以缓存,用户 VIP 状态不要只放 Redis。 + +- `vip_levels`:本地内存缓存 + 短 TTL,后台改配置后通过版本号或事件刷新。 +- `user_vip_memberships`:MySQL 为准,Redis 只做读优化。 +- 购买成功后删除或刷新 `vip:me:{user_id}` 缓存。 +- 过期时间短且强相关权益展示,缓存 TTL 不能超过 `min(60s, expires_at_ms-now)`。 + +## 后台配置约束 + +后台配置 VIP 等级时必须校验: + +- 等级唯一且大于 0。 +- 金币价格大于 0。 +- 有效期首版固定 7 天。 +- 绑定资源组必须存在且可用。 +- 资源组里的装扮资源建议使用 `inherit_vip_expiry`。 +- 禁用等级只影响新购买,不影响已购买用户的剩余有效期。 +- 修改价格只影响修改后的新订单,不回改历史订单。 + +后台可以修改资源组,但已成功订单使用订单快照解释;用户已经拿到的权益不因为后台换资源组被自动替换。 + +## 数据一致性原则 + +- VIP 购买不是普通“资源组发放”,它必须先扣金币。 +- 不允许 gateway 先扣金币再单独调用资源发放。 +- 不允许 user-service 写 VIP 状态。 +- 不允许客户端传资源 ID、资源组 ID、价格。 +- 不允许用 Redis 作为 VIP 当前状态唯一来源。 +- 不允许后台直接改用户 VIP 状态来修单,修单必须走后台补单/补偿命令,并写历史和审计。 + +## 实现顺序 + +1. 扩展 protobuf:新增 `VipService`、VIP 等级、我的 VIP、购买 VIP 响应结构。 +2. 增加 MySQL 表:`vip_levels`、`user_vip_memberships`、`vip_purchase_orders`、`user_vip_history`。 +3. 在 `wallet-service` 实现 VIP repository,先完成等级查询和用户 VIP 查询。 +4. 实现 `PurchaseVip` 事务:等级校验、幂等、行锁、扣金币、资源组发放、会员状态更新、历史和 outbox。 +5. 在 `gateway-service` 增加 App HTTP API:`/api/v1/vip/levels`、`/api/v1/vip/me`、`/api/v1/vip/purchase`。 +6. 接入用户资料聚合:底部“我的”和房间用户信息只展示 VIP 摘要。 +7. 增加过期扫描任务和 outbox 消费投影。 +8. 再做后台 VIP 配置页面和后台补单/审计能力。 + +## 测试重点 + +必须覆盖: + +- 无 VIP 购买 VIP1 成功。 +- VIP1 未过期时续费 VIP1,到期时间追加 7 天。 +- VIP1 未过期时购买 VIP3,等级变 VIP3,到期时间追加 7 天。 +- VIP3 未过期时购买 VIP1 被拒绝。 +- VIP 过期后可以购买任意等级。 +- 金币不足时不写 VIP 状态、不发资源。 +- 资源组发放失败时金币扣费回滚。 +- 同一 `command_id` 重试返回同一订单。 +- 同一用户并发购买时只产生符合顺序的一条最终状态。 +- 后台禁用等级后不能新购买,但不影响已购买用户有效期。 diff --git a/pkg/logx/logx.go b/pkg/logx/logx.go index c18d0d91..b1a5b72c 100644 --- a/pkg/logx/logx.go +++ b/pkg/logx/logx.go @@ -1,18 +1,24 @@ -// Package logx 提供服务边界日志能力。 -// 它只记录 transport 层可见的请求、响应、错误语义和耗时,不承载业务判断。 +// Package logx provides structured runtime logging for service boundaries and workers. +// Services emit JSON/text to stdout only; deployment collects stdout into CLS or local logs. package logx import ( "bytes" "context" "encoding/json" + "errors" + "fmt" "io" + "log" "log/slog" "mime" "net/http" + "os" "strings" + "sync" "time" + "hyapp/pkg/appcode" "hyapp/pkg/xerr" "google.golang.org/grpc" @@ -23,88 +29,281 @@ import ( ) const ( - // maxLoggedBytes 限制单个 request/response 字段的日志体积,避免大响应污染 stdout。 - maxLoggedBytes = 8192 - // redactedValue 是敏感字段统一替换值,保证日志可读但不可用于重放凭证。 - redactedValue = "***REDACTED***" + defaultLevel = "info" + defaultFormat = "json" + defaultEnv = "local" + defaultMaxPayloadBytes = 2048 + redactedValue = "***REDACTED***" ) -// HTTPMiddleware 在 gateway HTTP 边界记录请求体、响应体、状态码和耗时。 -// requestIDFromContext 由 gateway 注入,logx 不直接依赖具体 transport 包,避免反向耦合。 +// Config controls process-wide structured logging. Service code must not write to CLS directly. +type Config struct { + Service string `yaml:"service"` + Env string `yaml:"env"` + Version string `yaml:"version"` + NodeID string `yaml:"node_id"` + Level string `yaml:"level"` + Format string `yaml:"format"` + IncludeRequestBody bool `yaml:"include_request_body"` + IncludeResponseBody bool `yaml:"include_response_body"` + MaxPayloadBytes int `yaml:"max_payload_bytes"` + Output io.Writer `yaml:"-"` +} + +// WithRuntimeFields fills process identity fields from service config without duplicating YAML. +func (cfg Config) WithRuntimeFields(service string, env string, nodeID string) Config { + if strings.TrimSpace(cfg.Service) == "" { + cfg.Service = service + } + if strings.TrimSpace(cfg.Env) == "" { + cfg.Env = env + } + if strings.TrimSpace(cfg.NodeID) == "" { + cfg.NodeID = nodeID + } + return cfg +} + +type contextAttrsKey struct{} + +var ( + globalMu sync.RWMutex + globalLogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo, ReplaceAttr: replaceAttr})) + globalConfig = Config{Env: defaultEnv, Level: defaultLevel, Format: defaultFormat, MaxPayloadBytes: defaultMaxPayloadBytes} +) + +// Init installs the process logger and redirects the standard library log package into JSON/text logs. +func Init(cfg Config) error { + normalized, err := NormalizeConfig(cfg) + if err != nil { + return err + } + + level, err := parseLevel(normalized.Level) + if err != nil { + return err + } + output := normalized.Output + if output == nil { + output = os.Stdout + } + + handlerOptions := &slog.HandlerOptions{Level: level, ReplaceAttr: replaceAttr} + var handler slog.Handler + switch normalized.Format { + case "json": + handler = slog.NewJSONHandler(output, handlerOptions) + case "text": + handler = slog.NewTextHandler(output, handlerOptions) + default: + return fmt.Errorf("unsupported log format %q", normalized.Format) + } + + baseAttrs := make([]any, 0, 8) + baseAttrs = appendNonEmpty(baseAttrs, "env", normalized.Env) + baseAttrs = appendNonEmpty(baseAttrs, "service", normalized.Service) + baseAttrs = appendNonEmpty(baseAttrs, "version", normalized.Version) + baseAttrs = appendNonEmpty(baseAttrs, "node_id", normalized.NodeID) + logger := slog.New(handler).With(baseAttrs...) + + globalMu.Lock() + globalLogger = logger + globalConfig = normalized + globalMu.Unlock() + + slog.SetDefault(logger) + log.SetFlags(0) + log.SetOutput(stdLogWriter{}) + return nil +} + +// NormalizeConfig applies safe defaults and validates static logging knobs. +func NormalizeConfig(cfg Config) (Config, error) { + cfg.Service = strings.TrimSpace(cfg.Service) + cfg.Env = strings.ToLower(strings.TrimSpace(cfg.Env)) + if cfg.Env == "" { + cfg.Env = defaultEnv + } + cfg.Version = strings.TrimSpace(cfg.Version) + cfg.NodeID = strings.TrimSpace(cfg.NodeID) + cfg.Level = strings.ToLower(strings.TrimSpace(cfg.Level)) + if cfg.Level == "" { + cfg.Level = defaultLevel + } + cfg.Format = strings.ToLower(strings.TrimSpace(cfg.Format)) + if cfg.Format == "" { + cfg.Format = defaultFormat + } + if cfg.MaxPayloadBytes <= 0 { + cfg.MaxPayloadBytes = defaultMaxPayloadBytes + } + if _, err := parseLevel(cfg.Level); err != nil { + return Config{}, err + } + if cfg.Format != "json" && cfg.Format != "text" { + return Config{}, fmt.Errorf("unsupported log format %q", cfg.Format) + } + return cfg, nil +} + +// Logger returns a logger enriched with attributes attached to ctx through With. +func Logger(ctx context.Context) *slog.Logger { + globalMu.RLock() + logger := globalLogger + globalMu.RUnlock() + attrs := attrsFromContext(ctx) + if len(attrs) == 0 { + return logger + } + args := make([]any, 0, len(attrs)) + for _, attr := range attrs { + args = append(args, attr) + } + return logger.With(args...) +} + +// With attaches structured fields to ctx for subsequent Logger/Info/Warn/Error calls. +func With(ctx context.Context, attrs ...slog.Attr) context.Context { + if ctx == nil { + ctx = context.Background() + } + if len(attrs) == 0 { + return ctx + } + existing := attrsFromContext(ctx) + merged := make([]slog.Attr, 0, len(existing)+len(attrs)) + merged = append(merged, existing...) + merged = append(merged, attrs...) + return context.WithValue(ctx, contextAttrsKey{}, merged) +} + +// Info records a normal runtime event. +func Info(ctx context.Context, msg string, attrs ...slog.Attr) { + Logger(ctx).LogAttrs(ctx, slog.LevelInfo, msg, attrs...) +} + +// Warn records a recoverable runtime failure or degraded path. +func Warn(ctx context.Context, msg string, attrs ...slog.Attr) { + Logger(ctx).LogAttrs(ctx, slog.LevelWarn, msg, attrs...) +} + +// Error records a request, transaction, worker, or external dependency failure. +func Error(ctx context.Context, msg string, err error, attrs ...slog.Attr) { + if err != nil { + attrs = append(attrs, slog.String("error", err.Error())) + } + Logger(ctx).LogAttrs(ctx, slog.LevelError, msg, attrs...) +} + +// HTTPMiddleware records gateway HTTP access logs. Health checks are intentionally excluded by callers. func HTTPMiddleware(service string, requestIDFromContext func(context.Context) string, next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { startedAt := time.Now() - requestBody := readAndRestoreBody(request) - capture := newHTTPResponseCapture(writer) + cfg := configSnapshot() + requestBody := readAndRestoreBody(request, cfg) + capture := newHTTPResponseCapture(writer, cfg.IncludeResponseBody, cfg.MaxPayloadBytes) next.ServeHTTP(capture, request) requestID := strings.TrimSpace(requestIDFromContext(request.Context())) - args := []any{ - "service", service, - "request_id", requestID, - "method", request.Method, - "path", request.URL.Path, - "query", request.URL.RawQuery, - "status", capture.statusCode, - "duration_ms", time.Since(startedAt).Milliseconds(), - "request", safePayload(requestBody), - "response", safePayload(capture.body.Bytes()), + ctx := With(request.Context(), slog.String("request_id", requestID), slog.String("app_code", appcode.FromContext(request.Context()))) + attrs := []slog.Attr{ + slog.String("method", request.Method), + slog.String("path", request.URL.Path), + slog.String("query", request.URL.RawQuery), + slog.Int("status", capture.statusCode), + slog.Int64("duration_ms", time.Since(startedAt).Milliseconds()), + slog.String("client_ip", clientIP(request)), } - if capture.statusCode >= http.StatusInternalServerError { - slog.Error("http_access", args...) - return + attrs = appendServiceAttr(attrs, service) + if cfg.IncludeRequestBody { + attrs = append(attrs, slog.Any("request", safePayload(requestBody, cfg.MaxPayloadBytes, len(requestBody) > cfg.MaxPayloadBytes))) + } + if cfg.IncludeResponseBody { + attrs = append(attrs, slog.Any("response", safePayload(capture.body.Bytes(), cfg.MaxPayloadBytes, capture.truncated))) + } + + switch levelForHTTPStatus(capture.statusCode) { + case slog.LevelError: + Error(ctx, "http_access", nil, attrs...) + case slog.LevelWarn: + Warn(ctx, "http_access", attrs...) + default: + Info(ctx, "http_access", attrs...) } - slog.Info("http_access", args...) }) } -// UnaryServerInterceptor 在内部 gRPC 服务边界记录 protobuf request/response。 -// 内部链路统一 gRPC + protobuf,因此这里是排查跨服务参数和返回值的主入口。 +// UnaryServerInterceptor records internal gRPC access logs without coupling business services to a logger. func UnaryServerInterceptor(service string) grpc.UnaryServerInterceptor { return func(ctx context.Context, request any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { if strings.Contains(info.FullMethod, "/grpc.health.v1.Health/") { - // 健康检查高频且无业务参数,跳过可避免日志噪音淹没真实业务请求。 return handler(ctx, request) } startedAt := time.Now() + cfg := configSnapshot() response, err := handler(ctx, request) code := codes.OK if err != nil { code = status.Code(err) } + meta := requestMetaFromPayload(request) + ctx = With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", appcode.Normalize(meta.AppCode))) - args := []any{ - "service", service, - "request_id", requestIDFromPayload(request), - "method", info.FullMethod, - "grpc_code", code.String(), - "reason", string(xerr.ReasonFromGRPC(err)), - "duration_ms", time.Since(startedAt).Milliseconds(), - "request", safePayloadFromValue(request), - "response", safePayloadFromValue(response), + attrs := []slog.Attr{ + slog.String("grpc_method", info.FullMethod), + slog.String("grpc_code", code.String()), + slog.String("reason", string(xerr.ReasonFromGRPC(err))), + slog.Int64("duration_ms", time.Since(startedAt).Milliseconds()), } - if err != nil { - slog.Error("grpc_access", args...) - return response, err + attrs = appendServiceAttr(attrs, service) + attrs = append(attrs, commonAttrsFromPayload(request)...) + if cfg.IncludeRequestBody { + attrs = append(attrs, slog.Any("request", safePayloadFromValue(request, cfg.MaxPayloadBytes))) } - slog.Info("grpc_access", args...) - return response, nil + if cfg.IncludeResponseBody { + attrs = append(attrs, slog.Any("response", safePayloadFromValue(response, cfg.MaxPayloadBytes))) + } + + switch levelForGRPCCode(code) { + case slog.LevelError: + Error(ctx, "grpc_access", err, attrs...) + case slog.LevelWarn: + Warn(ctx, "grpc_access", attrs...) + default: + Info(ctx, "grpc_access", attrs...) + } + return response, err } } +type stdLogWriter struct{} + +func (stdLogWriter) Write(data []byte) (int, error) { + line := strings.TrimSpace(string(data)) + if line != "" { + Info(context.Background(), "stdlog", slog.String("message", redactLogLine(line))) + } + return len(data), nil +} + +type requestMeta struct { + RequestID string + AppCode string +} + type httpResponseCapture struct { http.ResponseWriter statusCode int + capture bool + maxBytes int + truncated bool body bytes.Buffer } -func newHTTPResponseCapture(writer http.ResponseWriter) *httpResponseCapture { - return &httpResponseCapture{ - ResponseWriter: writer, - statusCode: http.StatusOK, - } +func newHTTPResponseCapture(writer http.ResponseWriter, capture bool, maxBytes int) *httpResponseCapture { + return &httpResponseCapture{ResponseWriter: writer, statusCode: http.StatusOK, capture: capture, maxBytes: maxBytes} } func (c *httpResponseCapture) WriteHeader(statusCode int) { @@ -113,85 +312,98 @@ func (c *httpResponseCapture) WriteHeader(statusCode int) { } func (c *httpResponseCapture) Write(data []byte) (int, error) { - if c.body.Len() < maxLoggedBytes { - remaining := maxLoggedBytes - c.body.Len() + if c.capture && c.body.Len() < c.maxBytes { + remaining := c.maxBytes - c.body.Len() if len(data) > remaining { c.body.Write(data[:remaining]) + c.truncated = true } else { c.body.Write(data) } + } else if c.capture && len(data) > 0 { + c.truncated = true } - return c.ResponseWriter.Write(data) } -func readAndRestoreBody(request *http.Request) []byte { - if request.Body == nil { +func readAndRestoreBody(request *http.Request, cfg Config) []byte { + if !cfg.IncludeRequestBody || request.Body == nil { return nil } mediaType, _, _ := mime.ParseMediaType(request.Header.Get("Content-Type")) if strings.HasPrefix(mediaType, "multipart/") { - // 文件上传请求体可能很大,且包含二进制内容;日志只记录占位符并保留原始流给 handler。 return []byte("") } body, err := io.ReadAll(request.Body) if err != nil { - // 读取失败时把空 body 交还给后续 handler,让真实解码错误仍由 handler 返回。 request.Body = io.NopCloser(bytes.NewReader(nil)) return nil } - request.Body.Close() + _ = request.Body.Close() request.Body = io.NopCloser(bytes.NewReader(body)) return body } -func safePayloadFromValue(value any) any { +func safePayloadFromValue(value any, maxBytes int) any { if value == nil { return nil } - if message, ok := value.(proto.Message); ok { - raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(message) - if err != nil { - return "" - } - return safePayload(raw) - } - - raw, err := json.Marshal(value) + raw, err := marshalPayload(value) if err != nil { - return "" + return "" } - return safePayload(raw) + return safePayload(raw, maxBytes, len(raw) > maxBytes) } -func requestIDFromPayload(value any) string { - payload, ok := safePayloadFromValue(value).(map[string]any) - if !ok { - return "" +func requestMetaFromPayload(value any) requestMeta { + raw, err := marshalPayload(value) + if err != nil || len(raw) == 0 { + return requestMeta{} } + var payload map[string]any + if err := json.Unmarshal(raw, &payload); err != nil { + return requestMeta{} + } + result := requestMeta{RequestID: stringFromMap(payload, "request_id"), AppCode: stringFromMap(payload, "app_code")} meta, ok := payload["meta"].(map[string]any) if !ok { - return "" + return result } - if requestID, ok := meta["request_id"].(string); ok { - return requestID + if result.RequestID == "" { + result.RequestID = stringFromMap(meta, "request_id") } - return "" + if result.AppCode == "" { + result.AppCode = stringFromMap(meta, "app_code") + } + return result } -func safePayload(raw []byte) any { +func marshalPayload(value any) ([]byte, error) { + if value == nil { + return nil, nil + } + if message, ok := value.(proto.Message); ok { + return protojson.MarshalOptions{UseProtoNames: true}.Marshal(message) + } + return json.Marshal(value) +} + +func safePayload(raw []byte, maxBytes int, truncated bool) any { raw = bytes.TrimSpace(raw) if len(raw) == 0 { return nil } - if len(raw) > maxLoggedBytes { - raw = raw[:maxLoggedBytes] + if maxBytes <= 0 { + maxBytes = defaultMaxPayloadBytes + } + if truncated || len(raw) > maxBytes { + return map[string]any{"truncated": true, "bytes": len(raw)} } var payload any if err := json.Unmarshal(raw, &payload); err != nil { - return string(raw) + return redactLogLine(string(raw)) } return redactValue(payload) } @@ -209,15 +421,23 @@ func redactValue(value any) any { } return redacted case []any: + redacted := make([]any, len(typed)) for index, item := range typed { - typed[index] = redactValue(item) + redacted[index] = redactValue(item) } - return typed + return redacted default: return value } } +func replaceAttr(_ []string, attr slog.Attr) slog.Attr { + if isSensitiveKey(attr.Key) { + return slog.String(attr.Key, redactedValue) + } + return attr +} + func isSensitiveKey(key string) bool { normalized := strings.ToLower(strings.ReplaceAll(key, "-", "_")) sensitiveParts := []string{ @@ -234,6 +454,7 @@ func isSensitiveKey(key string) bool { "signature", "private_key", "callback_auth", + "receipt", } for _, part := range sensitiveParts { if strings.Contains(normalized, part) { @@ -242,3 +463,171 @@ func isSensitiveKey(key string) bool { } return false } + +func redactLogLine(line string) string { + fields := strings.Fields(line) + if len(fields) == 0 { + return line + } + changed := false + for index, field := range fields { + separator := strings.IndexAny(field, "=:") + if separator <= 0 { + continue + } + key := strings.Trim(field[:separator], "\"'") + if isSensitiveKey(key) { + fields[index] = field[:separator+1] + redactedValue + changed = true + } + } + if !changed { + return line + } + return strings.Join(fields, " ") +} + +func attrsFromContext(ctx context.Context) []slog.Attr { + if ctx == nil { + return nil + } + attrs, _ := ctx.Value(contextAttrsKey{}).([]slog.Attr) + return attrs +} + +func configSnapshot() Config { + globalMu.RLock() + cfg := globalConfig + globalMu.RUnlock() + if cfg.MaxPayloadBytes <= 0 { + cfg.MaxPayloadBytes = defaultMaxPayloadBytes + } + return cfg +} + +func parseLevel(value string) (slog.Level, error) { + switch strings.ToLower(strings.TrimSpace(value)) { + case "debug": + return slog.LevelDebug, nil + case "", "info": + return slog.LevelInfo, nil + case "warn", "warning": + return slog.LevelWarn, nil + case "error": + return slog.LevelError, nil + default: + return slog.LevelInfo, fmt.Errorf("%w %q", ErrUnsupportedLevel, value) + } +} + +func levelForHTTPStatus(statusCode int) slog.Level { + switch { + case statusCode >= http.StatusInternalServerError: + return slog.LevelError + case statusCode >= http.StatusBadRequest: + return slog.LevelWarn + default: + return slog.LevelInfo + } +} + +func levelForGRPCCode(code codes.Code) slog.Level { + switch code { + case codes.OK: + return slog.LevelInfo + case codes.Internal, codes.Unavailable, codes.DeadlineExceeded, codes.DataLoss, codes.Unknown: + return slog.LevelError + default: + return slog.LevelWarn + } +} + +func appendServiceAttr(attrs []slog.Attr, service string) []slog.Attr { + if strings.TrimSpace(service) == "" { + return attrs + } + cfg := configSnapshot() + if cfg.Service != "" { + return attrs + } + return append(attrs, slog.String("service", service)) +} + +func appendNonEmpty(attrs []any, key string, value string) []any { + if strings.TrimSpace(value) == "" { + return attrs + } + return append(attrs, key, value) +} + +func stringFromMap(values map[string]any, key string) string { + value, ok := values[key] + if !ok { + return "" + } + switch typed := value.(type) { + case string: + return typed + case fmt.Stringer: + return typed.String() + default: + return fmt.Sprint(typed) + } +} + +func clientIP(request *http.Request) string { + if request == nil { + return "" + } + for _, header := range []string{"X-Forwarded-For", "X-Real-IP"} { + value := strings.TrimSpace(request.Header.Get(header)) + if value == "" { + continue + } + if comma := strings.IndexByte(value, ','); comma >= 0 { + return strings.TrimSpace(value[:comma]) + } + return value + } + return request.RemoteAddr +} + +func commonAttrsFromPayload(value any) []slog.Attr { + raw, err := marshalPayload(value) + if err != nil || len(raw) == 0 { + return nil + } + var payload map[string]any + if err := json.Unmarshal(raw, &payload); err != nil { + return nil + } + + keys := []string{ + "command_id", + "user_id", + "room_id", + "room_version", + "transaction_id", + "biz_type", + "asset_type", + "sender_user_id", + "target_user_id", + "seller_user_id", + "gift_id", + "gift_count", + "event_id", + "event_type", + } + attrs := make([]slog.Attr, 0, len(keys)) + for _, key := range keys { + value, ok := payload[key] + if !ok || value == nil { + continue + } + attrs = append(attrs, slog.Any(key, value)) + } + return attrs +} + +// ErrUnsupportedLevel is kept as a stable sentinel for configuration tests. +var ErrUnsupportedLevel = errors.New("unsupported log level") diff --git a/pkg/logx/logx_test.go b/pkg/logx/logx_test.go new file mode 100644 index 00000000..d1830fbe --- /dev/null +++ b/pkg/logx/logx_test.go @@ -0,0 +1,177 @@ +package logx + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + userv1 "hyapp.local/api/proto/user/v1" + walletv1 "hyapp.local/api/proto/wallet/v1" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestNormalizeConfigRejectsBadLevel(t *testing.T) { + _, err := NormalizeConfig(Config{Level: "verbose"}) + if !errors.Is(err, ErrUnsupportedLevel) { + t.Fatalf("NormalizeConfig error=%v, want ErrUnsupportedLevel", err) + } +} + +func TestSafePayloadRedactsSensitiveFieldsAndOmitsLargePayload(t *testing.T) { + payload := safePayload([]byte(`{"username":"tom","password":"pw","nested":{"user_sig":"sig"}}`), 2048, false) + values, ok := payload.(map[string]any) + if !ok { + t.Fatalf("payload type=%T, want map", payload) + } + if values["password"] != redactedValue { + t.Fatalf("password was not redacted: %+v", values) + } + nested := values["nested"].(map[string]any) + if nested["user_sig"] != redactedValue { + t.Fatalf("nested user_sig was not redacted: %+v", nested) + } + + large := safePayload([]byte(`{"token":"secret-token","data":"abcdef"}`), 8, true) + largeMap, ok := large.(map[string]any) + if !ok || largeMap["truncated"] != true { + t.Fatalf("large payload=%+v, want truncated marker", large) + } + if encoded := toJSON(t, large); bytes.Contains(encoded, []byte("secret-token")) { + t.Fatalf("truncated payload leaked sensitive value: %s", encoded) + } +} + +func TestHTTPMiddlewareUsesConfiguredBodyLoggingAndWarnLevel(t *testing.T) { + var output bytes.Buffer + if err := Init(Config{Service: "gateway-service", Env: "test", Level: "debug", Format: "json", IncludeRequestBody: true, IncludeResponseBody: true, MaxPayloadBytes: 2048, Output: &output}); err != nil { + t.Fatalf("Init failed: %v", err) + } + + handler := HTTPMiddleware("gateway-service", func(context.Context) string { return "req-http-1" }, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusBadRequest) + _, _ = writer.Write([]byte(`{"code":"INVALID_ARGUMENT","token":"server-token"}`)) + })) + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login?debug=1", bytes.NewBufferString(`{"username":"tom","password":"client-password"}`)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Forwarded-For", "203.0.113.10, 10.0.0.1") + handler.ServeHTTP(httptest.NewRecorder(), req) + + entry := decodeOne(t, output.Bytes()) + if entry["level"] != "WARN" || entry["msg"] != "http_access" || entry["service"] != "gateway-service" { + t.Fatalf("unexpected http log entry: %+v", entry) + } + if entry["request_id"] != "req-http-1" || entry["path"] != "/api/v1/auth/login" || entry["client_ip"] != "203.0.113.10" { + t.Fatalf("missing access fields: %+v", entry) + } + request := entry["request"].(map[string]any) + if request["password"] != redactedValue { + t.Fatalf("request body was not redacted: %+v", request) + } + response := entry["response"].(map[string]any) + if response["token"] != redactedValue { + t.Fatalf("response body was not redacted: %+v", response) + } +} + +func TestHTTPMiddlewareOmitsBodiesByDefault(t *testing.T) { + var output bytes.Buffer + if err := Init(Config{Service: "gateway-service", Env: "test", Format: "json", Output: &output}); err != nil { + t.Fatalf("Init failed: %v", err) + } + + handler := HTTPMiddleware("gateway-service", func(context.Context) string { return "req-http-2" }, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + _, _ = writer.Write([]byte(`{"ok":true}`)) + })) + handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/api/v1/ping", bytes.NewBufferString(`{"password":"client-password"}`))) + + entry := decodeOne(t, output.Bytes()) + if _, exists := entry["request"]; exists { + t.Fatalf("request body should be omitted by default: %+v", entry) + } + if _, exists := entry["response"]; exists { + t.Fatalf("response body should be omitted by default: %+v", entry) + } +} + +func TestUnaryServerInterceptorLevelsAndRequestMeta(t *testing.T) { + var output bytes.Buffer + if err := Init(Config{Service: "user-service", Env: "test", Format: "json", IncludeRequestBody: true, Output: &output}); err != nil { + t.Fatalf("Init failed: %v", err) + } + + interceptor := UnaryServerInterceptor("user-service") + request := &userv1.GetUserRequest{Meta: &userv1.RequestMeta{RequestId: "req-grpc-1", AppCode: "lalu"}, UserId: 1001} + _, err := interceptor(context.Background(), request, &grpc.UnaryServerInfo{FullMethod: "/hyapp.user.v1.UserService/GetUser"}, func(ctx context.Context, req any) (any, error) { + return nil, status.Error(codes.InvalidArgument, "bad user id") + }) + if err == nil { + t.Fatal("interceptor should return handler error") + } + + entry := decodeOne(t, output.Bytes()) + if entry["level"] != "WARN" || entry["msg"] != "grpc_access" || entry["request_id"] != "req-grpc-1" || entry["app_code"] != "lalu" { + t.Fatalf("unexpected grpc log entry: %+v", entry) + } + if entry["grpc_code"] != "InvalidArgument" { + t.Fatalf("grpc_code=%v, want InvalidArgument", entry["grpc_code"]) + } +} + +func TestUnaryServerInterceptorExtractsTopLevelTraceFieldsWithoutBody(t *testing.T) { + var output bytes.Buffer + if err := Init(Config{Service: "wallet-service", Env: "test", Format: "json", Output: &output}); err != nil { + t.Fatalf("Init failed: %v", err) + } + + interceptor := UnaryServerInterceptor("wallet-service") + request := &walletv1.AdminCreditAssetRequest{ + CommandId: "cmd-credit-1", + TargetUserId: 42001, + AssetType: "COIN_SELLER_COIN", + Amount: 80000, + AppCode: "lalu", + } + _, err := interceptor(context.Background(), request, &grpc.UnaryServerInfo{FullMethod: "/hyapp.wallet.v1.WalletService/AdminCreditAsset"}, func(ctx context.Context, req any) (any, error) { + return &walletv1.AdminCreditAssetResponse{}, nil + }) + if err != nil { + t.Fatalf("interceptor returned error: %v", err) + } + + entry := decodeOne(t, output.Bytes()) + if entry["app_code"] != "lalu" || entry["command_id"] != "cmd-credit-1" || entry["asset_type"] != "COIN_SELLER_COIN" { + t.Fatalf("missing top-level trace fields: %+v", entry) + } + if _, exists := entry["request"]; exists { + t.Fatalf("request body should remain omitted: %+v", entry) + } +} + +func decodeOne(t *testing.T, raw []byte) map[string]any { + t.Helper() + lines := bytes.Split(bytes.TrimSpace(raw), []byte("\n")) + if len(lines) != 1 || len(lines[0]) == 0 { + t.Fatalf("log lines=%q, want exactly one", raw) + } + var entry map[string]any + if err := json.Unmarshal(lines[0], &entry); err != nil { + t.Fatalf("decode log JSON failed: %v raw=%s", err, lines[0]) + } + return entry +} + +func toJSON(t *testing.T, value any) []byte { + t.Helper() + encoded, err := json.Marshal(value) + if err != nil { + t.Fatalf("json marshal failed: %v", err) + } + return encoded +} diff --git a/pkg/xerr/catalog.go b/pkg/xerr/catalog.go new file mode 100644 index 00000000..c2fcfdac --- /dev/null +++ b/pkg/xerr/catalog.go @@ -0,0 +1,113 @@ +package xerr + +import "google.golang.org/grpc/codes" + +const ( + httpStatusBadRequest = 400 + httpStatusUnauthorized = 401 + httpStatusForbidden = 403 + httpStatusNotFound = 404 + httpStatusConflict = 409 + httpStatusInternalServerError = 500 + httpStatusBadGateway = 502 +) + +// Spec 是错误码注册表中的一条稳定契约。 +// GRPCCode 是服务间传输语义;PublicCode/PublicMessage 是 gateway 对 App 暴露的 envelope。 +type Spec struct { + GRPCCode codes.Code + HTTPStatus int + PublicCode string + PublicMessage string + Retryable bool +} + +var catalog = map[Code]Spec{ + InvalidArgument: spec(codes.InvalidArgument, httpStatusBadRequest, InvalidArgument, "invalid argument"), + NotFound: spec(codes.NotFound, httpStatusNotFound, NotFound, "not found"), + Conflict: spec(codes.FailedPrecondition, httpStatusConflict, Conflict, "conflict"), + RoomClosed: spec(codes.FailedPrecondition, httpStatusConflict, RoomClosed, "room closed"), + + Unauthorized: spec(codes.Unauthenticated, httpStatusUnauthorized, Unauthorized, "unauthorized"), + PermissionDenied: spec(codes.PermissionDenied, httpStatusForbidden, PermissionDenied, "permission denied"), + ProfileRequired: spec(codes.PermissionDenied, httpStatusForbidden, ProfileRequired, "profile required"), + Unavailable: { + GRPCCode: codes.Unavailable, + HTTPStatus: httpStatusBadGateway, + PublicCode: "UPSTREAM_ERROR", + PublicMessage: "upstream service error", + Retryable: true, + }, + Internal: spec(codes.Internal, httpStatusInternalServerError, Internal, "internal error"), + + AuthFailed: spec(codes.Unauthenticated, httpStatusUnauthorized, AuthFailed, "authentication failed"), + PasswordAlreadySet: spec(codes.AlreadyExists, httpStatusConflict, PasswordAlreadySet, "conflict"), + UserDisabled: spec(codes.PermissionDenied, httpStatusForbidden, UserDisabled, "permission denied"), + SessionExpired: spec(codes.Unauthenticated, httpStatusUnauthorized, SessionExpired, "unauthorized"), + SessionRevoked: spec(codes.Unauthenticated, httpStatusUnauthorized, SessionRevoked, "unauthorized"), + + DisplayUserIDInvalid: spec(codes.InvalidArgument, httpStatusBadRequest, DisplayUserIDInvalid, "invalid argument"), + DisplayUserIDExists: spec(codes.AlreadyExists, httpStatusConflict, DisplayUserIDExists, "conflict"), + DisplayUserIDCooldown: spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDCooldown, "conflict"), + DisplayUserIDNotFound: spec(codes.NotFound, httpStatusNotFound, DisplayUserIDNotFound, "not found"), + DisplayUserIDAllocateFailed: spec(codes.Internal, httpStatusInternalServerError, Internal, "internal error"), + DisplayUserIDPrettyNotAvailable: spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDPrettyNotAvailable, "conflict"), + DisplayUserIDPrettyActive: spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDPrettyActive, "conflict"), + DisplayUserIDLeaseExpired: spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDLeaseExpired, "conflict"), + DisplayUserIDPaymentRequired: spec(codes.FailedPrecondition, httpStatusConflict, DisplayUserIDPaymentRequired, "conflict"), + + CountryChangeCooldown: spec(codes.FailedPrecondition, httpStatusConflict, CountryChangeCooldown, "conflict"), + CountryNotFound: spec(codes.NotFound, httpStatusNotFound, CountryNotFound, "not found"), + 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"), + + InsufficientBalance: spec(codes.FailedPrecondition, httpStatusConflict, InsufficientBalance, "insufficient balance"), + DuplicateBillingCommand: spec(codes.AlreadyExists, httpStatusConflict, DuplicateBillingCommand, "conflict"), + LedgerConflict: spec(codes.Aborted, httpStatusConflict, LedgerConflict, "conflict"), + CoinSellerNotActive: spec(codes.FailedPrecondition, httpStatusConflict, CoinSellerNotActive, "conflict"), + CoinSellerStockTypeInvalid: spec(codes.InvalidArgument, httpStatusBadRequest, CoinSellerStockTypeInvalid, "invalid argument"), + CoinSellerStockAmountInvalid: spec(codes.InvalidArgument, httpStatusBadRequest, CoinSellerStockAmountInvalid, "invalid argument"), + CoinSellerPaymentRefDuplicated: spec(codes.AlreadyExists, httpStatusConflict, CoinSellerPaymentRefDuplicated, "conflict"), + IdempotencyConflict: spec(codes.AlreadyExists, httpStatusConflict, IdempotencyConflict, "conflict"), + + RuleNotActive: spec(codes.FailedPrecondition, httpStatusConflict, RuleNotActive, "conflict"), + EventAlreadyConsumed: spec(codes.AlreadyExists, httpStatusConflict, EventAlreadyConsumed, "conflict"), + RewardPending: spec(codes.FailedPrecondition, httpStatusConflict, RewardPending, "conflict"), + + InvalidSection: spec(codes.InvalidArgument, httpStatusBadRequest, InvalidSection, "invalid argument"), + MessageNotFound: spec(codes.NotFound, httpStatusNotFound, MessageNotFound, "not found"), + MessageRecalled: spec(codes.NotFound, httpStatusNotFound, MessageRecalled, "not found"), + PageTokenInvalid: spec(codes.InvalidArgument, httpStatusBadRequest, PageTokenInvalid, "invalid argument"), + RequestConflict: spec(codes.FailedPrecondition, httpStatusConflict, RequestConflict, "conflict"), + ProducerEventConflict: spec(codes.FailedPrecondition, httpStatusConflict, ProducerEventConflict, "conflict"), +} + +func spec(grpcCode codes.Code, httpStatus int, publicCode Code, publicMessage string) Spec { + return Spec{ + GRPCCode: grpcCode, + HTTPStatus: httpStatus, + PublicCode: string(publicCode), + PublicMessage: publicMessage, + } +} + +// SpecOf 返回业务码的注册表项。未知业务码降级为 INTERNAL_ERROR,避免泄漏未登记 reason。 +func SpecOf(code Code) Spec { + if registered, ok := catalog[code]; ok { + return registered + } + + return catalog[Internal] +} + +// RegisteredCodes 返回当前注册表中全部业务码,供测试和文档生成使用。 +func RegisteredCodes() []Code { + registered := make([]Code, 0, len(catalog)) + for code := range catalog { + registered = append(registered, code) + } + + return registered +} diff --git a/pkg/xerr/errors.go b/pkg/xerr/errors.go index c156428e..dfe8d4e0 100644 --- a/pkg/xerr/errors.go +++ b/pkg/xerr/errors.go @@ -76,6 +76,16 @@ const ( DuplicateBillingCommand Code = "DUPLICATE_BILLING_COMMAND" // LedgerConflict 表示账本并发写入或余额版本冲突。 LedgerConflict Code = "LEDGER_CONFLICT" + // CoinSellerNotActive 表示目标用户不存在 active 币商身份。 + CoinSellerNotActive Code = "COIN_SELLER_NOT_ACTIVE" + // CoinSellerStockTypeInvalid 表示币商库存入账类型不属于支持范围。 + CoinSellerStockTypeInvalid Code = "COIN_SELLER_STOCK_TYPE_INVALID" + // CoinSellerStockAmountInvalid 表示币商库存金币数或线下付款金额不合法。 + CoinSellerStockAmountInvalid Code = "COIN_SELLER_STOCK_AMOUNT_INVALID" + // CoinSellerPaymentRefDuplicated 表示同一 USDT 收款凭证已经入账。 + CoinSellerPaymentRefDuplicated Code = "COIN_SELLER_PAYMENT_REF_DUPLICATED" + // IdempotencyConflict 表示 command_id 相同但业务 payload 不一致。 + IdempotencyConflict Code = "IDEMPOTENCY_CONFLICT" // RuleNotActive 表示活动规则当前不可用。 RuleNotActive Code = "RULE_NOT_ACTIVE" diff --git a/pkg/xerr/grpc.go b/pkg/xerr/grpc.go index 061070c3..f51474a8 100644 --- a/pkg/xerr/grpc.go +++ b/pkg/xerr/grpc.go @@ -13,30 +13,7 @@ const errorDomain = "hyapp" // GRPCCode 把稳定业务 reason 映射为 gRPC 标准 code。 // gateway 后续只需要读取 gRPC code 和 ErrorInfo.reason 即可生成 HTTP envelope。 func GRPCCode(code Code) codes.Code { - switch code { - case InvalidArgument, DisplayUserIDInvalid, InvalidInviteCode, InvalidSection, PageTokenInvalid: - return codes.InvalidArgument - case NotFound, DisplayUserIDNotFound, CountryNotFound, RegionNotFound, MessageNotFound, MessageRecalled: - return codes.NotFound - case Conflict, RoomClosed, RegionCountryConflict, RegionDisabled, RequestConflict, ProducerEventConflict: - return codes.FailedPrecondition - case Unauthorized, AuthFailed, SessionExpired, SessionRevoked: - return codes.Unauthenticated - case PermissionDenied, ProfileRequired, UserDisabled: - return codes.PermissionDenied - case Unavailable: - return codes.Unavailable - case PasswordAlreadySet, DisplayUserIDExists, DuplicateBillingCommand, EventAlreadyConsumed: - return codes.AlreadyExists - case DisplayUserIDCooldown, DisplayUserIDPrettyNotAvailable, DisplayUserIDPrettyActive, DisplayUserIDLeaseExpired, DisplayUserIDPaymentRequired, CountryChangeCooldown, InsufficientBalance, RuleNotActive, RewardPending: - return codes.FailedPrecondition - case DisplayUserIDAllocateFailed: - return codes.Internal - case LedgerConflict: - return codes.Aborted - default: - return codes.Internal - } + return SpecOf(code).GRPCCode } // ToGRPCError 只在 transport/grpc 层调用。 diff --git a/pkg/xerr/grpc_test.go b/pkg/xerr/grpc_test.go index 7fb33949..f18af1b3 100644 --- a/pkg/xerr/grpc_test.go +++ b/pkg/xerr/grpc_test.go @@ -1,6 +1,10 @@ package xerr import ( + "go/ast" + "go/parser" + "go/token" + "strconv" "testing" "google.golang.org/grpc/codes" @@ -24,3 +28,109 @@ func TestToGRPCErrorCarriesReason(t *testing.T) { t.Fatalf("reason mismatch: got %s want %s", got, PasswordAlreadySet) } } + +func TestCatalogCoversDeclaredCodes(t *testing.T) { + declared := declaredCodesFromSource(t) + + for code, name := range declared { + if _, ok := catalog[code]; !ok { + t.Fatalf("declared xerr code %s (%s) is missing from catalog", name, code) + } + } + + for code := range catalog { + if _, ok := declared[code]; !ok { + t.Fatalf("catalog registers undeclared xerr code %s", code) + } + } +} + +func TestCatalogMappings(t *testing.T) { + tests := []struct { + name string + code Code + grpcCode codes.Code + httpStatus int + publicCode string + publicMessage string + }{ + { + name: "business reason keeps canonical grpc code", + code: InvalidInviteCode, + grpcCode: codes.InvalidArgument, + httpStatus: httpStatusBadRequest, + publicCode: string(InvalidInviteCode), + publicMessage: "invalid argument", + }, + { + name: "unavailable is exposed as upstream error", + code: Unavailable, + grpcCode: codes.Unavailable, + httpStatus: httpStatusBadGateway, + publicCode: "UPSTREAM_ERROR", + publicMessage: "upstream service error", + }, + { + name: "ledger conflict uses aborted grpc but http conflict", + code: LedgerConflict, + grpcCode: codes.Aborted, + httpStatus: httpStatusConflict, + publicCode: string(LedgerConflict), + publicMessage: "conflict", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + spec := SpecOf(test.code) + if spec.GRPCCode != test.grpcCode || + spec.HTTPStatus != test.httpStatus || + spec.PublicCode != test.publicCode || + spec.PublicMessage != test.publicMessage { + t.Fatalf("catalog mismatch: got %+v", spec) + } + }) + } +} + +func declaredCodesFromSource(t *testing.T) map[Code]string { + t.Helper() + + file, err := parser.ParseFile(token.NewFileSet(), "errors.go", nil, 0) + if err != nil { + t.Fatalf("parse errors.go: %v", err) + } + + declared := make(map[Code]string) + ast.Inspect(file, func(node ast.Node) bool { + decl, ok := node.(*ast.GenDecl) + if !ok || decl.Tok != token.CONST { + return true + } + + for _, spec := range decl.Specs { + valueSpec, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + for index, name := range valueSpec.Names { + if index >= len(valueSpec.Values) { + continue + } + literal, ok := valueSpec.Values[index].(*ast.BasicLit) + if !ok || literal.Kind != token.STRING { + continue + } + value, err := strconv.Unquote(literal.Value) + if err != nil { + t.Fatalf("unquote %s: %v", literal.Value, err) + } + declared[Code(value)] = name.Name + } + } + + return false + }) + + return declared +} diff --git a/server/admin/cmd/server/main.go b/server/admin/cmd/server/main.go index 3000ef9d..8b18d764 100644 --- a/server/admin/cmd/server/main.go +++ b/server/admin/cmd/server/main.go @@ -6,6 +6,7 @@ import ( "errors" "flag" "log" + "log/slog" "net/http" "os" "os/signal" @@ -38,6 +39,7 @@ import ( roomadminmodule "hyapp-admin-server/internal/modules/roomadmin" searchmodule "hyapp-admin-server/internal/modules/search" uploadmodule "hyapp-admin-server/internal/modules/upload" + "hyapp-admin-server/internal/platform/logging" "hyapp-admin-server/internal/platform/tencentcos" "hyapp-admin-server/internal/repository" "hyapp-admin-server/internal/router" @@ -60,37 +62,40 @@ func main() { if err != nil { log.Fatal(err) } + if err := logging.Init(cfg.ServiceName, cfg.Environment, cfg.NodeID, cfg.Log); err != nil { + log.Fatal(err) + } if err := ensureDatabase(cfg.MySQLDSN); err != nil { - log.Fatalf("ensure mysql database: %v", err) + fatalRuntime("ensure_mysql_database_failed", err) } if err := ensureDatabase(cfg.WalletMySQLDSN); err != nil { - log.Fatalf("ensure wallet mysql database: %v", err) + fatalRuntime("ensure_wallet_mysql_database_failed", err) } if err := ensureDatabase(cfg.RoomMySQLDSN); err != nil { - log.Fatalf("ensure room mysql database: %v", err) + fatalRuntime("ensure_room_mysql_database_failed", err) } - dbLogger := logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{ + dbLogger := logger.New(log.New(logging.StdWriter(), "", 0), logger.Config{ LogLevel: logger.Warn, IgnoreRecordNotFoundError: true, }) db, err := gorm.Open(mysql.Open(cfg.MySQLDSN), &gorm.Config{Logger: dbLogger}) if err != nil { - log.Fatalf("connect mysql: %v", err) + fatalRuntime("connect_mysql_failed", err) } sqlDB, err := db.DB() if err != nil { - log.Fatalf("mysql db handle: %v", err) + fatalRuntime("mysql_db_handle_failed", err) } if cfg.Migrations.Enabled { if err := migration.Apply(context.Background(), sqlDB, cfg.Migrations.Dir); err != nil { - log.Fatalf("apply migrations: %v", err) + fatalRuntime("apply_migrations_failed", err) } } redisClient, err := cache.NewRedis(context.Background(), cfg.Redis) if err != nil { - log.Fatalf("connect redis: %v", err) + fatalRuntime("connect_redis_failed", err) } if redisClient != nil { defer redisClient.Close() @@ -98,52 +103,52 @@ func main() { userDB, err := sql.Open("mysql", cfg.UserMySQLDSN) if err != nil { - log.Fatalf("connect user mysql: %v", err) + fatalRuntime("connect_user_mysql_failed", err) } if err := userDB.PingContext(context.Background()); err != nil { - log.Fatalf("ping user mysql: %v", err) + fatalRuntime("ping_user_mysql_failed", err) } defer userDB.Close() walletDB, err := sql.Open("mysql", cfg.WalletMySQLDSN) if err != nil { - log.Fatalf("connect wallet mysql: %v", err) + fatalRuntime("connect_wallet_mysql_failed", err) } if err := walletDB.PingContext(context.Background()); err != nil { - log.Fatalf("ping wallet mysql: %v", err) + fatalRuntime("ping_wallet_mysql_failed", err) } defer walletDB.Close() roomDB, err := sql.Open("mysql", cfg.RoomMySQLDSN) if err != nil { - log.Fatalf("connect room mysql: %v", err) + fatalRuntime("connect_room_mysql_failed", err) } if err := roomDB.PingContext(context.Background()); err != nil { - log.Fatalf("ping room mysql: %v", err) + fatalRuntime("ping_room_mysql_failed", err) } defer roomDB.Close() store := repository.New(db) if cfg.MySQLAutoMigrate { if err := store.AutoMigrate(); err != nil { - log.Fatalf("auto migrate: %v", err) + fatalRuntime("auto_migrate_failed", err) } } if *runBootstrap || cfg.Bootstrap.Enabled { if err := store.Seed(cfg); err != nil { - log.Fatalf("seed database: %v", err) + fatalRuntime("seed_database_failed", err) } } userConn, err := grpc.Dial(cfg.UserService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - log.Fatalf("connect user-service: %v", err) + fatalRuntime("connect_user_service_failed", err) } defer userConn.Close() walletConn, err := grpc.Dial(cfg.WalletService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - log.Fatalf("connect wallet-service: %v", err) + fatalRuntime("connect_wallet_service_failed", err) } defer walletConn.Close() @@ -168,7 +173,7 @@ func main() { AccessURL: cfg.TencentCOS.AccessURL, }, nil) if err != nil { - log.Fatalf("create tencent cos uploader: %v", err) + fatalRuntime("create_tencent_cos_uploader_failed", err) } objectUploader = uploader } @@ -182,7 +187,7 @@ func main() { CountryRegion: countryregionmodule.New(userclient.NewGRPC(userConn), userDB, cfg, auditHandler), Dashboard: dashboardmodule.New(store), Health: healthmodule.New(store, redisClient, jobStatus), - HostOrg: hostorgmodule.New(userclient.NewGRPC(userConn), userDB, walletDB, auditHandler), + HostOrg: hostorgmodule.New(userclient.NewGRPC(userConn), walletclient.NewGRPC(walletConn), userDB, walletDB, sqlDB, auditHandler), Job: jobmodule.New(store, cfg, auditHandler), Menu: menumodule.New(store, auditHandler), Notification: notificationmodule.New(store, auditHandler), @@ -202,9 +207,9 @@ func main() { } go func() { - log.Printf("hyapp-admin-server listening on %s", cfg.HTTPAddr) + slog.Info("service_listening", "addr", cfg.HTTPAddr) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("listen: %v", err) + fatalRuntime("listen_failed", err) } }() @@ -215,10 +220,15 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { - log.Printf("shutdown: %v", err) + slog.Error("shutdown_failed", "error", err.Error()) } } +func fatalRuntime(msg string, err error) { + slog.Error(msg, "error", err.Error()) + os.Exit(1) +} + func ensureDatabase(dsn string) error { parsed, err := mysqlDriver.ParseDSN(dsn) if err != nil { diff --git a/server/admin/configs/config.yaml b/server/admin/configs/config.yaml index 1831e225..9e5364b2 100644 --- a/server/admin/configs/config.yaml +++ b/server/admin/configs/config.yaml @@ -1,6 +1,12 @@ service_name: admin-server node_id: admin-local environment: local +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 http_addr: ":13100" mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_admin?parseTime=true&charset=utf8mb4&loc=Local" user_mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=Local" diff --git a/server/admin/docs/permission-management.md b/server/admin/docs/permission-management.md index b6b09a26..edab2baf 100644 --- a/server/admin/docs/permission-management.md +++ b/server/admin/docs/permission-management.md @@ -325,6 +325,7 @@ | `coin-seller:view` | `menu` | 币商列表、币商余额 | | `coin-seller:create` | `button` | 新增币商 | | `coin-seller:update` | `button` | 启用或禁用币商 | +| `coin-seller:stock-credit` | `button` | 币商进货、金币补偿 | | `salary:view` | `menu` | 薪资周期、薪资明细 | | `salary:calculate` | `button` | 计算薪资 | | `salary:approve` | `button` | 审批薪资 | diff --git a/server/admin/internal/config/config.go b/server/admin/internal/config/config.go index 4ac9f8eb..e68da971 100644 --- a/server/admin/internal/config/config.go +++ b/server/admin/internal/config/config.go @@ -7,6 +7,8 @@ import ( "strings" "time" + "hyapp-admin-server/internal/platform/logging" + "gopkg.in/yaml.v3" ) @@ -14,6 +16,7 @@ type Config struct { ServiceName string `yaml:"service_name"` NodeID string `yaml:"node_id"` Environment string `yaml:"environment"` + Log logging.Config `yaml:"log"` HTTPAddr string `yaml:"http_addr"` MySQLDSN string `yaml:"mysql_dsn"` UserMySQLDSN string `yaml:"user_mysql_dsn"` @@ -90,6 +93,7 @@ func Default() Config { ServiceName: "admin-server", NodeID: "admin-local", Environment: "local", + Log: logging.DefaultConfig(), HTTPAddr: ":13100", MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_admin?parseTime=true&charset=utf8mb4&loc=Local", UserMySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=Local", @@ -172,6 +176,15 @@ func (cfg *Config) Normalize() { if cfg.Environment == "" { cfg.Environment = "local" } + if cfg.Log.Level == "" { + cfg.Log.Level = "info" + } + if cfg.Log.Format == "" { + cfg.Log.Format = "json" + } + if cfg.Log.MaxPayloadBytes <= 0 { + cfg.Log.MaxPayloadBytes = 2048 + } cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) cfg.NodeID = strings.TrimSpace(cfg.NodeID) if cfg.NodeID == "" { diff --git a/server/admin/internal/integration/userclient/client.go b/server/admin/internal/integration/userclient/client.go index ed057320..3e916a51 100644 --- a/server/admin/internal/integration/userclient/client.go +++ b/server/admin/internal/integration/userclient/client.go @@ -24,6 +24,7 @@ type Client interface { SetBDStatus(ctx context.Context, req SetBDStatusRequest) (*BDProfile, error) CreateCoinSeller(ctx context.Context, req CreateCoinSellerRequest) (*CoinSellerProfile, error) SetCoinSellerStatus(ctx context.Context, req SetCoinSellerStatusRequest) (*CoinSellerProfile, error) + GetCoinSellerProfile(ctx context.Context, req GetCoinSellerProfileRequest) (*CoinSellerProfile, error) CreateAgency(ctx context.Context, req CreateAgencyRequest) (*CreateAgencyResult, error) CloseAgency(ctx context.Context, req CloseAgencyRequest) (*Agency, error) SetAgencyJoinEnabled(ctx context.Context, req SetAgencyJoinEnabledRequest) (*Agency, error) @@ -85,6 +86,7 @@ type GRPCClient struct { identityClient userv1.UserIdentityServiceClient countryClient userv1.CountryAdminServiceClient regionClient userv1.RegionAdminServiceClient + hostClient userv1.UserHostServiceClient hostAdminClient userv1.UserHostAdminServiceClient } @@ -94,6 +96,7 @@ func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient { identityClient: userv1.NewUserIdentityServiceClient(conn), countryClient: userv1.NewCountryAdminServiceClient(conn), regionClient: userv1.NewRegionAdminServiceClient(conn), + hostClient: userv1.NewUserHostServiceClient(conn), hostAdminClient: userv1.NewUserHostAdminServiceClient(conn), } } @@ -223,6 +226,12 @@ type SetCoinSellerStatusRequest struct { Reason string } +type GetCoinSellerProfileRequest struct { + RequestID string + Caller string + UserID int64 +} + type CreateAgencyRequest struct { RequestID string Caller string @@ -256,28 +265,46 @@ type SetAgencyJoinEnabledRequest struct { } type BDProfile struct { - UserID int64 `json:"userId,string"` - Role string `json:"role"` - RegionID int64 `json:"regionId"` - ParentLeaderUserID int64 `json:"parentLeaderUserId,string"` - Status string `json:"status"` - CreatedByUserID int64 `json:"createdByUserId"` - CreatedAtMs int64 `json:"createdAtMs"` - UpdatedAtMs int64 `json:"updatedAtMs"` + UserID int64 `json:"userId,string"` + Role string `json:"role"` + RegionID int64 `json:"regionId"` + ParentLeaderUserID int64 `json:"parentLeaderUserId,string"` + Status string `json:"status"` + CreatedByUserID int64 `json:"createdByUserId"` + 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"` } type Agency struct { - AgencyID int64 `json:"agencyId"` - 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"` - CreatedAtMs int64 `json:"createdAtMs"` - UpdatedAtMs int64 `json:"updatedAtMs"` + AgencyID int64 `json:"agencyId"` + 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"` + 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"` } type HostProfile struct { @@ -290,6 +317,11 @@ type HostProfile struct { 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"` } type CoinSellerProfile struct { @@ -396,6 +428,17 @@ func (c *GRPCClient) SetCoinSellerStatus(ctx context.Context, req SetCoinSellerS return fromProtoCoinSellerProfile(resp.GetCoinSellerProfile()), nil } +func (c *GRPCClient) GetCoinSellerProfile(ctx context.Context, req GetCoinSellerProfileRequest) (*CoinSellerProfile, error) { + resp, err := c.hostClient.GetCoinSellerProfile(ctx, &userv1.GetCoinSellerProfileRequest{ + Meta: requestMeta(ctx, req.RequestID, req.Caller), + UserId: req.UserID, + }) + if err != nil { + return nil, err + } + return fromProtoCoinSellerProfile(resp.GetCoinSellerProfile()), nil +} + func (c *GRPCClient) CreateAgency(ctx context.Context, req CreateAgencyRequest) (*CreateAgencyResult, error) { resp, err := c.hostAdminClient.CreateAgency(ctx, &userv1.CreateAgencyRequest{ Meta: requestMeta(ctx, req.RequestID, req.Caller), diff --git a/server/admin/internal/integration/walletclient/client.go b/server/admin/internal/integration/walletclient/client.go index a81ab887..bc15cabb 100644 --- a/server/admin/internal/integration/walletclient/client.go +++ b/server/admin/internal/integration/walletclient/client.go @@ -27,6 +27,7 @@ type Client interface { GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error) GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) ListResourceGrants(ctx context.Context, req *walletv1.ListResourceGrantsRequest) (*walletv1.ListResourceGrantsResponse, error) + AdminCreditCoinSellerStock(ctx context.Context, req *walletv1.AdminCreditCoinSellerStockRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) ListRechargeBills(ctx context.Context, req *walletv1.ListRechargeBillsRequest) (*walletv1.ListRechargeBillsResponse, error) } @@ -106,6 +107,10 @@ func (c *GRPCClient) ListResourceGrants(ctx context.Context, req *walletv1.ListR return c.client.ListResourceGrants(ctx, req) } +func (c *GRPCClient) AdminCreditCoinSellerStock(ctx context.Context, req *walletv1.AdminCreditCoinSellerStockRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) { + return c.client.AdminCreditCoinSellerStock(ctx, req) +} + func (c *GRPCClient) ListRechargeBills(ctx context.Context, req *walletv1.ListRechargeBillsRequest) (*walletv1.ListRechargeBillsResponse, error) { return c.client.ListRechargeBills(ctx, req) } diff --git a/server/admin/internal/job/runner.go b/server/admin/internal/job/runner.go index ce20f281..5faf32bb 100644 --- a/server/admin/internal/job/runner.go +++ b/server/admin/internal/job/runner.go @@ -7,7 +7,7 @@ import ( "encoding/json" "errors" "fmt" - "log" + "log/slog" "os" "path/filepath" "strings" @@ -128,12 +128,12 @@ func (r *Runner) runOnce(ctx context.Context) { if errors.Is(err, gorm.ErrRecordNotFound) { return } - log.Printf("find pending job: %v", err) + slog.Error("job_find_pending_failed", "worker_id", r.workerID, "error", err.Error()) return } locked, unlock, err := r.lock(ctx, job) if err != nil { - log.Printf("lock job: %v", err) + slog.Error("job_lock_failed", "worker_id", r.workerID, "job_id", job.ID, "error", err.Error()) _ = r.store.ReleaseJobLease(job.ID) return } diff --git a/server/admin/internal/modules/hostorg/handler.go b/server/admin/internal/modules/hostorg/handler.go index dc30db15..8d693f81 100644 --- a/server/admin/internal/modules/hostorg/handler.go +++ b/server/admin/internal/modules/hostorg/handler.go @@ -7,6 +7,7 @@ import ( "strings" "hyapp-admin-server/internal/integration/userclient" + "hyapp-admin-server/internal/integration/walletclient" "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/response" @@ -19,8 +20,8 @@ type Handler struct { audit shared.OperationLogger } -func New(userClient userclient.Client, userDB *sql.DB, walletDB *sql.DB, audit shared.OperationLogger) *Handler { - return &Handler{service: NewService(userClient, NewReader(userDB, walletDB)), audit: audit} +func New(userClient userclient.Client, walletClient walletclient.Client, userDB *sql.DB, walletDB *sql.DB, adminDB *sql.DB, audit shared.OperationLogger) *Handler { + return &Handler{service: NewService(userClient, walletClient, NewReader(userDB, walletDB, adminDB)), audit: audit} } func (h *Handler) ListBDLeaders(c *gin.Context) { @@ -140,6 +141,21 @@ func (h *Handler) SetBDStatus(c *gin.Context) { response.OK(c, profile) } +func (h *Handler) DeleteBDLeader(c *gin.Context) { + targetUserID, ok := parseInt64ID(c, "user_id") + if !ok { + return + } + profile, err := h.service.DeleteBDLeader(c.Request.Context(), targetUserID) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + writeHostOrgAuditLog(c, h.audit, "delete-bd-leader", "bd_profiles", profile.UserID, + fmt.Sprintf("user_id=%d", profile.UserID)) + response.OK(c, profile) +} + func (h *Handler) CreateCoinSeller(c *gin.Context) { var req createCoinSellerRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -176,6 +192,27 @@ func (h *Handler) SetCoinSellerStatus(c *gin.Context) { response.OK(c, profile) } +func (h *Handler) CreditCoinSellerStock(c *gin.Context) { + sellerUserID, ok := parseInt64ID(c, "user_id") + if !ok { + return + } + var req coinSellerStockCreditRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "币商进货参数不正确") + return + } + result, err := h.service.CreditCoinSellerStock(c.Request.Context(), adminActorID(c), sellerUserID, middleware.CurrentRequestID(c), req) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + writeHostOrgAuditLog(c, h.audit, "coin-seller-stock-credit", "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, coinSellerStockCreditFromProto(result)) +} + func (h *Handler) CreateAgency(c *gin.Context) { var req createAgencyRequest if err := c.ShouldBindJSON(&req); err != nil { diff --git a/server/admin/internal/modules/hostorg/reader.go b/server/admin/internal/modules/hostorg/reader.go index bfa8c709..a0833590 100644 --- a/server/admin/internal/modules/hostorg/reader.go +++ b/server/admin/internal/modules/hostorg/reader.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "strings" + "time" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/userclient" @@ -13,10 +14,11 @@ import ( type Reader struct { db *sql.DB walletDB *sql.DB + adminDB *sql.DB } -func NewReader(db *sql.DB, walletDB *sql.DB) *Reader { - return &Reader{db: db, walletDB: walletDB} +func NewReader(db *sql.DB, walletDB *sql.DB, adminDB *sql.DB) *Reader { + return &Reader{db: db, walletDB: walletDB, adminDB: adminDB} } type CoinSellerListItem struct { @@ -24,6 +26,7 @@ type CoinSellerListItem struct { Status string `json:"status"` MerchantAssetType string `json:"merchantAssetType"` MerchantBalance int64 `json:"merchantBalance"` + Contact string `json:"contact"` DisplayUserID string `json:"displayUserId"` Username string `json:"username"` Avatar string `json:"avatar"` @@ -40,7 +43,13 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin return nil, 0, errUserDBNotConfigured() } appCode := appctx.FromContext(ctx) - whereSQL := "FROM bd_profiles bp LEFT JOIN users u ON u.app_code = bp.app_code AND u.user_id = bp.user_id WHERE bp.app_code = ? AND bp.role = ?" + whereSQL := ` + FROM bd_profiles bp + LEFT JOIN users u ON u.app_code = bp.app_code AND u.user_id = bp.user_id + LEFT JOIN users parent_leader ON parent_leader.app_code = bp.app_code AND parent_leader.user_id = bp.parent_leader_user_id + LEFT JOIN users creator ON creator.app_code = bp.app_code AND creator.user_id = bp.created_by_user_id + LEFT JOIN regions r ON r.app_code = bp.app_code AND r.region_id = bp.region_id + WHERE bp.app_code = ? AND bp.role = ?` args := []any{appCode, role} if query.Status != "" { whereSQL += " AND bp.status = ?" @@ -56,8 +65,8 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin } if keyword := strings.TrimSpace(query.Keyword); keyword != "" { like := "%" + keyword + "%" - whereSQL += " AND (CAST(bp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ?)" - args = append(args, like, like, like) + whereSQL += " AND (CAST(bp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ? OR r.name LIKE ?)" + args = append(args, like, like, like, like) } total, err := countRows(ctx, r.db, whereSQL, args...) @@ -66,7 +75,20 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin } rows, err := r.db.QueryContext(ctx, fmt.Sprintf(` SELECT bp.user_id, bp.role, bp.region_id, COALESCE(bp.parent_leader_user_id, 0), - bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms + bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms, + COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''), + COALESCE(u.avatar, ''), COALESCE(r.name, ''), + COALESCE(parent_leader.current_display_user_id, ''), COALESCE(parent_leader.username, ''), + COALESCE(parent_leader.avatar, ''), + COALESCE(creator.current_display_user_id, ''), COALESCE(creator.username, ''), + COALESCE(creator.avatar, ''), + ( + SELECT COUNT(1) + FROM bd_profiles child + WHERE child.app_code = bp.app_code + AND child.role = 'bd' + AND child.parent_leader_user_id = bp.user_id + ) %s ORDER BY bp.created_at_ms DESC, bp.user_id DESC LIMIT ? OFFSET ? @@ -79,12 +101,139 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin items := make([]*userclient.BDProfile, 0, query.PageSize) for rows.Next() { item := &userclient.BDProfile{} - if err := rows.Scan(&item.UserID, &item.Role, &item.RegionID, &item.ParentLeaderUserID, &item.Status, &item.CreatedByUserID, &item.CreatedAtMs, &item.UpdatedAtMs); err != nil { + if err := rows.Scan( + &item.UserID, + &item.Role, + &item.RegionID, + &item.ParentLeaderUserID, + &item.Status, + &item.CreatedByUserID, + &item.CreatedAtMs, + &item.UpdatedAtMs, + &item.DisplayUserID, + &item.Username, + &item.Avatar, + &item.RegionName, + &item.ParentLeaderDisplayID, + &item.ParentLeaderUsername, + &item.ParentLeaderAvatar, + &item.CreatedByDisplayUserID, + &item.CreatedByUsername, + &item.CreatedByAvatar, + &item.SubBDCount, + ); err != nil { return nil, 0, err } items = append(items, item) } - return items, total, rows.Err() + if err := rows.Err(); err != nil { + return nil, 0, err + } + if err := r.fillBDProfileCreators(ctx, items); err != nil { + return nil, 0, err + } + return items, total, nil +} + +func (r *Reader) fillBDProfileCreators(ctx context.Context, items []*userclient.BDProfile) error { + if r == nil || r.adminDB == nil || len(items) == 0 { + return nil + } + seen := make(map[int64]struct{}, len(items)) + ids := make([]any, 0, len(items)) + for _, item := range items { + if item.CreatedByUserID <= 0 { + continue + } + if _, ok := seen[item.CreatedByUserID]; ok { + continue + } + seen[item.CreatedByUserID] = struct{}{} + ids = append(ids, item.CreatedByUserID) + } + if len(ids) == 0 { + return nil + } + + rows, err := r.adminDB.QueryContext(ctx, ` + SELECT id, username, name + FROM admin_users + WHERE id IN (`+sqlPlaceholders(len(ids))+`) + `, ids...) + if err != nil { + return err + } + defer rows.Close() + + type creator struct { + account string + name string + } + creators := make(map[int64]creator, len(ids)) + for rows.Next() { + var id int64 + var item creator + if err := rows.Scan(&id, &item.account, &item.name); err != nil { + return err + } + creators[id] = item + } + if err := rows.Err(); err != nil { + return err + } + for _, item := range items { + creator, ok := creators[item.CreatedByUserID] + if !ok { + continue + } + item.CreatedByDisplayUserID = creator.account + item.CreatedByUsername = firstNonBlank(creator.name, creator.account) + } + return nil +} + +func (r *Reader) DeleteBDLeader(ctx context.Context, userID int64) (*userclient.BDProfile, error) { + if r == nil || r.db == nil { + return nil, errUserDBNotConfigured() + } + appCode := appctx.FromContext(ctx) + item := &userclient.BDProfile{} + err := r.db.QueryRowContext(ctx, ` + SELECT bp.user_id, bp.role, bp.region_id, COALESCE(bp.parent_leader_user_id, 0), + bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms + FROM bd_profiles bp + WHERE bp.app_code = ? AND bp.user_id = ? AND bp.role = 'bd_leader' + `, appCode, userID).Scan( + &item.UserID, + &item.Role, + &item.RegionID, + &item.ParentLeaderUserID, + &item.Status, + &item.CreatedByUserID, + &item.CreatedAtMs, + &item.UpdatedAtMs, + ) + if err == sql.ErrNoRows { + return nil, fmt.Errorf("bd leader not found") + } + if err != nil { + return nil, err + } + result, err := r.db.ExecContext(ctx, ` + DELETE FROM bd_profiles + WHERE app_code = ? AND user_id = ? AND role = 'bd_leader' + `, appCode, userID) + if err != nil { + return nil, err + } + affected, err := result.RowsAffected() + if err != nil { + return nil, err + } + if affected == 0 { + return nil, fmt.Errorf("bd leader not found") + } + return item, nil } // ListAgencies 只读 user-service 的 Agency 事实表;后台关闭和入会开关仍走写命令。 @@ -93,7 +242,12 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie return nil, 0, errUserDBNotConfigured() } appCode := appctx.FromContext(ctx) - whereSQL := "FROM agencies a LEFT JOIN users owner ON owner.app_code = a.app_code AND owner.user_id = a.owner_user_id WHERE a.app_code = ?" + whereSQL := ` + FROM agencies a + LEFT JOIN users owner ON owner.app_code = a.app_code AND owner.user_id = a.owner_user_id + LEFT JOIN users parent_bd ON parent_bd.app_code = a.app_code AND parent_bd.user_id = a.parent_bd_user_id + LEFT JOIN regions r ON r.app_code = a.app_code AND r.region_id = a.region_id + WHERE a.app_code = ?` args := []any{appCode} if query.Status != "" { whereSQL += " AND a.status = ?" @@ -109,8 +263,8 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie } if keyword := strings.TrimSpace(query.Keyword); keyword != "" { like := "%" + keyword + "%" - whereSQL += " AND (a.name LIKE ? OR CAST(a.agency_id AS CHAR) LIKE ? OR CAST(a.owner_user_id AS CHAR) LIKE ? OR owner.current_display_user_id LIKE ? OR owner.username LIKE ?)" - args = append(args, like, like, like, like, like) + whereSQL += " AND (a.name LIKE ? OR CAST(a.agency_id AS CHAR) LIKE ? OR CAST(a.owner_user_id AS CHAR) LIKE ? OR owner.current_display_user_id LIKE ? OR owner.username LIKE ? OR CAST(a.parent_bd_user_id AS CHAR) LIKE ? OR parent_bd.current_display_user_id LIKE ? OR parent_bd.username LIKE ? OR r.name LIKE ?)" + args = append(args, like, like, like, like, like, like, like, like, like) } total, err := countRows(ctx, r.db, whereSQL, args...) @@ -120,7 +274,11 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie rows, err := r.db.QueryContext(ctx, fmt.Sprintf(` SELECT a.agency_id, a.owner_user_id, a.region_id, a.parent_bd_user_id, a.name, a.status, a.join_enabled, a.max_hosts, - a.created_by_user_id, a.created_at_ms, a.updated_at_ms + a.created_by_user_id, a.created_at_ms, a.updated_at_ms, + COALESCE(owner.current_display_user_id, ''), COALESCE(owner.username, ''), + COALESCE(owner.avatar, ''), + COALESCE(parent_bd.current_display_user_id, ''), COALESCE(parent_bd.username, ''), + COALESCE(parent_bd.avatar, ''), COALESCE(r.name, '') %s ORDER BY a.created_at_ms DESC, a.agency_id DESC LIMIT ? OFFSET ? @@ -133,7 +291,26 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie items := make([]*userclient.Agency, 0, query.PageSize) for rows.Next() { item := &userclient.Agency{} - if err := rows.Scan(&item.AgencyID, &item.OwnerUserID, &item.RegionID, &item.ParentBDUserID, &item.Name, &item.Status, &item.JoinEnabled, &item.MaxHosts, &item.CreatedByUserID, &item.CreatedAtMs, &item.UpdatedAtMs); err != nil { + if err := rows.Scan( + &item.AgencyID, + &item.OwnerUserID, + &item.RegionID, + &item.ParentBDUserID, + &item.Name, + &item.Status, + &item.JoinEnabled, + &item.MaxHosts, + &item.CreatedByUserID, + &item.CreatedAtMs, + &item.UpdatedAtMs, + &item.OwnerDisplayUserID, + &item.OwnerUsername, + &item.OwnerAvatar, + &item.ParentBDDisplayUserID, + &item.ParentBDUsername, + &item.ParentBDAvatar, + &item.RegionName, + ); err != nil { return nil, 0, err } items = append(items, item) @@ -147,7 +324,12 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user return nil, 0, errUserDBNotConfigured() } appCode := appctx.FromContext(ctx) - whereSQL := "FROM host_profiles hp LEFT JOIN users u ON u.app_code = hp.app_code AND u.user_id = hp.user_id LEFT JOIN agencies a ON a.app_code = hp.app_code AND a.agency_id = hp.current_agency_id WHERE hp.app_code = ?" + whereSQL := ` + FROM host_profiles hp + LEFT JOIN users u ON u.app_code = hp.app_code AND u.user_id = hp.user_id + LEFT JOIN agencies a ON a.app_code = hp.app_code AND a.agency_id = hp.current_agency_id + LEFT JOIN regions r ON r.app_code = hp.app_code AND r.region_id = hp.region_id + WHERE hp.app_code = ?` args := []any{appCode} if query.Status != "" { whereSQL += " AND hp.status = ?" @@ -163,8 +345,8 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user } if keyword := strings.TrimSpace(query.Keyword); keyword != "" { like := "%" + keyword + "%" - whereSQL += " AND (CAST(hp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ? OR a.name LIKE ?)" - args = append(args, like, like, like, like) + whereSQL += " AND (CAST(hp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ? OR a.name LIKE ? OR r.name LIKE ?)" + args = append(args, like, like, like, like, like) } total, err := countRows(ctx, r.db, whereSQL, args...) @@ -174,7 +356,9 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user rows, err := r.db.QueryContext(ctx, fmt.Sprintf(` SELECT hp.user_id, hp.status, hp.region_id, COALESCE(hp.current_agency_id, 0), COALESCE(hp.current_membership_id, 0), hp.source, - hp.first_became_host_at_ms, hp.created_at_ms, hp.updated_at_ms + hp.first_became_host_at_ms, hp.created_at_ms, hp.updated_at_ms, + COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''), + COALESCE(u.avatar, ''), COALESCE(r.name, ''), COALESCE(a.name, '') %s ORDER BY hp.created_at_ms DESC, hp.user_id DESC LIMIT ? OFFSET ? @@ -187,7 +371,22 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user items := make([]*userclient.HostProfile, 0, query.PageSize) for rows.Next() { item := &userclient.HostProfile{} - if err := rows.Scan(&item.UserID, &item.Status, &item.RegionID, &item.CurrentAgencyID, &item.CurrentMembershipID, &item.Source, &item.FirstBecameHostAtMs, &item.CreatedAtMs, &item.UpdatedAtMs); err != nil { + if err := rows.Scan( + &item.UserID, + &item.Status, + &item.RegionID, + &item.CurrentAgencyID, + &item.CurrentMembershipID, + &item.Source, + &item.FirstBecameHostAtMs, + &item.CreatedAtMs, + &item.UpdatedAtMs, + &item.DisplayUserID, + &item.Username, + &item.Avatar, + &item.RegionName, + &item.CurrentAgencyName, + ); err != nil { return nil, 0, err } items = append(items, item) @@ -201,6 +400,9 @@ func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinS return nil, 0, errUserDBNotConfigured() } appCode := appctx.FromContext(ctx) + if err := r.ensureCoinSellerContactColumn(ctx); err != nil { + return nil, 0, err + } whereSQL := ` FROM coin_seller_profiles csp LEFT JOIN users u ON u.app_code = csp.app_code AND u.user_id = csp.user_id @@ -217,8 +419,8 @@ func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinS } if keyword := strings.TrimSpace(query.Keyword); keyword != "" { like := "%" + keyword + "%" - whereSQL += " AND (CAST(csp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ?)" - args = append(args, like, like, like) + whereSQL += " AND (CAST(csp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ? OR csp.contact_info LIKE ?)" + args = append(args, like, like, like, like) } total, err := countRows(ctx, r.db, whereSQL, args...) @@ -227,6 +429,7 @@ func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinS } rows, err := r.db.QueryContext(ctx, fmt.Sprintf(` SELECT csp.user_id, csp.status, csp.merchant_asset_type, + COALESCE(csp.contact_info, ''), csp.created_by_user_id, csp.created_at_ms, csp.updated_at_ms, COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''), COALESCE(u.avatar, ''), COALESCE(u.region_id, 0), @@ -248,6 +451,7 @@ func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinS &item.UserID, &item.Status, &item.MerchantAssetType, + &item.Contact, &item.CreatedByUserID, &item.CreatedAtMs, &item.UpdatedAtMs, @@ -275,6 +479,42 @@ func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinS return items, total, nil } +func (r *Reader) UpdateCoinSellerContact(ctx context.Context, userID int64, contact string) error { + if r == nil || r.db == nil { + return errUserDBNotConfigured() + } + if err := r.ensureCoinSellerContactColumn(ctx); err != nil { + return err + } + _, err := r.db.ExecContext(ctx, ` + UPDATE coin_seller_profiles + SET contact_info = ?, updated_at_ms = ? + WHERE app_code = ? AND user_id = ? + `, strings.TrimSpace(contact), time.Now().UnixMilli(), appctx.FromContext(ctx), userID) + return err +} + +func (r *Reader) ensureCoinSellerContactColumn(ctx context.Context) error { + var count int + if err := r.db.QueryRowContext(ctx, ` + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'coin_seller_profiles' + AND column_name = 'contact_info' + `).Scan(&count); err != nil { + return err + } + if count > 0 { + return nil + } + _, err := r.db.ExecContext(ctx, ` + ALTER TABLE coin_seller_profiles + ADD COLUMN contact_info VARCHAR(128) NOT NULL DEFAULT '' AFTER merchant_asset_type + `) + return err +} + func (r *Reader) coinSellerBalances(ctx context.Context, appCode string, userIDs []int64) (map[int64]int64, error) { result := make(map[int64]int64, len(userIDs)) if r == nil || r.walletDB == nil || len(userIDs) == 0 { @@ -305,6 +545,13 @@ func (r *Reader) coinSellerBalances(ctx context.Context, appCode string, userIDs return result, rows.Err() } +func sqlPlaceholders(count int) string { + if count <= 0 { + return "" + } + return strings.TrimRight(strings.Repeat("?,", count), ",") +} + 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) diff --git a/server/admin/internal/modules/hostorg/request.go b/server/admin/internal/modules/hostorg/request.go index 5aa84cec..1220a955 100644 --- a/server/admin/internal/modules/hostorg/request.go +++ b/server/admin/internal/modules/hostorg/request.go @@ -33,14 +33,26 @@ type bdStatusRequest struct { type createCoinSellerRequest struct { CommandID string `json:"commandId" binding:"required"` + Contact string `json:"contact"` TargetUserID int64 `json:"targetUserId" binding:"required"` Reason string `json:"reason"` } type coinSellerStatusRequest struct { - CommandID string `json:"commandId" binding:"required"` - Status string `json:"status" binding:"required"` - Reason string `json:"reason"` + CommandID string `json:"commandId" binding:"required"` + Contact *string `json:"contact"` + Status string `json:"status" binding:"required"` + Reason string `json:"reason"` +} + +type coinSellerStockCreditRequest struct { + CommandID string `json:"commandId" binding:"required"` + Type string `json:"type" binding:"required"` + CoinAmount int64 `json:"coinAmount" binding:"required"` + RechargeAmount string `json:"rechargeAmount"` + PaymentRef string `json:"paymentRef"` + EvidenceRef string `json:"evidenceRef"` + Reason string `json:"reason"` } type createAgencyRequest struct { diff --git a/server/admin/internal/modules/hostorg/response.go b/server/admin/internal/modules/hostorg/response.go new file mode 100644 index 00000000..a9ecef0e --- /dev/null +++ b/server/admin/internal/modules/hostorg/response.go @@ -0,0 +1,32 @@ +package hostorg + +import walletv1 "hyapp.local/api/proto/wallet/v1" + +type coinSellerStockCreditResponse struct { + TransactionID string `json:"transactionId"` + SellerUserID int64 `json:"sellerUserId,string"` + StockType string `json:"stockType"` + CoinAmount int64 `json:"coinAmount"` + 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 { + if item == nil { + return coinSellerStockCreditResponse{} + } + return coinSellerStockCreditResponse{ + TransactionID: item.GetTransactionId(), + SellerUserID: item.GetSellerUserId(), + StockType: item.GetStockType(), + CoinAmount: item.GetCoinAmount(), + PaidCurrencyCode: item.GetPaidCurrencyCode(), + PaidAmountMicro: item.GetPaidAmountMicro(), + CountsAsSellerRecharge: item.GetCountsAsSellerRecharge(), + BalanceAfter: item.GetBalanceAfter(), + CreatedAtMS: item.GetCreatedAtMs(), + } +} diff --git a/server/admin/internal/modules/hostorg/routes.go b/server/admin/internal/modules/hostorg/routes.go index 93bb270a..db50c6aa 100644 --- a/server/admin/internal/modules/hostorg/routes.go +++ b/server/admin/internal/modules/hostorg/routes.go @@ -10,6 +10,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) { protected.GET("/admin/bd-leaders", middleware.RequirePermission("bd:view"), h.ListBDLeaders) protected.POST("/admin/bd-leaders", middleware.RequirePermission("bd:create"), h.CreateBDLeader) protected.PATCH("/admin/bd-leaders/:user_id/status", middleware.RequirePermission("bd:update"), h.SetBDStatus) + protected.DELETE("/admin/bd-leaders/:user_id", middleware.RequirePermission("bd:update"), h.DeleteBDLeader) protected.GET("/admin/bds", middleware.RequirePermission("bd:view"), h.ListBDs) protected.POST("/admin/bds", middleware.RequirePermission("bd:create"), h.CreateBD) protected.PATCH("/admin/bds/:user_id/status", middleware.RequirePermission("bd:update"), h.SetBDStatus) @@ -21,4 +22,5 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) { protected.GET("/admin/coin-sellers", middleware.RequirePermission("coin-seller:view"), h.ListCoinSellers) protected.POST("/admin/coin-sellers", middleware.RequirePermission("coin-seller:create"), h.CreateCoinSeller) protected.PATCH("/admin/coin-sellers/:user_id/status", middleware.RequirePermission("coin-seller:update"), h.SetCoinSellerStatus) + protected.POST("/admin/coin-sellers/:user_id/stock-credits", middleware.RequirePermission("coin-seller:stock-credit"), h.CreditCoinSellerStock) } diff --git a/server/admin/internal/modules/hostorg/service.go b/server/admin/internal/modules/hostorg/service.go index 3710d499..d33104f3 100644 --- a/server/admin/internal/modules/hostorg/service.go +++ b/server/admin/internal/modules/hostorg/service.go @@ -6,16 +6,28 @@ import ( "strconv" "strings" + "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/userclient" + "hyapp-admin-server/internal/integration/walletclient" + walletv1 "hyapp.local/api/proto/wallet/v1" +) + +const ( + coinSellerStatusActive = "active" + coinSellerMerchantAssetType = "COIN_SELLER_COIN" + coinSellerStockTypePurchase = "usdt_purchase" + coinSellerStockTypeCompensate = "coin_compensation" + paidCurrencyUSDT = "USDT" ) type Service struct { - userClient userclient.Client - reader *Reader + userClient userclient.Client + walletClient walletclient.Client + reader *Reader } -func NewService(userClient userclient.Client, reader *Reader) *Service { - return &Service{userClient: userClient, reader: reader} +func NewService(userClient userclient.Client, walletClient walletclient.Client, reader *Reader) *Service { + return &Service{userClient: userClient, walletClient: walletClient, reader: reader} } func (s *Service) ListBDLeaders(ctx context.Context, query listQuery) ([]*userclient.BDProfile, int64, error) { @@ -92,12 +104,19 @@ func (s *Service) SetBDStatus(ctx context.Context, actorID int64, targetUserID i }) } +func (s *Service) DeleteBDLeader(ctx context.Context, targetUserID int64) (*userclient.BDProfile, error) { + if targetUserID <= 0 { + return nil, fmt.Errorf("target_user_id is required") + } + return s.reader.DeleteBDLeader(ctx, targetUserID) +} + func (s *Service) CreateCoinSeller(ctx context.Context, actorID int64, requestID string, req createCoinSellerRequest) (*userclient.CoinSellerProfile, error) { targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID) if err != nil { return nil, err } - return s.userClient.CreateCoinSeller(ctx, userclient.CreateCoinSellerRequest{ + profile, err := s.userClient.CreateCoinSeller(ctx, userclient.CreateCoinSellerRequest{ RequestID: requestID, Caller: "hyapp-admin-server", CommandID: strings.TrimSpace(req.CommandID), @@ -105,10 +124,20 @@ func (s *Service) CreateCoinSeller(ctx context.Context, actorID int64, requestID TargetUserID: targetUserID, Reason: strings.TrimSpace(req.Reason), }) + if err != nil { + return nil, err + } + contact := firstNonBlank(req.Contact, req.Reason) + if contact != "" { + if err := s.reader.UpdateCoinSellerContact(ctx, targetUserID, contact); err != nil { + return nil, err + } + } + return profile, nil } func (s *Service) SetCoinSellerStatus(ctx context.Context, actorID int64, targetUserID int64, requestID string, req coinSellerStatusRequest) (*userclient.CoinSellerProfile, error) { - return s.userClient.SetCoinSellerStatus(ctx, userclient.SetCoinSellerStatusRequest{ + profile, err := s.userClient.SetCoinSellerStatus(ctx, userclient.SetCoinSellerStatusRequest{ RequestID: requestID, Caller: "hyapp-admin-server", CommandID: strings.TrimSpace(req.CommandID), @@ -117,6 +146,76 @@ func (s *Service) SetCoinSellerStatus(ctx context.Context, actorID int64, target Status: strings.TrimSpace(req.Status), Reason: strings.TrimSpace(req.Reason), }) + if err != nil { + return nil, err + } + if req.Contact != nil { + if err := s.reader.UpdateCoinSellerContact(ctx, targetUserID, *req.Contact); err != nil { + return nil, err + } + } + return profile, nil +} + +func (s *Service) CreditCoinSellerStock(ctx context.Context, actorID int64, sellerUserID int64, requestID string, req coinSellerStockCreditRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) { + if s.walletClient == nil { + return nil, fmt.Errorf("wallet client is not configured") + } + if sellerUserID <= 0 || actorID <= 0 { + return nil, fmt.Errorf("seller_user_id and operator_user_id are required") + } + commandID := strings.TrimSpace(req.CommandID) + stockType := strings.ToLower(strings.TrimSpace(req.Type)) + evidenceRef := strings.TrimSpace(req.EvidenceRef) + reason := strings.TrimSpace(req.Reason) + if commandID == "" || req.CoinAmount <= 0 { + return nil, fmt.Errorf("command_id and coin_amount are required") + } + + paidCurrencyCode := "" + paidAmountMicro := int64(0) + paymentRef := strings.TrimSpace(req.PaymentRef) + switch stockType { + case coinSellerStockTypePurchase: + paidCurrencyCode = paidCurrencyUSDT + parsed, err := parseUSDTAmountMicro(req.RechargeAmount) + if err != nil { + return nil, err + } + paidAmountMicro = parsed + case coinSellerStockTypeCompensate: + if strings.TrimSpace(req.RechargeAmount) != "" || paymentRef != "" { + return nil, fmt.Errorf("coin compensation must not include recharge_amount or payment_ref") + } + default: + return nil, fmt.Errorf("stock type is invalid") + } + + profile, err := s.userClient.GetCoinSellerProfile(ctx, userclient.GetCoinSellerProfileRequest{ + RequestID: requestID, + Caller: "hyapp-admin-server", + UserID: sellerUserID, + }) + 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") + } + + return s.walletClient.AdminCreditCoinSellerStock(ctx, &walletv1.AdminCreditCoinSellerStockRequest{ + CommandId: commandID, + SellerUserId: sellerUserID, + StockType: stockType, + CoinAmount: req.CoinAmount, + PaidCurrencyCode: paidCurrencyCode, + PaidAmountMicro: paidAmountMicro, + PaymentRef: paymentRef, + EvidenceRef: evidenceRef, + OperatorUserId: actorID, + Reason: reason, + AppCode: appctx.FromContext(ctx), + }) } func (s *Service) CreateAgency(ctx context.Context, actorID int64, requestID string, req createAgencyRequest) (*userclient.CreateAgencyResult, error) { @@ -191,6 +290,42 @@ func (s *Service) SetAgencyJoinEnabled(ctx context.Context, actorID int64, agenc }) } +func parseUSDTAmountMicro(raw string) (int64, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return 0, fmt.Errorf("recharge_amount is required for usdt purchase") + } + parts := strings.Split(raw, ".") + if len(parts) > 2 || parts[0] == "" { + return 0, fmt.Errorf("recharge_amount is invalid") + } + whole := parts[0] + frac := "" + if len(parts) == 2 { + frac = parts[1] + if frac == "" || len(frac) > 6 { + return 0, fmt.Errorf("recharge_amount supports up to 6 decimals") + } + } + digits := whole + frac + strings.Repeat("0", 6-len(frac)) + var amount int64 + const maxInt64 = int64(^uint64(0) >> 1) + for _, char := range digits { + if char < '0' || char > '9' { + return 0, fmt.Errorf("recharge_amount is invalid") + } + next := int64(char - '0') + if amount > (maxInt64-next)/10 { + return 0, fmt.Errorf("recharge_amount is too large") + } + amount = amount*10 + next + } + if amount <= 0 { + return 0, fmt.Errorf("recharge_amount must be positive") + } + return amount, nil +} + func normalizeListQuery(query listQuery) listQuery { if query.Page < 1 { query.Page = 1 @@ -205,3 +340,12 @@ func normalizeListQuery(query listQuery) listQuery { query.Status = strings.ToLower(strings.TrimSpace(query.Status)) return query } + +func firstNonBlank(values ...string) string { + for _, value := range values { + if trimmed := strings.TrimSpace(value); trimmed != "" { + return trimmed + } + } + return "" +} diff --git a/server/admin/internal/platform/logging/logging.go b/server/admin/internal/platform/logging/logging.go new file mode 100644 index 00000000..11a8d177 --- /dev/null +++ b/server/admin/internal/platform/logging/logging.go @@ -0,0 +1,200 @@ +// Package logging provides admin-server-local structured stdout logging. +// It intentionally lives under server/admin/internal/platform because admin is a separate Go module. +package logging + +import ( + "context" + "fmt" + "io" + "log" + "log/slog" + "net/http" + "os" + "strings" + "sync" + "time" + + "hyapp-admin-server/internal/appctx" + + "github.com/gin-gonic/gin" +) + +const ( + defaultLevel = "info" + defaultFormat = "json" + defaultMaxPayloadBytes = 2048 + redactedValue = "***REDACTED***" +) + +type Config struct { + Level string `yaml:"level"` + Format string `yaml:"format"` + IncludeRequestBody bool `yaml:"include_request_body"` + IncludeResponseBody bool `yaml:"include_response_body"` + MaxPayloadBytes int `yaml:"max_payload_bytes"` +} + +var ( + globalMu sync.RWMutex + globalLogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo, ReplaceAttr: replaceAttr})) +) + +func DefaultConfig() Config { + return Config{Level: defaultLevel, Format: defaultFormat, MaxPayloadBytes: defaultMaxPayloadBytes} +} + +func Init(service string, env string, nodeID string, cfg Config) error { + level, err := parseLevel(cfg.Level) + if err != nil { + return err + } + format := strings.ToLower(strings.TrimSpace(cfg.Format)) + if format == "" { + format = defaultFormat + } + + var handler slog.Handler + options := &slog.HandlerOptions{Level: level, ReplaceAttr: replaceAttr} + switch format { + case "json": + handler = slog.NewJSONHandler(os.Stdout, options) + case "text": + handler = slog.NewTextHandler(os.Stdout, options) + default: + return fmt.Errorf("unsupported log format %q", cfg.Format) + } + logger := slog.New(handler).With( + "env", nonEmpty(env, "local"), + "service", nonEmpty(service, "admin-server"), + "node_id", nonEmpty(nodeID, service), + ) + + globalMu.Lock() + globalLogger = logger + globalMu.Unlock() + slog.SetDefault(logger) + log.SetFlags(0) + log.SetOutput(stdWriter{}) + return nil +} + +func StdWriter() io.Writer { + return stdWriter{} +} + +func GinAccessLogger() gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.URL.Path == "/healthz" || c.Request.URL.Path == "/readyz" { + c.Next() + return + } + startedAt := time.Now() + c.Next() + + attrs := []slog.Attr{ + slog.String("request_id", requestID(c)), + slog.String("app_code", appctx.FromContext(c.Request.Context())), + slog.String("method", c.Request.Method), + slog.String("path", c.Request.URL.Path), + slog.Int("status", c.Writer.Status()), + slog.Int64("duration_ms", time.Since(startedAt).Milliseconds()), + slog.String("client_ip", c.ClientIP()), + } + if len(c.Errors) > 0 { + attrs = append(attrs, slog.String("error", c.Errors.String())) + } + switch { + case c.Writer.Status() >= http.StatusInternalServerError: + logAttrs(context.Background(), slog.LevelError, "http_access", attrs...) + case c.Writer.Status() >= http.StatusBadRequest: + logAttrs(context.Background(), slog.LevelWarn, "http_access", attrs...) + default: + logAttrs(context.Background(), slog.LevelInfo, "http_access", attrs...) + } + } +} + +type stdWriter struct{} + +func (stdWriter) Write(data []byte) (int, error) { + line := strings.TrimSpace(string(data)) + if line != "" { + logAttrs(context.Background(), slog.LevelInfo, "stdlog", slog.String("message", redactLine(line))) + } + return len(data), nil +} + +func logAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) { + globalMu.RLock() + logger := globalLogger + globalMu.RUnlock() + logger.LogAttrs(ctx, level, msg, attrs...) +} + +func requestID(c *gin.Context) string { + if value, ok := c.Get("requestID"); ok { + if requestID, ok := value.(string); ok { + return requestID + } + } + return c.GetHeader("X-Request-ID") +} + +func parseLevel(value string) (slog.Level, error) { + switch strings.ToLower(strings.TrimSpace(value)) { + case "debug": + return slog.LevelDebug, nil + case "", "info": + return slog.LevelInfo, nil + case "warn", "warning": + return slog.LevelWarn, nil + case "error": + return slog.LevelError, nil + default: + return slog.LevelInfo, fmt.Errorf("unsupported log level %q", value) + } +} + +func replaceAttr(_ []string, attr slog.Attr) slog.Attr { + if isSensitiveKey(attr.Key) { + return slog.String(attr.Key, redactedValue) + } + return attr +} + +func isSensitiveKey(key string) bool { + normalized := strings.ToLower(strings.ReplaceAll(key, "-", "_")) + for _, part := range []string{"password", "credential", "secret", "token", "user_sig", "usersig", "authorization", "signature", "private_key", "receipt"} { + if strings.Contains(normalized, part) { + return true + } + } + return false +} + +func redactLine(line string) string { + fields := strings.Fields(line) + changed := false + for index, field := range fields { + separator := strings.IndexAny(field, "=:") + if separator <= 0 { + continue + } + if isSensitiveKey(strings.Trim(field[:separator], "\"'")) { + fields[index] = field[:separator+1] + redactedValue + changed = true + } + } + if !changed { + return line + } + return strings.Join(fields, " ") +} + +func nonEmpty(value string, fallback string) string { + value = strings.TrimSpace(value) + if value == "" { + return fallback + } + return value +} diff --git a/server/admin/internal/repository/seed.go b/server/admin/internal/repository/seed.go index 3a372a6c..fac141bf 100644 --- a/server/admin/internal/repository/seed.go +++ b/server/admin/internal/repository/seed.go @@ -55,6 +55,7 @@ var defaultPermissions = []model.Permission{ {Name: "币商查看", Code: "coin-seller:view", Kind: "menu"}, {Name: "币商创建", Code: "coin-seller:create", Kind: "button"}, {Name: "币商更新", Code: "coin-seller:update", Kind: "button"}, + {Name: "币商进货", Code: "coin-seller:stock-credit", Kind: "button"}, {Name: "支付账单查看", Code: "payment-bill:view", Kind: "menu"}, {Name: "角色查看", Code: "role:view", Kind: "menu"}, {Name: "角色创建", Code: "role:create", Kind: "button"}, @@ -105,7 +106,7 @@ func (s *Store) seedMenus() error { paymentID := uint(0) menus := []model.Menu{ {Title: "总览", Code: "overview", Path: "/overview", Icon: "dashboard", PermissionCode: "overview:view", Sort: 10, Visible: true}, - {Title: "系统管理", Code: "system", Path: "", Icon: "settings", PermissionCode: "", Sort: 30, Visible: true}, + {Title: "后台设置", Code: "system", Path: "", Icon: "settings", PermissionCode: "", Sort: 30, Visible: true}, {Title: "日志审计", Code: "logs", Path: "", Icon: "history", PermissionCode: "", Sort: 40, Visible: true}, {Title: "通知中心", Code: "notifications", Path: "/notifications", Icon: "notifications", PermissionCode: "notification:view", Sort: 50, Visible: true}, {Title: "用户管理", Code: "app-users", Path: "", Icon: "users", PermissionCode: "", Sort: 60, Visible: true}, @@ -152,7 +153,7 @@ func (s *Store) seedMenus() error { } children := []model.Menu{ - {ParentID: &systemID, Title: "用户管理", Code: "system-users", Path: "/system/users", Icon: "users", PermissionCode: "user:view", Sort: 31, Visible: true}, + {ParentID: &systemID, Title: "后台用户", Code: "system-users", Path: "/system/users", Icon: "users", PermissionCode: "user:view", Sort: 31, Visible: true}, {ParentID: &systemID, Title: "角色配置", Code: "system-roles", Path: "/system/roles", Icon: "shield", PermissionCode: "role:view", Sort: 32, Visible: true}, {ParentID: &systemID, Title: "菜单权限", Code: "system-menus", Path: "/system/menus", Icon: "menu", PermissionCode: "menu:view", Sort: 33, Visible: true}, {ParentID: &logID, Title: "登录日志", Code: "logs-login", Path: "/logs/login", Icon: "login", PermissionCode: "log:view", Sort: 41, Visible: true}, @@ -409,7 +410,7 @@ func defaultRolePermissionCodes(code string) []string { "host:view", "agency:view", "agency:create", "agency:status", "bd:view", "bd:create", "bd:update", - "coin-seller:view", "coin-seller:create", "coin-seller:update", + "coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "payment-bill:view", "log:view", "notification:view", "notification:read", @@ -462,7 +463,7 @@ func defaultRolePermissionMigrationCodes(code string) []string { "upload:create", "country:view", "country:create", "country:update", "country:status", "region:view", "region:create", "region:update", "region:status", - "coin-seller:view", "coin-seller:create", "coin-seller:update", + "coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "payment-bill:view", } case "auditor", "readonly": diff --git a/server/admin/internal/router/router.go b/server/admin/internal/router/router.go index 17c1593e..a0a90962 100644 --- a/server/admin/internal/router/router.go +++ b/server/admin/internal/router/router.go @@ -22,6 +22,7 @@ import ( "hyapp-admin-server/internal/modules/roomadmin" "hyapp-admin-server/internal/modules/search" "hyapp-admin-server/internal/modules/upload" + "hyapp-admin-server/internal/platform/logging" "hyapp-admin-server/internal/service" "github.com/gin-gonic/gin" @@ -51,7 +52,7 @@ type Handlers struct { func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine { engine := gin.New() - engine.Use(middleware.RequestID(), middleware.AppCode(), gin.Logger(), gin.Recovery(), middleware.CORS(cfg)) + engine.Use(middleware.RequestID(), middleware.AppCode(), logging.GinAccessLogger(), gin.Recovery(), middleware.CORS(cfg)) engine.GET("/healthz", h.Health.Health) engine.GET("/readyz", h.Health.Ready) diff --git a/services/activity-service/cmd/server/main.go b/services/activity-service/cmd/server/main.go index 05a60cb2..8ccbc403 100644 --- a/services/activity-service/cmd/server/main.go +++ b/services/activity-service/cmd/server/main.go @@ -4,9 +4,11 @@ import ( "context" "flag" "log" + "os" "os/signal" "syscall" + "hyapp/pkg/logx" "hyapp/services/activity-service/internal/app" "hyapp/services/activity-service/internal/config" ) @@ -20,10 +22,13 @@ func main() { if err != nil { log.Fatal(err) } + if err := logx.Init(cfg.Log.WithRuntimeFields(cfg.ServiceName, cfg.Environment, cfg.NodeID)); err != nil { + log.Fatal(err) + } application, err := app.New(cfg) if err != nil { - log.Fatal(err) + fatalRuntime("app_new_failed", err) } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) @@ -39,7 +44,12 @@ func main() { application.Close() case err := <-errCh: if err != nil { - log.Fatal(err) + fatalRuntime("service_run_failed", err) } } } + +func fatalRuntime(msg string, err error) { + logx.Error(context.Background(), msg, err) + os.Exit(1) +} diff --git a/services/activity-service/configs/config.docker.yaml b/services/activity-service/configs/config.docker.yaml index d66a33c1..1f82dad4 100644 --- a/services/activity-service/configs/config.docker.yaml +++ b/services/activity-service/configs/config.docker.yaml @@ -1,5 +1,12 @@ service_name: activity-service node_id: activity-docker +environment: local +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 grpc_addr: ":13006" mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=Local" user_service_addr: "user-service:13005" diff --git a/services/activity-service/configs/config.tencent.example.yaml b/services/activity-service/configs/config.tencent.example.yaml index f2e6a837..4eac9b65 100644 --- a/services/activity-service/configs/config.tencent.example.yaml +++ b/services/activity-service/configs/config.tencent.example.yaml @@ -1,5 +1,12 @@ service_name: activity-service node_id: activity-tencent +environment: prod +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 grpc_addr: ":13006" mysql_dsn: "${TENCENT_MYSQL_ACTIVITY_DSN}" user_service_addr: "user-service.internal:13005" diff --git a/services/activity-service/configs/config.yaml b/services/activity-service/configs/config.yaml index feadc911..8cae64e2 100644 --- a/services/activity-service/configs/config.yaml +++ b/services/activity-service/configs/config.yaml @@ -1,5 +1,12 @@ service_name: activity-service node_id: activity-local +environment: local +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 grpc_addr: ":13006" mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=Local" user_service_addr: "127.0.0.1:13005" diff --git a/services/activity-service/internal/config/config.go b/services/activity-service/internal/config/config.go index 07bd00f1..c234265a 100644 --- a/services/activity-service/internal/config/config.go +++ b/services/activity-service/internal/config/config.go @@ -4,12 +4,14 @@ import ( "strings" "hyapp/pkg/configx" + "hyapp/pkg/logx" ) // Config 描述 activity-service 启动配置。 type Config struct { ServiceName string `yaml:"service_name"` NodeID string `yaml:"node_id"` + Environment string `yaml:"environment"` GRPCAddr string `yaml:"grpc_addr"` // MySQLDSN 是活动状态、事件消费幂等和 outbox 的必需存储连接串。 MySQLDSN string `yaml:"mysql_dsn"` @@ -19,6 +21,7 @@ type Config struct { MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"` Consumer ConsumerConfig `yaml:"consumer"` MessageInbox MessageInboxConfig `yaml:"message_inbox"` + Log logx.Config `yaml:"log"` } // ConsumerConfig 保存 room outbox 消费底座配置。 @@ -47,6 +50,7 @@ func Default() Config { return Config{ ServiceName: "activity-service", NodeID: "activity-local", + Environment: "local", GRPCAddr: ":13006", MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=Local", UserServiceAddr: "127.0.0.1:13005", @@ -65,6 +69,11 @@ func Default() Config { MaxRetry: 8, }, }, + Log: logx.Config{ + Level: "info", + Format: "json", + MaxPayloadBytes: 2048, + }, } } @@ -81,6 +90,18 @@ func Load(path string) (Config, error) { if strings.TrimSpace(cfg.UserServiceAddr) == "" { cfg.UserServiceAddr = Default().UserServiceAddr } + cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) + if cfg.ServiceName == "" { + cfg.ServiceName = "activity-service" + } + cfg.NodeID = strings.TrimSpace(cfg.NodeID) + if cfg.NodeID == "" { + cfg.NodeID = cfg.ServiceName + } + cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment)) + if cfg.Environment == "" { + cfg.Environment = "local" + } return cfg, nil } diff --git a/services/gateway-service/cmd/server/main.go b/services/gateway-service/cmd/server/main.go index ec736ca4..c15c5ec8 100644 --- a/services/gateway-service/cmd/server/main.go +++ b/services/gateway-service/cmd/server/main.go @@ -4,9 +4,11 @@ import ( "context" "flag" "log" + "os" "os/signal" "syscall" + "hyapp/pkg/logx" "hyapp/services/gateway-service/internal/app" "hyapp/services/gateway-service/internal/config" ) @@ -20,10 +22,13 @@ func main() { if err != nil { log.Fatal(err) } + if err := logx.Init(cfg.Log.WithRuntimeFields(cfg.ServiceName, cfg.Environment, cfg.NodeID)); err != nil { + log.Fatal(err) + } application, err := app.New(cfg) if err != nil { - log.Fatal(err) + fatalRuntime("app_new_failed", err) } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) @@ -37,11 +42,16 @@ func main() { select { case <-ctx.Done(): if err := application.Close(); err != nil { - log.Fatal(err) + fatalRuntime("app_close_failed", err) } case err := <-errCh: if err != nil { - log.Fatal(err) + fatalRuntime("service_run_failed", err) } } } + +func fatalRuntime(msg string, err error) { + logx.Error(context.Background(), msg, err) + os.Exit(1) +} diff --git a/services/gateway-service/configs/config.docker.yaml b/services/gateway-service/configs/config.docker.yaml index df14f3dd..31f7f07a 100644 --- a/services/gateway-service/configs/config.docker.yaml +++ b/services/gateway-service/configs/config.docker.yaml @@ -1,3 +1,12 @@ +service_name: gateway-service +node_id: gateway-docker +environment: local +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 http_addr: ":13000" jwt_secret: "dev-shared-secret" room_service_addr: "room-service:13001" diff --git a/services/gateway-service/configs/config.tencent.example.yaml b/services/gateway-service/configs/config.tencent.example.yaml index 4c55d97c..9c61bf76 100644 --- a/services/gateway-service/configs/config.tencent.example.yaml +++ b/services/gateway-service/configs/config.tencent.example.yaml @@ -1,3 +1,12 @@ +service_name: gateway-service +node_id: gateway-tencent +environment: prod +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 http_addr: ":13000" jwt_secret: "REPLACE_WITH_GATEWAY_JWT_SECRET" room_service_addr: "room-service.internal:13001" diff --git a/services/gateway-service/configs/config.yaml b/services/gateway-service/configs/config.yaml index 0fdc6f4e..52c33b5e 100644 --- a/services/gateway-service/configs/config.yaml +++ b/services/gateway-service/configs/config.yaml @@ -1,3 +1,12 @@ +service_name: gateway-service +node_id: gateway-local +environment: local +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 http_addr: ":13000" jwt_secret: "dev-shared-secret" room_service_addr: "127.0.0.1:13001" diff --git a/services/gateway-service/internal/config/config.go b/services/gateway-service/internal/config/config.go index c4fa16c6..456698e1 100644 --- a/services/gateway-service/internal/config/config.go +++ b/services/gateway-service/internal/config/config.go @@ -6,11 +6,15 @@ import ( "time" "hyapp/pkg/configx" + "hyapp/pkg/logx" "hyapp/pkg/tencentrtc" ) // Config 描述 gateway-service 启动所需的最小配置。 type Config struct { + ServiceName string `yaml:"service_name"` + NodeID string `yaml:"node_id"` + Environment string `yaml:"environment"` HTTPAddr string `yaml:"http_addr"` JWTSecret string `yaml:"jwt_secret"` RoomServiceAddr string `yaml:"room_service_addr"` @@ -22,6 +26,7 @@ type Config struct { TencentIM TencentIMConfig `yaml:"tencent_im"` TencentRTC TencentRTCConfig `yaml:"tencent_rtc"` TencentCOS TencentCOSConfig `yaml:"tencent-cos"` + Log logx.Config `yaml:"log"` } // AppConfigConfig 描述 gateway 读取后台 App 运行时配置的只读数据源。 @@ -110,6 +115,9 @@ type TencentCOSConfig struct { // Default 返回本地开发默认配置。 func Default() Config { return Config{ + ServiceName: "gateway-service", + NodeID: "gateway-local", + Environment: "local", HTTPAddr: ":13000", JWTSecret: "gateway-dev-secret", RoomServiceAddr: "127.0.0.1:13001", @@ -139,6 +147,11 @@ func Default() Config { RoomIDType: "string", AppScene: "voice_chat_room", }, + Log: logx.Config{ + Level: "info", + Format: "json", + MaxPayloadBytes: 2048, + }, } } @@ -166,6 +179,18 @@ func Load(path string) (Config, error) { // Normalize fills policy defaults and rejects unsupported RTC room/app modes at startup. func (cfg *Config) Normalize() error { + cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) + if cfg.ServiceName == "" { + cfg.ServiceName = "gateway-service" + } + cfg.NodeID = strings.TrimSpace(cfg.NodeID) + if cfg.NodeID == "" { + cfg.NodeID = cfg.ServiceName + } + cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment)) + if cfg.Environment == "" { + cfg.Environment = "local" + } cfg.RoomServiceAddr = strings.TrimSpace(cfg.RoomServiceAddr) cfg.UserServiceAddr = strings.TrimSpace(cfg.UserServiceAddr) cfg.WalletServiceAddr = strings.TrimSpace(cfg.WalletServiceAddr) diff --git a/services/gateway-service/internal/transport/http/response.go b/services/gateway-service/internal/transport/http/response.go index 815430f4..22b7e542 100644 --- a/services/gateway-service/internal/transport/http/response.go +++ b/services/gateway-service/internal/transport/http/response.go @@ -104,34 +104,8 @@ func writeRPCError(writer http.ResponseWriter, request *http.Request, err error) } func mapReasonToHTTP(reason xerr.Code) (int, string, string) { - switch reason { - case xerr.InvalidArgument, xerr.DisplayUserIDInvalid, xerr.InvalidInviteCode, xerr.InvalidSection, xerr.PageTokenInvalid: - return http.StatusBadRequest, string(reason), "invalid argument" - case xerr.AuthFailed: - return http.StatusUnauthorized, string(reason), "authentication failed" - case xerr.Unauthorized, xerr.SessionExpired, xerr.SessionRevoked: - return http.StatusUnauthorized, string(reason), "unauthorized" - case xerr.Conflict, xerr.PasswordAlreadySet, xerr.DisplayUserIDExists, xerr.RequestConflict, xerr.ProducerEventConflict: - return http.StatusConflict, string(reason), "conflict" - case xerr.RoomClosed: - return http.StatusConflict, string(reason), "room closed" - case xerr.DisplayUserIDCooldown, xerr.DisplayUserIDPrettyNotAvailable, xerr.DisplayUserIDPrettyActive, xerr.DisplayUserIDLeaseExpired, xerr.DisplayUserIDPaymentRequired, xerr.CountryChangeCooldown, xerr.RegionCountryConflict, xerr.RegionDisabled: - return http.StatusConflict, string(reason), "conflict" - case xerr.InsufficientBalance: - return http.StatusConflict, string(reason), "insufficient balance" - case xerr.ProfileRequired: - return http.StatusForbidden, codeProfileRequired, "profile required" - case xerr.UserDisabled, xerr.PermissionDenied: - return http.StatusForbidden, string(reason), "permission denied" - case xerr.NotFound, xerr.DisplayUserIDNotFound, xerr.CountryNotFound, xerr.RegionNotFound, xerr.MessageNotFound, xerr.MessageRecalled: - return http.StatusNotFound, string(reason), "not found" - case xerr.Unavailable: - return http.StatusBadGateway, codeUpstreamError, "upstream service error" - case xerr.Internal: - return http.StatusInternalServerError, codeInternalError, "internal error" - default: - return http.StatusBadGateway, codeUpstreamError, "upstream service error" - } + spec := xerr.SpecOf(reason) + return spec.HTTPStatus, spec.PublicCode, spec.PublicMessage } // writeEnvelope 是 gateway 业务 API 的唯一 JSON 响应出口。 diff --git a/services/gateway-service/internal/transport/http/rtc_handler.go b/services/gateway-service/internal/transport/http/rtc_handler.go index 45d2470c..03d3aa00 100644 --- a/services/gateway-service/internal/transport/http/rtc_handler.go +++ b/services/gateway-service/internal/transport/http/rtc_handler.go @@ -1,12 +1,13 @@ package http import ( - "log" + "log/slog" "net/http" "time" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/appcode" + "hyapp/pkg/logx" "hyapp/pkg/roomid" "hyapp/pkg/tencentrtc" "hyapp/pkg/xerr" @@ -34,6 +35,7 @@ func (h *Handler) issueTencentRTCToken(writer http.ResponseWriter, request *http writeError(writer, request, http.StatusUnauthorized, codeUnauthorized, "unauthorized") return } + logCtx := logx.With(request.Context(), slog.String("request_id", requestIDFromContext(request.Context())), slog.String("app_code", appcode.FromContext(request.Context()))) rtcConfig := tencentrtc.TokenConfig{ Enabled: h.tencentRTC.Enabled, @@ -44,7 +46,7 @@ func (h *Handler) issueTencentRTCToken(writer http.ResponseWriter, request *http AppScene: h.tencentRTC.AppScene, } if err := tencentrtc.ValidateConfig(rtcConfig); err != nil { - log.Printf("gateway_rtc_token result=config_error request_id=%s user_id=%d room_id=%s reason=%q", requestIDFromContext(request.Context()), userID, body.RoomID, err.Error()) + logx.Error(logCtx, "gateway_rtc_token_config_error", err, slog.Int64("user_id", userID), slog.String("room_id", body.RoomID)) writeError(writer, request, http.StatusInternalServerError, codeInternalError, "internal error") return } @@ -72,12 +74,18 @@ func (h *Handler) issueTencentRTCToken(writer http.ResponseWriter, request *http token, err := tencentrtc.GenerateToken(rtcConfig, userID, body.RoomID, timeNow()) if err != nil { - log.Printf("gateway_rtc_token result=config_error request_id=%s user_id=%d room_id=%s reason=%q", requestIDFromContext(request.Context()), userID, body.RoomID, err.Error()) + logx.Error(logCtx, "gateway_rtc_token_sign_failed", err, slog.Int64("user_id", userID), slog.String("room_id", body.RoomID)) writeError(writer, request, http.StatusInternalServerError, codeInternalError, "internal error") return } - log.Printf("gateway_rtc_token result=issued request_id=%s user_id=%d rtc_user_id=%s room_id=%s str_room_id=%s room_version=%d", requestIDFromContext(request.Context()), userID, token.RTCUserID, body.RoomID, token.StrRoomID, guardResp.GetRoomVersion()) + logx.Info(logCtx, "gateway_rtc_token_issued", + slog.Int64("user_id", userID), + slog.String("rtc_user_id", token.RTCUserID), + slog.String("room_id", body.RoomID), + slog.String("str_room_id", token.StrRoomID), + slog.Int64("room_version", guardResp.GetRoomVersion()), + ) writeOK(writer, request, token) } @@ -89,7 +97,8 @@ func (h *Handler) writeRTCPresenceDenied(writer http.ResponseWriter, request *ht roomVersion = guardResp.GetRoomVersion() } - log.Printf("gateway_rtc_token result=denied request_id=%s user_id=%d room_id=%s room_version=%d reason=%q", requestIDFromContext(request.Context()), userID, targetRoomID, roomVersion, reason) + logCtx := logx.With(request.Context(), slog.String("request_id", requestIDFromContext(request.Context())), slog.String("app_code", appcode.FromContext(request.Context()))) + logx.Warn(logCtx, "gateway_rtc_token_denied", slog.Int64("user_id", userID), slog.String("room_id", targetRoomID), slog.Int64("room_version", roomVersion), slog.String("reason", reason)) if reason == "room_not_found" { writeError(writer, request, http.StatusNotFound, codeNotFound, "not found") return diff --git a/services/gateway-service/internal/transport/http/tencent_rtc_callback.go b/services/gateway-service/internal/transport/http/tencent_rtc_callback.go index f1838812..4581344c 100644 --- a/services/gateway-service/internal/transport/http/tencent_rtc_callback.go +++ b/services/gateway-service/internal/transport/http/tencent_rtc_callback.go @@ -7,7 +7,7 @@ import ( "encoding/json" "fmt" "io" - "log" + "log/slog" "net/http" "strconv" "strings" @@ -15,6 +15,7 @@ import ( roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/appcode" + "hyapp/pkg/logx" "hyapp/pkg/tencentrtc" "hyapp/pkg/xerr" ) @@ -86,8 +87,16 @@ func (h *Handler) handleTencentRTCCallback(writer http.ResponseWriter, request * roomID, ok := parseTencentRTCRoomID(body.EventInfo.RoomID) userID, userOK := parseTencentIMUserID(body.EventInfo.UserID) eventTimeMS := tencentRTCEventTimeMS(body) + logCtx := logx.With(request.Context(), slog.String("request_id", requestIDFromContext(request.Context())), slog.String("app_code", tencentRTCCallbackAppCode(request))) if !ok || !userOK || eventTimeMS <= 0 || h.roomClient == nil { - log.Printf("gateway_rtc_callback result=ignored request_id=%s event_group=%d event_type=%d room_id=%q user_id=%q reason=input_invalid", requestIDFromContext(request.Context()), body.EventGroupID, body.EventType, roomID, body.EventInfo.UserID) + logx.Warn(logCtx, "gateway_rtc_callback_ignored", + slog.String("provider", "tencent"), + slog.Int("event_group", body.EventGroupID), + slog.Int("event_type", body.EventType), + slog.String("room_id", roomID), + slog.String("external_user_id", body.EventInfo.UserID), + slog.String("reason", "input_invalid"), + ) writeTencentRTCCallback(writer, http.StatusOK, 0, "") return } @@ -112,16 +121,37 @@ func (h *Handler) handleTencentRTCCallback(writer http.ResponseWriter, request * }) if err != nil { if tencentRTCCallbackCanAckBusinessError(err) { - log.Printf("gateway_rtc_callback result=business_ignored request_id=%s room_id=%s user_id=%d event_group=%d event_type=%d reason=%s", requestIDFromContext(request.Context()), roomID, userID, body.EventGroupID, body.EventType, xerr.ReasonFromGRPC(err)) + logx.Warn(logCtx, "gateway_rtc_callback_business_ignored", + slog.String("provider", "tencent"), + slog.String("room_id", roomID), + slog.Int64("user_id", userID), + slog.Int("event_group", body.EventGroupID), + slog.Int("event_type", body.EventType), + slog.String("reason", string(xerr.ReasonFromGRPC(err))), + ) writeTencentRTCCallback(writer, http.StatusOK, 0, "") return } - log.Printf("gateway_rtc_callback result=room_service_error request_id=%s room_id=%s user_id=%d event_group=%d event_type=%d error=%q", requestIDFromContext(request.Context()), roomID, userID, body.EventGroupID, body.EventType, err.Error()) + logx.Error(logCtx, "gateway_rtc_callback_room_service_error", err, + slog.String("provider", "tencent"), + slog.String("room_id", roomID), + slog.Int64("user_id", userID), + slog.Int("event_group", body.EventGroupID), + slog.Int("event_type", body.EventType), + ) writeTencentRTCCallback(writer, http.StatusInternalServerError, 1, "room service error") return } - log.Printf("gateway_rtc_callback result=accepted request_id=%s room_id=%s user_id=%d event_group=%d event_type=%d room_version=%d applied=%t", requestIDFromContext(request.Context()), roomID, userID, body.EventGroupID, body.EventType, resp.GetResult().GetRoomVersion(), resp.GetResult().GetApplied()) + logx.Info(logCtx, "gateway_rtc_callback_accepted", + slog.String("provider", "tencent"), + slog.String("room_id", roomID), + slog.Int64("user_id", userID), + slog.Int("event_group", body.EventGroupID), + slog.Int("event_type", body.EventType), + slog.Int64("room_version", resp.GetResult().GetRoomVersion()), + slog.Bool("applied", resp.GetResult().GetApplied()), + ) writeTencentRTCCallback(writer, http.StatusOK, 0, "") } diff --git a/services/room-service/cmd/server/main.go b/services/room-service/cmd/server/main.go index 30a3e82f..0ebaa2b6 100644 --- a/services/room-service/cmd/server/main.go +++ b/services/room-service/cmd/server/main.go @@ -5,9 +5,11 @@ import ( "context" "flag" "log" + "os" "os/signal" "syscall" + "hyapp/pkg/logx" "hyapp/services/room-service/internal/app" "hyapp/services/room-service/internal/config" ) @@ -23,11 +25,14 @@ func main() { if err != nil { log.Fatal(err) } + if err := logx.Init(cfg.Log.WithRuntimeFields(cfg.ServiceName, cfg.Environment, cfg.NodeID)); err != nil { + log.Fatal(err) + } // App.New 会建立 MySQL repository、Redis directory、wallet/im gRPC client 和 gRPC server。 application, err := app.New(cfg) if err != nil { - log.Fatal(err) + fatalRuntime("app_new_failed", err) } // SIGINT/SIGTERM 是正常下线入口,下线时要先进入 draining 再停止 gRPC。 @@ -47,7 +52,12 @@ func main() { case err := <-errCh: if err != nil { // gRPC listener 异常退出属于进程级失败,必须暴露给 supervisor。 - log.Fatal(err) + fatalRuntime("service_run_failed", err) } } } + +func fatalRuntime(msg string, err error) { + logx.Error(context.Background(), msg, err) + os.Exit(1) +} diff --git a/services/room-service/configs/config.docker.yaml b/services/room-service/configs/config.docker.yaml index 3acf087d..8caf172b 100644 --- a/services/room-service/configs/config.docker.yaml +++ b/services/room-service/configs/config.docker.yaml @@ -1,4 +1,12 @@ +service_name: room-service +environment: local node_id: "room-node-1" +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 grpc_addr: ":13001" lease_ttl: "10s" rank_limit: 20 diff --git a/services/room-service/configs/config.tencent.example.yaml b/services/room-service/configs/config.tencent.example.yaml index f42874f1..b6864c18 100644 --- a/services/room-service/configs/config.tencent.example.yaml +++ b/services/room-service/configs/config.tencent.example.yaml @@ -1,4 +1,12 @@ +service_name: room-service +environment: prod node_id: "room-node-prod-1" +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 grpc_addr: ":13001" lease_ttl: "10s" rank_limit: 20 diff --git a/services/room-service/configs/config.yaml b/services/room-service/configs/config.yaml index 6f961504..b8b6c923 100644 --- a/services/room-service/configs/config.yaml +++ b/services/room-service/configs/config.yaml @@ -1,4 +1,12 @@ +service_name: room-service +environment: local node_id: "room-node-1" +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 lease_ttl: "10s" rank_limit: 20 snapshot_every_n: 1 diff --git a/services/room-service/internal/config/config.go b/services/room-service/internal/config/config.go index 62297e06..915958ce 100644 --- a/services/room-service/internal/config/config.go +++ b/services/room-service/internal/config/config.go @@ -8,11 +8,16 @@ import ( "time" "hyapp/pkg/configx" + "hyapp/pkg/logx" "hyapp/pkg/tencentim" ) // Config 描述 room-service 进程启动所需的最小配置。 type Config struct { + // ServiceName 是日志和部署识别使用的稳定服务名。 + ServiceName string `yaml:"service_name"` + // Environment 是 local/dev/staging/prod 等运行环境标签。 + Environment string `yaml:"environment"` // NodeID 是当前 room-service 实例标识,Redis lease owner 使用它。 NodeID string `yaml:"node_id"` // GRPCAddr 是内部 gRPC 监听地址,gateway-service 通过它访问 room-service。 @@ -51,6 +56,8 @@ type Config struct { MicPublishScanInterval time.Duration `yaml:"mic_publish_scan_interval"` // OutboxWorker 控制 room_outbox 到腾讯云 IM 补偿投递 worker。 OutboxWorker OutboxWorkerConfig `yaml:"outbox_worker"` + // Log 控制 stdout 结构化日志格式、等级和 access body 策略。 + Log logx.Config `yaml:"log"` } // OutboxWorkerConfig 控制 room_outbox pending 事件的补偿投递循环。 @@ -110,6 +117,8 @@ func (c TencentIMConfig) RESTConfig() tencentim.RESTConfig { func Default() Config { // 默认端口遵守项目 13xxx 约束,MySQL/Redis 指向本地 Docker 暴露端口。 return Config{ + ServiceName: "room-service", + Environment: "local", NodeID: "room-node-local", GRPCAddr: ":13001", LeaseTTL: 10 * time.Second, @@ -135,6 +144,11 @@ func Default() Config { MicPublishTimeout: 15 * time.Second, MicPublishScanInterval: time.Second, OutboxWorker: defaultOutboxWorkerConfig(), + Log: logx.Config{ + Level: "info", + Format: "json", + MaxPayloadBytes: 2048, + }, } } @@ -152,6 +166,18 @@ func defaultOutboxWorkerConfig() OutboxWorkerConfig { // Normalize 补齐动态配置默认值,并拒绝会改变补偿语义的未知策略。 func Normalize(cfg Config) (Config, error) { // 配置归一化只处理需要跨环境保持语义的字段,其他默认值由 Default 提供。 + cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) + if cfg.ServiceName == "" { + cfg.ServiceName = "room-service" + } + cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment)) + if cfg.Environment == "" { + cfg.Environment = "local" + } + cfg.NodeID = strings.TrimSpace(cfg.NodeID) + if cfg.NodeID == "" { + cfg.NodeID = cfg.ServiceName + } outboxWorker, err := normalizeOutboxWorkerConfig(cfg.OutboxWorker) if err != nil { return Config{}, err diff --git a/services/room-service/internal/room/service/current_room.go b/services/room-service/internal/room/service/current_room.go index ff6b36d5..64e226b7 100644 --- a/services/room-service/internal/room/service/current_room.go +++ b/services/room-service/internal/room/service/current_room.go @@ -2,10 +2,11 @@ package service import ( "context" - "log" + "log/slog" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/appcode" + "hyapp/pkg/logx" "hyapp/pkg/xerr" "hyapp/services/room-service/internal/room/state" ) @@ -92,7 +93,11 @@ func (s *Service) projectRoomPresenceBestEffort(ctx context.Context, snapshot *r return } if err := s.repository.ProjectRoomPresence(ctx, projection); err != nil { - log.Printf("component=room_presence_projector room_id=%s version=%d status=project_failed error=%q", projection.RoomID, projection.RoomVersion, err.Error()) + logx.Error(ctx, "room_presence_project_failed", err, + slog.String("component", "room_presence_projector"), + slog.String("room_id", projection.RoomID), + slog.Int64("room_version", projection.RoomVersion), + ) } } diff --git a/services/room-service/internal/room/service/list.go b/services/room-service/internal/room/service/list.go index 9264f8cb..997ad6ea 100644 --- a/services/room-service/internal/room/service/list.go +++ b/services/room-service/internal/room/service/list.go @@ -4,11 +4,12 @@ import ( "context" "encoding/base64" "encoding/json" - "log" + "log/slog" "strings" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/appcode" + "hyapp/pkg/logx" "hyapp/pkg/xerr" ) @@ -102,11 +103,11 @@ func (s *Service) projectRoomListBestEffort(ctx context.Context, snapshot *roomv meta, exists, err := s.repository.GetRoomMeta(ctx, snapshot.GetRoomId()) if err != nil { // 投影不能回滚已提交 Room Cell 命令,先记录日志;后续可用 projection task 补偿。 - log.Printf("component=room_list_projector room_id=%s status=load_meta_failed error=%q", snapshot.GetRoomId(), err.Error()) + logx.Error(ctx, "room_list_load_meta_failed", err, slog.String("component", "room_list_projector"), slog.String("room_id", snapshot.GetRoomId())) return } if !exists { - log.Printf("component=room_list_projector room_id=%s status=meta_missing", snapshot.GetRoomId()) + logx.Warn(ctx, "room_list_meta_missing", slog.String("component", "room_list_projector"), slog.String("room_id", snapshot.GetRoomId())) return } @@ -114,7 +115,7 @@ func (s *Service) projectRoomListBestEffort(ctx context.Context, snapshot *roomv entry := roomListEntryFromSnapshot(snapshot, meta.VisibleRegionID, nowMS) if err := s.repository.UpsertRoomListEntry(ctx, entry); err != nil { // 列表读模型是最终一致;失败不改变命令成功语义。 - log.Printf("component=room_list_projector room_id=%s region_id=%d status=upsert_failed error=%q", snapshot.GetRoomId(), meta.VisibleRegionID, err.Error()) + logx.Error(ctx, "room_list_upsert_failed", err, slog.String("component", "room_list_projector"), slog.String("room_id", snapshot.GetRoomId()), slog.Int64("region_id", meta.VisibleRegionID)) } } diff --git a/services/room-service/internal/room/service/outbox_worker.go b/services/room-service/internal/room/service/outbox_worker.go index e777840a..f072a66f 100644 --- a/services/room-service/internal/room/service/outbox_worker.go +++ b/services/room-service/internal/room/service/outbox_worker.go @@ -3,11 +3,12 @@ package service import ( "context" "fmt" - "log" + "log/slog" "strings" "time" "hyapp/pkg/appcode" + "hyapp/pkg/logx" ) const ( @@ -68,12 +69,12 @@ func (s *Service) RunOutboxWorker(ctx context.Context, options OutboxWorkerOptio options = normalizeOutboxWorkerOptions(options) if options.RetryStrategy != outboxRetryFixedInterval { // 配置层会拒绝未知策略;这里兜底避免测试或手写构造静默改变语义。 - log.Printf("worker=%s node_id=%s status=stopped error=%q", outboxWorkerName, s.nodeID, "unsupported retry_strategy") + logx.Error(ctx, "worker_stopped", fmt.Errorf("unsupported retry_strategy"), slog.String("worker", outboxWorkerName), slog.String("node_id", s.nodeID)) return } if err := s.ProcessPendingOutbox(ctx, options); err != nil && ctx.Err() == nil { - log.Printf("worker=%s node_id=%s status=batch_failed error=%q", outboxWorkerName, s.nodeID, trimOutboxError(err.Error())) + logx.Error(ctx, "worker_batch_failed", err, slog.String("worker", outboxWorkerName), slog.String("node_id", s.nodeID), slog.Int("batch_size", options.BatchSize)) } ticker := time.NewTicker(options.PollInterval) @@ -85,7 +86,7 @@ func (s *Service) RunOutboxWorker(ctx context.Context, options OutboxWorkerOptio return case <-ticker.C: if err := s.ProcessPendingOutbox(ctx, options); err != nil && ctx.Err() == nil { - log.Printf("worker=%s node_id=%s status=batch_failed error=%q", outboxWorkerName, s.nodeID, trimOutboxError(err.Error())) + logx.Error(ctx, "worker_batch_failed", err, slog.String("worker", outboxWorkerName), slog.String("node_id", s.nodeID), slog.Int("batch_size", options.BatchSize)) } } } @@ -123,7 +124,15 @@ func (s *Service) ProcessPendingOutbox(ctx context.Context, options OutboxWorker return markErr } - log.Printf("worker=%s node_id=%s event_id=%s event_type=%s room_id=%s retry_count=%d status=retryable error=%q", outboxWorkerName, s.nodeID, record.EventID, record.EventType, record.RoomID, record.RetryCount+1, trimOutboxError(err.Error())) + logx.Warn(ctx, "outbox_publish_retryable", + slog.String("worker", outboxWorkerName), + slog.String("node_id", s.nodeID), + slog.String("event_id", record.EventID), + slog.String("event_type", record.EventType), + slog.String("room_id", record.RoomID), + slog.Int("retry_count", record.RetryCount+1), + slog.String("error", trimOutboxError(err.Error())), + ) continue } @@ -135,7 +144,14 @@ func (s *Service) ProcessPendingOutbox(ctx context.Context, options OutboxWorker return markErr } - log.Printf("worker=%s node_id=%s event_id=%s event_type=%s room_id=%s retry_count=%d status=delivered", outboxWorkerName, s.nodeID, record.EventID, record.EventType, record.RoomID, record.RetryCount) + logx.Info(ctx, "outbox_delivered", + slog.String("worker", outboxWorkerName), + slog.String("node_id", s.nodeID), + slog.String("event_id", record.EventID), + slog.String("event_type", record.EventType), + slog.String("room_id", record.RoomID), + slog.Int("retry_count", record.RetryCount), + ) } return nil diff --git a/services/room-service/internal/room/service/pipeline.go b/services/room-service/internal/room/service/pipeline.go index b3f95949..579f2632 100644 --- a/services/room-service/internal/room/service/pipeline.go +++ b/services/room-service/internal/room/service/pipeline.go @@ -3,10 +3,11 @@ package service import ( "bytes" "context" - "log" + "log/slog" "time" "hyapp/pkg/appcode" + "hyapp/pkg/logx" "hyapp/pkg/xerr" "hyapp/services/room-service/internal/room/command" "hyapp/services/room-service/internal/room/outbox" @@ -145,7 +146,12 @@ func (s *Service) mutateRoom(ctx context.Context, cmd command.Command, replayabl markErr := s.repository.MarkOutboxDelivered(markCtx, result.syncEvent.EventID) cancel() if markErr != nil { - log.Printf("worker=room_outbox node_id=%s event_id=%s room_id=%s status=mark_delivered_failed error=%q", s.nodeID, result.syncEvent.EventID, result.syncEvent.RoomID, trimOutboxError(markErr.Error())) + logx.Error(ctx, "outbox_mark_delivered_failed", markErr, + slog.String("worker", outboxWorkerName), + slog.String("node_id", s.nodeID), + slog.String("event_id", result.syncEvent.EventID), + slog.String("room_id", result.syncEvent.RoomID), + ) } } } diff --git a/services/user-service/cmd/server/main.go b/services/user-service/cmd/server/main.go index 77a0db43..d74677d7 100644 --- a/services/user-service/cmd/server/main.go +++ b/services/user-service/cmd/server/main.go @@ -5,9 +5,11 @@ import ( "context" "flag" "log" + "os" "os/signal" "syscall" + "hyapp/pkg/logx" "hyapp/services/user-service/internal/app" "hyapp/services/user-service/internal/config" ) @@ -23,11 +25,14 @@ func main() { if err != nil { log.Fatal(err) } + if err := logx.Init(cfg.Log.WithRuntimeFields(cfg.ServiceName, cfg.Environment, cfg.NodeID)); err != nil { + log.Fatal(err) + } // App.New 会建立 MySQL repository、创建 auth/user service,并注册 gRPC 服务。 application, err := app.New(cfg) if err != nil { - log.Fatal(err) + fatalRuntime("app_new_failed", err) } // SIGINT/SIGTERM 是正常下线入口,Close 内部会进入 draining 并 GracefulStop。 @@ -47,7 +52,12 @@ func main() { case err := <-errCh: if err != nil { // listener 异常退出属于进程级失败,必须暴露给 supervisor。 - log.Fatal(err) + fatalRuntime("service_run_failed", err) } } } + +func fatalRuntime(msg string, err error) { + logx.Error(context.Background(), msg, err) + os.Exit(1) +} diff --git a/services/user-service/configs/config.docker.yaml b/services/user-service/configs/config.docker.yaml index b9d5aca6..b56e3bfb 100644 --- a/services/user-service/configs/config.docker.yaml +++ b/services/user-service/configs/config.docker.yaml @@ -1,5 +1,12 @@ service_name: user-service node_id: user-docker +environment: local +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 grpc_addr: ":13005" mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=Local" mysql_auto_migrate: false @@ -30,6 +37,11 @@ mic_time_worker: room_mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_room?parseTime=true&charset=utf8mb4&loc=Local" poll_interval_sec: 5 batch_size: 100 + compensation_enabled: true + compensation_interval_sec: 60 + compensation_batch_size: 100 + pending_publish_max_age_sec: 120 + publishing_session_max_age_sec: 43200 jwt: issuer: "hyapp" access_token_ttl_sec: 1800 diff --git a/services/user-service/configs/config.tencent.example.yaml b/services/user-service/configs/config.tencent.example.yaml index f02447da..e2ca3627 100644 --- a/services/user-service/configs/config.tencent.example.yaml +++ b/services/user-service/configs/config.tencent.example.yaml @@ -1,5 +1,12 @@ service_name: user-service node_id: user-tencent +environment: prod +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 grpc_addr: ":13005" mysql_dsn: "${TENCENT_MYSQL_USER_DSN}" mysql_auto_migrate: false @@ -30,6 +37,11 @@ mic_time_worker: room_mysql_dsn: "${TENCENT_MYSQL_ROOM_DSN}" poll_interval_sec: 5 batch_size: 100 + compensation_enabled: true + compensation_interval_sec: 60 + compensation_batch_size: 100 + pending_publish_max_age_sec: 120 + publishing_session_max_age_sec: 43200 jwt: issuer: "hyapp" access_token_ttl_sec: 1800 diff --git a/services/user-service/configs/config.yaml b/services/user-service/configs/config.yaml index cc3d2fc3..a82d26c1 100644 --- a/services/user-service/configs/config.yaml +++ b/services/user-service/configs/config.yaml @@ -1,5 +1,12 @@ service_name: user-service node_id: user-local +environment: local +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 grpc_addr: ":13005" mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=Local" mysql_auto_migrate: false @@ -30,6 +37,11 @@ mic_time_worker: room_mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_room?parseTime=true&charset=utf8mb4&loc=Local" poll_interval_sec: 5 batch_size: 100 + compensation_enabled: true + compensation_interval_sec: 60 + compensation_batch_size: 100 + pending_publish_max_age_sec: 120 + publishing_session_max_age_sec: 43200 jwt: issuer: "hyapp" access_token_ttl_sec: 1800 diff --git a/services/user-service/deploy/mysql/initdb/001_user_service.sql b/services/user-service/deploy/mysql/initdb/001_user_service.sql index 5943de0f..fd3b06c9 100644 --- a/services/user-service/deploy/mysql/initdb/001_user_service.sql +++ b/services/user-service/deploy/mysql/initdb/001_user_service.sql @@ -415,6 +415,7 @@ CREATE TABLE IF NOT EXISTS coin_seller_profiles ( user_id BIGINT NOT NULL PRIMARY KEY, status VARCHAR(32) NOT NULL, merchant_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN_SELLER_COIN', + contact_info VARCHAR(128) NOT NULL DEFAULT '', created_by_user_id BIGINT NOT NULL, created_at_ms BIGINT NOT NULL, updated_at_ms BIGINT NOT NULL, diff --git a/services/user-service/internal/app/app.go b/services/user-service/internal/app/app.go index 3f8420f8..1b93049e 100644 --- a/services/user-service/internal/app/app.go +++ b/services/user-service/internal/app/app.go @@ -249,6 +249,19 @@ func (a *App) Run() error { }) }() } + if a.cfg.MicTimeWorker.CompensationEnabled && a.micTimeSvc != nil { + a.workerWG.Add(1) + go func() { + defer a.workerWG.Done() + a.micTimeSvc.RunOpenSessionCompensationWorker(a.workerCtx, mictimeservice.CompensationWorkerOptions{ + WorkerID: a.cfg.NodeID, + PollInterval: time.Duration(a.cfg.MicTimeWorker.CompensationIntervalSec) * time.Second, + BatchSize: a.cfg.MicTimeWorker.CompensationBatchSize, + PendingPublishMaxAge: time.Duration(a.cfg.MicTimeWorker.PendingPublishMaxAgeSec) * time.Second, + PublishingSessionMaxAge: time.Duration(a.cfg.MicTimeWorker.PublishingSessionMaxAgeSec) * time.Second, + }) + }() + } err := a.server.Serve(a.listener) a.health.MarkStopped() if errors.Is(err, grpc.ErrServerStopped) { diff --git a/services/user-service/internal/config/config.go b/services/user-service/internal/config/config.go index a79ef90a..72b2903f 100644 --- a/services/user-service/internal/config/config.go +++ b/services/user-service/internal/config/config.go @@ -1,7 +1,12 @@ // Package config 定义 user-service 的启动配置和 YAML 加载入口。 package config -import "hyapp/pkg/configx" +import ( + "strings" + + "hyapp/pkg/configx" + "hyapp/pkg/logx" +) // Config 描述 user-service 启动配置。 type Config struct { @@ -9,6 +14,8 @@ type Config struct { ServiceName string `yaml:"service_name"` // NodeID 是当前服务节点标识,预留给日志和后续健康响应使用。 NodeID string `yaml:"node_id"` + // Environment 是日志和部署策略使用的环境标签。 + Environment string `yaml:"environment"` // GRPCAddr 是 user-service 内部 gRPC 监听地址。 GRPCAddr string `yaml:"grpc_addr"` // MySQLDSN 是用户主数据、认证身份、session 和短号事务的必需存储连接串。 @@ -29,6 +36,8 @@ type Config struct { MicTimeWorker MicTimeWorkerConfig `yaml:"mic_time_worker"` // JWT 控制 access token 和 refresh session 的签发参数。 JWT JWTConfig `yaml:"jwt"` + // Log 控制 stdout 结构化日志格式、等级和 access body 策略。 + Log logx.Config `yaml:"log"` } // IDGeneratorConfig 保存 user_id 发号节点配置。 @@ -97,6 +106,16 @@ type MicTimeWorkerConfig struct { PollIntervalSec int64 `yaml:"poll_interval_sec"` // BatchSize 是单轮最多消费的 RoomMicChanged 事件数。 BatchSize int `yaml:"batch_size"` + // CompensationEnabled 控制是否启动异常 open session 补偿。 + CompensationEnabled bool `yaml:"compensation_enabled"` + // CompensationIntervalSec 是补偿扫描间隔秒数。 + CompensationIntervalSec int64 `yaml:"compensation_interval_sec"` + // CompensationBatchSize 是单轮最多关闭的异常 session 数。 + CompensationBatchSize int `yaml:"compensation_batch_size"` + // PendingPublishMaxAgeSec 是 pending_publish 缺失 down 事件时的兜底关闭窗口。 + PendingPublishMaxAgeSec int64 `yaml:"pending_publish_max_age_sec"` + // PublishingSessionMaxAgeSec 是 publishing 缺失 down 事件时允许累计的最大窗口。 + PublishingSessionMaxAgeSec int64 `yaml:"publishing_session_max_age_sec"` } // JWTConfig 保存 access token 和 refresh token 的签发策略。 @@ -119,6 +138,7 @@ func Default() Config { return Config{ ServiceName: "user-service", NodeID: "user-local", + Environment: "local", GRPCAddr: ":13005", MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_user?parseTime=true&charset=utf8mb4&loc=Local", MySQLAutoMigrate: false, @@ -148,10 +168,15 @@ func Default() Config { BatchSize: 100, }, MicTimeWorker: MicTimeWorkerConfig{ - Enabled: true, - RoomMySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_room?parseTime=true&charset=utf8mb4&loc=Local", - PollIntervalSec: 5, - BatchSize: 100, + Enabled: true, + RoomMySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_room?parseTime=true&charset=utf8mb4&loc=Local", + PollIntervalSec: 5, + BatchSize: 100, + CompensationEnabled: true, + CompensationIntervalSec: 60, + CompensationBatchSize: 100, + PendingPublishMaxAgeSec: 120, + PublishingSessionMaxAgeSec: 43200, }, JWT: JWTConfig{ Issuer: "hyapp", @@ -160,6 +185,11 @@ func Default() Config { SigningAlg: "HS256", SigningSecret: "dev-shared-secret", }, + Log: logx.Config{ + Level: "info", + Format: "json", + MaxPayloadBytes: 2048, + }, } } @@ -176,5 +206,18 @@ func Load(path string) (Config, error) { return Config{}, err } + cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) + if cfg.ServiceName == "" { + cfg.ServiceName = "user-service" + } + cfg.NodeID = strings.TrimSpace(cfg.NodeID) + if cfg.NodeID == "" { + cfg.NodeID = cfg.ServiceName + } + cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment)) + if cfg.Environment == "" { + cfg.Environment = "local" + } + return cfg, nil } diff --git a/services/user-service/internal/domain/mictime/mictime.go b/services/user-service/internal/domain/mictime/mictime.go index d0051404..ed507b2c 100644 --- a/services/user-service/internal/domain/mictime/mictime.go +++ b/services/user-service/internal/domain/mictime/mictime.go @@ -27,6 +27,18 @@ const ( SessionStatusPublishing = "publishing" // SessionStatusEnded is terminal and must never be reopened by replayed events. SessionStatusEnded = "ended" + + // EventTypeUserMicSessionClosed is emitted after a session is closed and aggregates are durable. + EventTypeUserMicSessionClosed = "UserMicSessionClosed" + // EventTypeUserMicDailyStatsUpdated is emitted for each UTC day touched by a closed session. + EventTypeUserMicDailyStatsUpdated = "UserMicDailyStatsUpdated" + // EventTypeUserMicLifetimeStatsUpdated is emitted when lifetime totals receive one session delta. + EventTypeUserMicLifetimeStatsUpdated = "UserMicLifetimeStatsUpdated" + + // CompensationReasonPendingPublishTimeout closes pending sessions whose expected room-service timeout event never arrived. + CompensationReasonPendingPublishTimeout = "compensated_publish_timeout" + // CompensationReasonPublishingMaxWindow closes publishing sessions at a configured safety ceiling. + CompensationReasonPublishingMaxWindow = "compensated_publishing_max_window" ) // RoomMicEvent is the decoded RoomMicChanged fact used by user-service duration aggregation. @@ -64,6 +76,21 @@ type ApplyResult struct { Reason string } +// CompensationOptions bounds abnormal open sessions so missing down events cannot accumulate forever. +type CompensationOptions struct { + NowMs int64 + BatchSize int + PendingPublishMaxAgeMs int64 + PublishingSessionMaxAgeMs int64 +} + +// CompensationResult summarizes one compensation scan. +type CompensationResult struct { + ClosedCount int + PendingPublishClosed int + PublishingSessionClosed int +} + // LifetimeStats is the user-wide aggregate consumed by host tasks and profile views. type LifetimeStats struct { AppCode string diff --git a/services/user-service/internal/service/auth/third_party.go b/services/user-service/internal/service/auth/third_party.go index 3a518f69..aba1b8c4 100644 --- a/services/user-service/internal/service/auth/third_party.go +++ b/services/user-service/internal/service/auth/third_party.go @@ -2,13 +2,14 @@ package auth import ( "context" - "log" + "log/slog" "strings" "time" _ "time/tzdata" "unicode/utf8" "hyapp/pkg/appcode" + "hyapp/pkg/logx" "hyapp/pkg/xerr" authdomain "hyapp/services/user-service/internal/domain/auth" invitedomain "hyapp/services/user-service/internal/domain/invite" @@ -74,7 +75,7 @@ func (s *Service) LoginThirdParty(ctx context.Context, provider string, credenti } if !xerr.IsCode(err, xerr.NotFound) { // 非 not found 的存储错误不能退化为注册。 - log.Printf("component=user_auth op=third_party_lookup request_id=%s provider=%s status=failed error=%q", meta.RequestID, provider, err.Error()) + logx.Error(logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", meta.AppCode)), "third_party_lookup_failed", err, slog.String("component", "user_auth"), slog.String("provider", provider)) s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err))) return authdomain.Token{}, false, err } @@ -178,7 +179,7 @@ func (s *Service) createThirdPartyUser(ctx context.Context, provider string, pro } s.audit(ctx, meta, 0, loginThird, provider, resultFailed, string(xerr.CodeOf(err))) - log.Printf("component=user_auth op=third_party_register request_id=%s provider=%s status=failed error=%q", meta.RequestID, provider, err.Error()) + logx.Error(logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", meta.AppCode)), "third_party_register_failed", err, slog.String("component", "user_auth"), slog.String("provider", provider)) return authdomain.Token{}, false, err } diff --git a/services/user-service/internal/service/invite/service.go b/services/user-service/internal/service/invite/service.go index cf787f8d..914778f7 100644 --- a/services/user-service/internal/service/invite/service.go +++ b/services/user-service/internal/service/invite/service.go @@ -3,9 +3,10 @@ package invite import ( "context" - "log" + "log/slog" "time" + "hyapp/pkg/logx" invitedomain "hyapp/services/user-service/internal/domain/invite" ) @@ -40,7 +41,7 @@ func New(repository Repository, reader WalletOutboxReader) *Service { // RunWalletRechargeWorker consumes WalletRechargeRecorded facts and advances valid_invite_count. func (s *Service) RunWalletRechargeWorker(ctx context.Context, options WorkerOptions) { if s == nil || s.repository == nil || s.reader == nil { - log.Printf("component=user_invite op=wallet_recharge_worker status=disabled reason=missing_dependency") + logx.Warn(ctx, "worker_disabled", slog.String("worker", "wallet_recharge"), slog.String("component", "user_invite"), slog.String("reason", "missing_dependency")) return } if options.PollInterval <= 0 { @@ -54,7 +55,7 @@ func (s *Service) RunWalletRechargeWorker(ctx context.Context, options WorkerOpt for { processed, err := s.processBatch(ctx, &cursor, options.BatchSize) if err != nil { - log.Printf("component=user_invite op=wallet_recharge_worker worker_id=%s status=failed error=%q", options.WorkerID, err.Error()) + logx.Error(ctx, "worker_batch_failed", err, slog.String("worker", "wallet_recharge"), slog.String("component", "user_invite"), slog.String("worker_id", options.WorkerID), slog.Int("batch_size", options.BatchSize)) } if ctx.Err() != nil { return diff --git a/services/user-service/internal/service/mictime/service.go b/services/user-service/internal/service/mictime/service.go index 7efd858e..d3a5c888 100644 --- a/services/user-service/internal/service/mictime/service.go +++ b/services/user-service/internal/service/mictime/service.go @@ -3,9 +3,10 @@ package mictime import ( "context" - "log" + "log/slog" "time" + "hyapp/pkg/logx" "hyapp/pkg/xerr" mictimedomain "hyapp/services/user-service/internal/domain/mictime" ) @@ -13,6 +14,7 @@ import ( // Repository applies room mic events and returns user duration aggregates. type Repository interface { ProcessRoomMicEvent(ctx context.Context, event mictimedomain.RoomMicEvent) (mictimedomain.ApplyResult, error) + CompensateOpenSessions(ctx context.Context, options mictimedomain.CompensationOptions) (mictimedomain.CompensationResult, error) GetLifetimeStats(ctx context.Context, appCode string, userID int64) (mictimedomain.LifetimeStats, error) } @@ -34,6 +36,15 @@ type WorkerOptions struct { BatchSize int } +// CompensationWorkerOptions controls abnormal open session closure. +type CompensationWorkerOptions struct { + WorkerID string + PollInterval time.Duration + BatchSize int + PendingPublishMaxAge time.Duration + PublishingSessionMaxAge time.Duration +} + // New creates a mic time service with persistence and optional room outbox reader. func New(repository Repository, reader RoomOutboxReader) *Service { return &Service{repository: repository, reader: reader} @@ -50,7 +61,7 @@ func (s *Service) GetLifetimeStats(ctx context.Context, appCode string, userID i // RunRoomMicWorker consumes RoomMicChanged facts from room-service outbox. func (s *Service) RunRoomMicWorker(ctx context.Context, options WorkerOptions) { if s == nil || s.repository == nil || s.reader == nil { - log.Printf("component=user_mic_time op=room_mic_worker status=disabled reason=missing_dependency") + logx.Warn(ctx, "worker_disabled", slog.String("worker", "room_mic"), slog.String("component", "user_mic_time"), slog.String("reason", "missing_dependency")) return } if options.PollInterval <= 0 { @@ -64,7 +75,7 @@ func (s *Service) RunRoomMicWorker(ctx context.Context, options WorkerOptions) { for { processed, err := s.processBatch(ctx, &cursor, options.BatchSize) if err != nil { - log.Printf("component=user_mic_time op=room_mic_worker worker_id=%s status=failed error=%q", options.WorkerID, err.Error()) + logx.Error(ctx, "worker_batch_failed", err, slog.String("worker", "room_mic"), slog.String("component", "user_mic_time"), slog.String("worker_id", options.WorkerID), slog.Int("batch_size", options.BatchSize)) } if ctx.Err() != nil { return @@ -81,6 +92,45 @@ func (s *Service) RunRoomMicWorker(ctx context.Context, options WorkerOptions) { } } +// RunOpenSessionCompensationWorker closes stale open sessions when room-service down events are missing. +func (s *Service) RunOpenSessionCompensationWorker(ctx context.Context, options CompensationWorkerOptions) { + if s == nil || s.repository == nil { + logx.Warn(ctx, "worker_disabled", slog.String("worker", "open_session_compensation"), slog.String("component", "user_mic_time"), slog.String("reason", "missing_dependency")) + return + } + if options.PollInterval <= 0 { + options.PollInterval = time.Minute + } + if options.BatchSize <= 0 { + options.BatchSize = 100 + } + if options.PendingPublishMaxAge <= 0 { + options.PendingPublishMaxAge = 2 * time.Minute + } + if options.PublishingSessionMaxAge <= 0 { + options.PublishingSessionMaxAge = 12 * time.Hour + } + + for { + closed, err := s.compensateBatch(ctx, options) + if err != nil { + logx.Error(ctx, "worker_batch_failed", err, slog.String("worker", "open_session_compensation"), slog.String("component", "user_mic_time"), slog.String("worker_id", options.WorkerID), slog.Int("batch_size", options.BatchSize)) + } + if ctx.Err() != nil { + return + } + if closed >= options.BatchSize { + // A full batch means backlog exists; continue immediately to keep abnormal sessions bounded. + continue + } + select { + case <-ctx.Done(): + return + case <-time.After(options.PollInterval): + } + } +} + func (s *Service) processBatch(ctx context.Context, cursor *mictimedomain.RoomOutboxCursor, batchSize int) (int, error) { events, err := s.reader.ListRoomMicEventsAfter(ctx, *cursor, batchSize) if err != nil { @@ -95,3 +145,16 @@ func (s *Service) processBatch(ctx context.Context, cursor *mictimedomain.RoomOu } return len(events), nil } + +func (s *Service) compensateBatch(ctx context.Context, options CompensationWorkerOptions) (int, error) { + result, err := s.repository.CompensateOpenSessions(ctx, mictimedomain.CompensationOptions{ + NowMs: time.Now().UnixMilli(), + BatchSize: options.BatchSize, + PendingPublishMaxAgeMs: options.PendingPublishMaxAge.Milliseconds(), + PublishingSessionMaxAgeMs: options.PublishingSessionMaxAge.Milliseconds(), + }) + if err != nil { + return 0, err + } + return result.ClosedCount, nil +} diff --git a/services/user-service/internal/service/user/region_rebuild_worker.go b/services/user-service/internal/service/user/region_rebuild_worker.go index 4eecef33..10e2aee2 100644 --- a/services/user-service/internal/service/user/region_rebuild_worker.go +++ b/services/user-service/internal/service/user/region_rebuild_worker.go @@ -2,10 +2,11 @@ package user import ( "context" - "log" + "log/slog" "strings" "time" + "hyapp/pkg/logx" userdomain "hyapp/services/user-service/internal/domain/user" ) @@ -49,7 +50,7 @@ func normalizeRegionRebuildWorkerOptions(options RegionRebuildWorkerOptions) Reg func (s *Service) RunRegionRebuildWorker(ctx context.Context, options RegionRebuildWorkerOptions) { options = normalizeRegionRebuildWorkerOptions(options) if s.regionRebuildRepository == nil { - log.Printf("worker=user_region_rebuild status=stopped reason=%q", "repository is not configured") + logx.Warn(ctx, "worker_disabled", slog.String("worker", "user_region_rebuild"), slog.String("reason", "repository is not configured")) return } @@ -76,13 +77,22 @@ func (s *Service) ProcessNextRegionRebuildTask(ctx context.Context, options Regi result, err := s.regionRebuildRepository.ProcessNextRegionRebuildTask(ctx, options.WorkerID, s.now().UnixMilli(), options.LockTTL, options.BatchSize) if err != nil { - log.Printf("worker=user_region_rebuild worker_id=%s status=failed error=%q", options.WorkerID, err.Error()) + logx.Error(ctx, "worker_batch_failed", err, slog.String("worker", "user_region_rebuild"), slog.String("worker_id", options.WorkerID), slog.Int("batch_size", options.BatchSize)) return result, err } if result.NoTask { return result, nil } - log.Printf("worker=user_region_rebuild worker_id=%s task_id=%d country=%s target_region_id=%d status=%s processed_users=%d cursor_user_id=%d", options.WorkerID, result.TaskID, result.CountryCode, result.TargetRegionID, result.Status, result.ProcessedUsers, result.CursorUserID) + logx.Info(ctx, "worker_batch_processed", + slog.String("worker", "user_region_rebuild"), + slog.String("worker_id", options.WorkerID), + slog.Int64("task_id", result.TaskID), + slog.String("country", result.CountryCode), + slog.Int64("target_region_id", result.TargetRegionID), + slog.String("status", result.Status), + slog.Int("processed_users", result.ProcessedUsers), + slog.Int64("cursor_user_id", result.CursorUserID), + ) return result, nil } diff --git a/services/user-service/internal/storage/mysql/mictime/repository.go b/services/user-service/internal/storage/mysql/mictime/repository.go index 713383da..82d204b7 100644 --- a/services/user-service/internal/storage/mysql/mictime/repository.go +++ b/services/user-service/internal/storage/mysql/mictime/repository.go @@ -4,7 +4,9 @@ package mictime import ( "context" "database/sql" + "encoding/json" "errors" + "fmt" "strings" "time" @@ -19,6 +21,13 @@ type Repository struct { db *sql.DB } +const ( + defaultCompensationBatchSize = 100 + defaultPendingPublishCompensationMaxAgeMs = int64(2 * 60 * 1000) + defaultPublishingCompensationMaxAgeMs = int64(12 * 60 * 60 * 1000) + userOutboxStatusPending = "pending" +) + // New creates a mic duration repository backed by the shared user-service connection pool. func New(db *sql.DB) *Repository { return &Repository{db: db} @@ -121,6 +130,83 @@ func (r *Repository) GetLifetimeStats(ctx context.Context, appCode string, userI return stats, nil } +// CompensateOpenSessions closes abnormal sessions whose authoritative down event never reached user-service. +func (r *Repository) CompensateOpenSessions(ctx context.Context, options mictimedomain.CompensationOptions) (mictimedomain.CompensationResult, error) { + if r == nil || r.db == nil { + return mictimedomain.CompensationResult{}, xerr.New(xerr.Unavailable, "mic time repository is not configured") + } + options = normalizeCompensationOptions(options) + + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return mictimedomain.CompensationResult{}, err + } + defer tx.Rollback() + + pendingCutoff := options.NowMs - options.PendingPublishMaxAgeMs + publishingCutoff := options.NowMs - options.PublishingSessionMaxAgeMs + rows, err := tx.QueryContext(ctx, ` + SELECT app_code, mic_session_id, user_id, room_id, first_seat_no, current_seat_no, status, seat_started_at_ms, + publishing_started_at_ms, ended_at_ms, last_room_version + FROM user_mic_sessions + WHERE ( + status = ? AND seat_started_at_ms <= ? + ) OR ( + status = ? AND seat_started_at_ms <= ? + ) + ORDER BY seat_started_at_ms ASC, mic_session_id ASC + LIMIT ? + FOR UPDATE SKIP LOCKED + `, mictimedomain.SessionStatusPendingPublish, pendingCutoff, mictimedomain.SessionStatusPublishing, publishingCutoff, options.BatchSize) + if err != nil { + return mictimedomain.CompensationResult{}, err + } + + candidates := make([]micSessionRow, 0, options.BatchSize) + for rows.Next() { + var session micSessionRow + if err := rows.Scan(&session.AppCode, &session.MicSessionID, &session.UserID, &session.RoomID, &session.FirstSeatNo, &session.CurrentSeatNo, &session.Status, &session.SeatStartedAtMs, &session.PublishingStartedAtMs, &session.EndedAtMs, &session.LastRoomVersion); err != nil { + _ = rows.Close() + return mictimedomain.CompensationResult{}, err + } + candidates = append(candidates, session) + } + if err := rows.Close(); err != nil { + return mictimedomain.CompensationResult{}, err + } + if err := rows.Err(); err != nil { + return mictimedomain.CompensationResult{}, err + } + + var result mictimedomain.CompensationResult + for _, session := range candidates { + input := closeMicSessionInput{ + ClosedEventID: fmt.Sprintf("compensated:%s", session.MicSessionID), + EndAtMs: compensationEndAt(session, options), + UpdatedAtMs: options.NowMs, + RoomVersion: session.LastRoomVersion, + } + switch session.Status { + case mictimedomain.SessionStatusPendingPublish: + input.EndReason = mictimedomain.CompensationReasonPendingPublishTimeout + result.PendingPublishClosed++ + case mictimedomain.SessionStatusPublishing: + input.EndReason = mictimedomain.CompensationReasonPublishingMaxWindow + result.PublishingSessionClosed++ + default: + continue + } + if err := closeMicSession(ctx, tx, session, input); err != nil { + return mictimedomain.CompensationResult{}, err + } + result.ClosedCount++ + } + if err := tx.Commit(); err != nil { + return mictimedomain.CompensationResult{}, err + } + return result, nil +} + func applyUp(ctx context.Context, tx *sql.Tx, event mictimedomain.RoomMicEvent) (mictimedomain.ApplyResult, error) { seatNo := event.ToSeat if seatNo <= 0 { @@ -233,44 +319,87 @@ func applyDown(ctx context.Context, tx *sql.Tx, event mictimedomain.RoomMicEvent return mictimedomain.ApplyResult{Skipped: true, Reason: "session_already_ended"}, nil } - endAt := event.OccurredAtMs - if endAt < session.SeatStartedAtMs { - endAt = session.SeatStartedAtMs + endReason := strings.TrimSpace(event.Reason) + if endReason == "" { + endReason = "down" } - seatOccupiedMs := maxInt64(0, endAt-session.SeatStartedAtMs) + if err := closeMicSession(ctx, tx, session, closeMicSessionInput{ + ClosedEventID: event.EventID, + EndAtMs: event.OccurredAtMs, + EndReason: endReason, + UpdatedAtMs: event.OccurredAtMs, + RoomVersion: event.RoomVersion, + }); err != nil { + return mictimedomain.ApplyResult{}, err + } + return mictimedomain.ApplyResult{Consumed: true, Closed: true}, nil +} + +type closeMicSessionInput struct { + ClosedEventID string + EndAtMs int64 + EndReason string + UpdatedAtMs int64 + RoomVersion int64 +} + +func closeMicSession(ctx context.Context, tx *sql.Tx, session micSessionRow, input closeMicSessionInput) error { + input.ClosedEventID = strings.TrimSpace(input.ClosedEventID) + if input.ClosedEventID == "" { + input.ClosedEventID = "closed:" + session.MicSessionID + } + input.EndReason = strings.TrimSpace(input.EndReason) + if input.EndReason == "" { + input.EndReason = "down" + } + if input.EndAtMs < session.SeatStartedAtMs { + input.EndAtMs = session.SeatStartedAtMs + } + if input.UpdatedAtMs <= 0 { + input.UpdatedAtMs = input.EndAtMs + } + + seatOccupiedMs := maxInt64(0, input.EndAtMs-session.SeatStartedAtMs) micOnlineMs := int64(0) if session.PublishingStartedAtMs.Valid { publishStart := session.PublishingStartedAtMs.Int64 if publishStart < session.SeatStartedAtMs { publishStart = session.SeatStartedAtMs } - micOnlineMs = maxInt64(0, endAt-publishStart) - } - endReason := strings.TrimSpace(event.Reason) - if endReason == "" { - endReason = "down" + micOnlineMs = maxInt64(0, input.EndAtMs-publishStart) } - _, err = tx.ExecContext(ctx, ` + _, err := tx.ExecContext(ctx, ` UPDATE user_mic_sessions SET status = ?, ended_at_ms = ?, end_reason = ?, seat_occupied_ms = ?, mic_online_ms = ?, closed_event_id = ?, last_event_id = ?, last_room_version = GREATEST(last_room_version, ?), updated_at_ms = ? WHERE app_code = ? AND mic_session_id = ? AND status <> ? - `, mictimedomain.SessionStatusEnded, endAt, endReason, seatOccupiedMs, micOnlineMs, - event.EventID, event.EventID, event.RoomVersion, event.OccurredAtMs, event.AppCode, event.MicSessionID, mictimedomain.SessionStatusEnded) + `, mictimedomain.SessionStatusEnded, input.EndAtMs, input.EndReason, seatOccupiedMs, micOnlineMs, + input.ClosedEventID, input.ClosedEventID, input.RoomVersion, input.UpdatedAtMs, session.AppCode, session.MicSessionID, mictimedomain.SessionStatusEnded) if err != nil { - return mictimedomain.ApplyResult{}, err + return err } - if err := incrementLifetime(ctx, tx, session, event, endAt, seatOccupiedMs, micOnlineMs); err != nil { - return mictimedomain.ApplyResult{}, err + if err := incrementLifetime(ctx, tx, session, input, seatOccupiedMs, micOnlineMs); err != nil { + return err } - if err := incrementDaily(ctx, tx, session, event, endAt); err != nil { - return mictimedomain.ApplyResult{}, err + if err := incrementDaily(ctx, tx, session, input); err != nil { + return err } - return mictimedomain.ApplyResult{Consumed: true, Closed: true}, nil + return insertUserOutbox(ctx, tx, session.AppCode, userMicOutboxEventID(mictimedomain.EventTypeUserMicSessionClosed, session.MicSessionID, ""), mictimedomain.EventTypeUserMicSessionClosed, "user_mic_session", session.UserID, input.UpdatedAtMs, map[string]any{ + "app_code": session.AppCode, + "user_id": session.UserID, + "room_id": session.RoomID, + "mic_session_id": session.MicSessionID, + "seat_occupied_ms": seatOccupiedMs, + "mic_online_ms": micOnlineMs, + "seat_started_at_ms": session.SeatStartedAtMs, + "ended_at_ms": input.EndAtMs, + "end_reason": input.EndReason, + "closed_event_id": input.ClosedEventID, + }) } -func incrementLifetime(ctx context.Context, tx *sql.Tx, session micSessionRow, event mictimedomain.RoomMicEvent, endAt int64, seatMs int64, micMs int64) error { +func incrementLifetime(ctx context.Context, tx *sql.Tx, session micSessionRow, input closeMicSessionInput, seatMs int64, micMs int64) error { _, err := tx.ExecContext(ctx, ` INSERT INTO user_mic_lifetime_stats ( app_code, user_id, seat_occupied_ms, mic_online_ms, session_count, first_mic_at_ms, last_mic_at_ms, updated_at_ms @@ -282,19 +411,30 @@ func incrementLifetime(ctx context.Context, tx *sql.Tx, session micSessionRow, e first_mic_at_ms = CASE WHEN first_mic_at_ms = 0 OR first_mic_at_ms > VALUES(first_mic_at_ms) THEN VALUES(first_mic_at_ms) ELSE first_mic_at_ms END, last_mic_at_ms = GREATEST(last_mic_at_ms, VALUES(last_mic_at_ms)), updated_at_ms = VALUES(updated_at_ms) - `, event.AppCode, session.UserID, seatMs, micMs, session.SeatStartedAtMs, endAt, event.OccurredAtMs) - return err + `, session.AppCode, session.UserID, seatMs, micMs, session.SeatStartedAtMs, input.EndAtMs, input.UpdatedAtMs) + if err != nil { + return err + } + return insertUserOutbox(ctx, tx, session.AppCode, userMicOutboxEventID(mictimedomain.EventTypeUserMicLifetimeStatsUpdated, session.MicSessionID, ""), mictimedomain.EventTypeUserMicLifetimeStatsUpdated, "user_mic_lifetime_stats", session.UserID, input.UpdatedAtMs, map[string]any{ + "app_code": session.AppCode, + "user_id": session.UserID, + "mic_session_id": session.MicSessionID, + "seat_occupied_delta_ms": seatMs, + "mic_online_delta_ms": micMs, + "session_count_delta": 1, + "updated_at_ms": input.UpdatedAtMs, + }) } -func incrementDaily(ctx context.Context, tx *sql.Tx, session micSessionRow, event mictimedomain.RoomMicEvent, endAt int64) error { - seatParts := splitByUTCDay(session.SeatStartedAtMs, endAt) +func incrementDaily(ctx context.Context, tx *sql.Tx, session micSessionRow, input closeMicSessionInput) error { + seatParts := splitByUTCDay(session.SeatStartedAtMs, input.EndAtMs) micParts := map[string]dayDuration{} if session.PublishingStartedAtMs.Valid { publishStart := session.PublishingStartedAtMs.Int64 if publishStart < session.SeatStartedAtMs { publishStart = session.SeatStartedAtMs } - micParts = splitByUTCDay(publishStart, endAt) + micParts = splitByUTCDay(publishStart, input.EndAtMs) } dates := map[string]struct{}{} for date := range seatParts { @@ -331,15 +471,30 @@ func incrementDaily(ctx context.Context, tx *sql.Tx, session micSessionRow, even first_mic_at_ms = CASE WHEN first_mic_at_ms = 0 OR first_mic_at_ms > VALUES(first_mic_at_ms) THEN VALUES(first_mic_at_ms) ELSE first_mic_at_ms END, last_mic_at_ms = GREATEST(last_mic_at_ms, VALUES(last_mic_at_ms)), updated_at_ms = VALUES(updated_at_ms) - `, event.AppCode, session.UserID, date, seatPart.DurationMs, micPart.DurationMs, sessionCount, roomCount, firstAt, lastAt, event.OccurredAtMs) + `, session.AppCode, session.UserID, date, seatPart.DurationMs, micPart.DurationMs, sessionCount, roomCount, firstAt, lastAt, input.UpdatedAtMs) if err != nil { return err } + if err := insertUserOutbox(ctx, tx, session.AppCode, userMicOutboxEventID(mictimedomain.EventTypeUserMicDailyStatsUpdated, session.MicSessionID, date), mictimedomain.EventTypeUserMicDailyStatsUpdated, "user_mic_daily_stats", session.UserID, input.UpdatedAtMs, map[string]any{ + "app_code": session.AppCode, + "user_id": session.UserID, + "stat_date": date, + "mic_session_id": session.MicSessionID, + "seat_occupied_delta_ms": seatPart.DurationMs, + "mic_online_delta_ms": micPart.DurationMs, + "session_count_delta": sessionCount, + "room_count_delta": roomCount, + "updated_at_ms": input.UpdatedAtMs, + }); err != nil { + return err + } } return nil } type micSessionRow struct { + AppCode string + MicSessionID string UserID int64 RoomID string FirstSeatNo int32 @@ -354,12 +509,12 @@ type micSessionRow struct { func sessionForUpdate(ctx context.Context, tx *sql.Tx, appCode string, micSessionID string) (micSessionRow, bool, error) { var session micSessionRow err := tx.QueryRowContext(ctx, ` - SELECT user_id, room_id, first_seat_no, current_seat_no, status, seat_started_at_ms, + SELECT app_code, mic_session_id, user_id, room_id, first_seat_no, current_seat_no, status, seat_started_at_ms, publishing_started_at_ms, ended_at_ms, last_room_version FROM user_mic_sessions WHERE app_code = ? AND mic_session_id = ? FOR UPDATE - `, appCode, micSessionID).Scan(&session.UserID, &session.RoomID, &session.FirstSeatNo, &session.CurrentSeatNo, &session.Status, + `, appCode, micSessionID).Scan(&session.AppCode, &session.MicSessionID, &session.UserID, &session.RoomID, &session.FirstSeatNo, &session.CurrentSeatNo, &session.Status, &session.SeatStartedAtMs, &session.PublishingStartedAtMs, &session.EndedAtMs, &session.LastRoomVersion) if err == sql.ErrNoRows { return micSessionRow{}, false, nil @@ -410,6 +565,52 @@ func normalizeRoomMicEvent(event mictimedomain.RoomMicEvent) mictimedomain.RoomM return event } +func normalizeCompensationOptions(options mictimedomain.CompensationOptions) mictimedomain.CompensationOptions { + if options.NowMs <= 0 { + options.NowMs = time.Now().UnixMilli() + } + if options.BatchSize <= 0 { + options.BatchSize = defaultCompensationBatchSize + } + if options.PendingPublishMaxAgeMs <= 0 { + options.PendingPublishMaxAgeMs = defaultPendingPublishCompensationMaxAgeMs + } + if options.PublishingSessionMaxAgeMs <= 0 { + options.PublishingSessionMaxAgeMs = defaultPublishingCompensationMaxAgeMs + } + return options +} + +func compensationEndAt(session micSessionRow, options mictimedomain.CompensationOptions) int64 { + switch session.Status { + case mictimedomain.SessionStatusPendingPublish: + return minInt64(options.NowMs, session.SeatStartedAtMs+options.PendingPublishMaxAgeMs) + case mictimedomain.SessionStatusPublishing: + return minInt64(options.NowMs, session.SeatStartedAtMs+options.PublishingSessionMaxAgeMs) + default: + return options.NowMs + } +} + +func insertUserOutbox(ctx context.Context, tx *sql.Tx, appCode string, eventID string, eventType string, aggregateType string, aggregateID int64, createdAtMs int64, payload map[string]any) error { + payloadBytes, err := json.Marshal(payload) + if err != nil { + return err + } + _, err = tx.ExecContext(ctx, ` + INSERT IGNORE INTO user_outbox (app_code, event_id, event_type, aggregate_type, aggregate_id, status, payload_json, created_at_ms, updated_at_ms) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, appcode.Normalize(appCode), eventID, eventType, aggregateType, aggregateID, userOutboxStatusPending, string(payloadBytes), createdAtMs, createdAtMs) + return err +} + +func userMicOutboxEventID(eventType string, micSessionID string, suffix string) string { + if strings.TrimSpace(suffix) == "" { + return eventType + ":" + micSessionID + } + return eventType + ":" + micSessionID + ":" + suffix +} + type dayDuration struct { DurationMs int64 FirstAtMs int64 diff --git a/services/user-service/internal/storage/mysql/mictime/repository_test.go b/services/user-service/internal/storage/mysql/mictime/repository_test.go index 6f909e71..915168c2 100644 --- a/services/user-service/internal/storage/mysql/mictime/repository_test.go +++ b/services/user-service/internal/storage/mysql/mictime/repository_test.go @@ -2,6 +2,7 @@ package mictime_test import ( "context" + "database/sql" "testing" "time" @@ -10,7 +11,8 @@ import ( ) func TestProcessRoomMicEventsAggregatesLifetimeAndDeduplicates(t *testing.T) { - repo := mysqltest.NewRepository(t).Repository.MicTimeRepository() + wrapped := mysqltest.NewRepository(t) + repo := wrapped.Repository.MicTimeRepository() ctx := context.Background() base := time.Date(2026, 5, 8, 10, 0, 0, 0, time.UTC).UnixMilli() micSessionID := "mic_session_normal" @@ -27,6 +29,9 @@ func TestProcessRoomMicEventsAggregatesLifetimeAndDeduplicates(t *testing.T) { if stats.SeatOccupiedMs != 11*60_000 || stats.MicOnlineMs != 10*60_000 || stats.SessionCount != 1 { t.Fatalf("normal mic duration mismatch: %+v", stats) } + assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicSessionClosed:"+micSessionID) + assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicLifetimeStatsUpdated:"+micSessionID) + assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicDailyStatsUpdated:"+micSessionID+":2026-05-08") duplicate, err := repo.ProcessRoomMicEvent(ctx, roomMicEvent("evt-down", micSessionID, mictimedomain.ActionDown, 10001, 2, 0, base+11*60_000)) if err != nil || !duplicate.Skipped || duplicate.Reason != "already_consumed" { @@ -58,6 +63,54 @@ func TestProcessPendingPublishTimeoutCountsSeatButNotMicOnline(t *testing.T) { } } +func TestCompensateOpenSessionsClosesPendingAndPublishingSessions(t *testing.T) { + wrapped := mysqltest.NewRepository(t) + repo := wrapped.Repository.MicTimeRepository() + ctx := context.Background() + base := time.Date(2026, 5, 8, 14, 0, 0, 0, time.UTC).UnixMilli() + + pendingSessionID := "mic_session_comp_pending" + mustApply(t, repo, roomMicEvent("evt-comp-pending-up", pendingSessionID, mictimedomain.ActionUp, 10004, 0, 1, base)) + + publishingSessionID := "mic_session_comp_publishing" + mustApply(t, repo, roomMicEvent("evt-comp-pub-up", publishingSessionID, mictimedomain.ActionUp, 10005, 0, 1, base)) + mustApply(t, repo, roomMicEvent("evt-comp-pub-confirm", publishingSessionID, mictimedomain.ActionPublishConfirmed, 10005, 1, 1, base+60_000)) + + result, err := repo.CompensateOpenSessions(ctx, mictimedomain.CompensationOptions{ + NowMs: base + 3*60*60*1000, + BatchSize: 10, + PendingPublishMaxAgeMs: 2 * 60 * 1000, + PublishingSessionMaxAgeMs: 60 * 60 * 1000, + }) + if err != nil { + t.Fatalf("CompensateOpenSessions failed: %v", err) + } + if result.ClosedCount != 2 || result.PendingPublishClosed != 1 || result.PublishingSessionClosed != 1 { + t.Fatalf("compensation result mismatch: %+v", result) + } + + pendingStats, err := repo.GetLifetimeStats(ctx, "lalu", 10004) + if err != nil { + t.Fatalf("GetLifetimeStats pending failed: %v", err) + } + if pendingStats.SeatOccupiedMs != 2*60*1000 || pendingStats.MicOnlineMs != 0 || pendingStats.SessionCount != 1 { + t.Fatalf("pending compensation stats mismatch: %+v", pendingStats) + } + assertSessionEnded(t, wrapped.RawDB(), pendingSessionID, mictimedomain.CompensationReasonPendingPublishTimeout) + assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicSessionClosed:"+pendingSessionID) + + publishingStats, err := repo.GetLifetimeStats(ctx, "lalu", 10005) + if err != nil { + t.Fatalf("GetLifetimeStats publishing failed: %v", err) + } + if publishingStats.SeatOccupiedMs != 60*60*1000 || publishingStats.MicOnlineMs != 59*60*1000 || publishingStats.SessionCount != 1 { + t.Fatalf("publishing compensation stats mismatch: %+v", publishingStats) + } + assertSessionEnded(t, wrapped.RawDB(), publishingSessionID, mictimedomain.CompensationReasonPublishingMaxWindow) + assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicSessionClosed:"+publishingSessionID) + assertUserOutboxEvent(t, wrapped.RawDB(), "UserMicLifetimeStatsUpdated:"+publishingSessionID) +} + func TestProcessCrossDayMicSessionKeepsLifetimeTotals(t *testing.T) { repo := mysqltest.NewRepository(t).Repository.MicTimeRepository() ctx := context.Background() @@ -113,3 +166,34 @@ func roomMicEvent(eventID string, micSessionID string, action string, userID int } return event } + +func assertUserOutboxEvent(t *testing.T, db *sql.DB, eventID string) { + t.Helper() + var count int + if err := db.QueryRowContext(context.Background(), ` + SELECT COUNT(*) + FROM user_outbox + WHERE app_code = 'lalu' AND event_id = ? + `, eventID).Scan(&count); err != nil { + t.Fatalf("query user_outbox failed: %v", err) + } + if count != 1 { + t.Fatalf("user_outbox event %s count=%d, want 1", eventID, count) + } +} + +func assertSessionEnded(t *testing.T, db *sql.DB, micSessionID string, reason string) { + t.Helper() + var status string + var endReason string + if err := db.QueryRowContext(context.Background(), ` + SELECT status, end_reason + FROM user_mic_sessions + WHERE app_code = 'lalu' AND mic_session_id = ? + `, micSessionID).Scan(&status, &endReason); err != nil { + t.Fatalf("query user_mic_sessions failed: %v", err) + } + if status != mictimedomain.SessionStatusEnded || endReason != reason { + t.Fatalf("session %s terminal state mismatch: status=%s reason=%s", micSessionID, status, endReason) + } +} diff --git a/services/user-service/internal/testutil/mysqltest/mysqltest.go b/services/user-service/internal/testutil/mysqltest/mysqltest.go index 1624c92f..2a15b27c 100644 --- a/services/user-service/internal/testutil/mysqltest/mysqltest.go +++ b/services/user-service/internal/testutil/mysqltest/mysqltest.go @@ -3,6 +3,7 @@ package mysqltest import ( "context" + "database/sql" "strings" "testing" "time" @@ -52,6 +53,14 @@ func (r *Repository) Close() { } } +// RawDB exposes the isolated test schema for assertions that span read-model tables. +func (r *Repository) RawDB() *sql.DB { + if r == nil || r.schema == nil { + return nil + } + return r.schema.DB +} + // GetUser 让测试 wrapper 继续满足业务层用户主数据接口。 func (r *Repository) GetUser(ctx context.Context, userID int64) (userdomain.User, error) { return r.Repository.UserRepository().GetUser(ctx, userID) diff --git a/services/wallet-service/cmd/server/main.go b/services/wallet-service/cmd/server/main.go index fd59210b..0d080d9f 100644 --- a/services/wallet-service/cmd/server/main.go +++ b/services/wallet-service/cmd/server/main.go @@ -4,9 +4,11 @@ import ( "context" "flag" "log" + "os" "os/signal" "syscall" + "hyapp/pkg/logx" "hyapp/services/wallet-service/internal/app" "hyapp/services/wallet-service/internal/config" ) @@ -20,10 +22,13 @@ func main() { if err != nil { log.Fatal(err) } + if err := logx.Init(cfg.Log.WithRuntimeFields(cfg.ServiceName, cfg.Environment, cfg.NodeID)); err != nil { + log.Fatal(err) + } application, err := app.New(cfg) if err != nil { - log.Fatal(err) + fatalRuntime("app_new_failed", err) } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) @@ -39,7 +44,12 @@ func main() { application.Close() case err := <-errCh: if err != nil { - log.Fatal(err) + fatalRuntime("service_run_failed", err) } } } + +func fatalRuntime(msg string, err error) { + logx.Error(context.Background(), msg, err) + os.Exit(1) +} diff --git a/services/wallet-service/configs/config.docker.yaml b/services/wallet-service/configs/config.docker.yaml index 650743a6..e7991271 100644 --- a/services/wallet-service/configs/config.docker.yaml +++ b/services/wallet-service/configs/config.docker.yaml @@ -1,5 +1,12 @@ service_name: wallet-service node_id: wallet-docker +environment: local +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 grpc_addr: ":13004" mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=Local" mysql_auto_migrate: false diff --git a/services/wallet-service/configs/config.tencent.example.yaml b/services/wallet-service/configs/config.tencent.example.yaml index e20d88d2..fc9095d1 100644 --- a/services/wallet-service/configs/config.tencent.example.yaml +++ b/services/wallet-service/configs/config.tencent.example.yaml @@ -1,5 +1,12 @@ service_name: wallet-service node_id: wallet-tencent +environment: prod +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 grpc_addr: ":13004" mysql_dsn: "${TENCENT_MYSQL_WALLET_DSN}" mysql_auto_migrate: false diff --git a/services/wallet-service/configs/config.yaml b/services/wallet-service/configs/config.yaml index d7199429..ead3b652 100644 --- a/services/wallet-service/configs/config.yaml +++ b/services/wallet-service/configs/config.yaml @@ -1,5 +1,12 @@ service_name: wallet-service node_id: wallet-local +environment: local +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 grpc_addr: ":13004" mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=Local" mysql_auto_migrate: false diff --git a/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql b/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql index 11cbddb5..b0fb2db1 100644 --- a/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql +++ b/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql @@ -127,6 +127,31 @@ CREATE TABLE IF NOT EXISTS wallet_recharge_records ( KEY idx_wallet_recharge_records_region_time (app_code, target_region_id, created_at_ms) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS coin_seller_stock_records ( + app_code VARCHAR(32) NOT NULL DEFAULT 'lalu', + stock_id VARCHAR(96) NOT NULL, + transaction_id VARCHAR(96) NOT NULL, + command_id VARCHAR(128) NOT NULL, + seller_user_id BIGINT NOT NULL, + stock_type VARCHAR(32) NOT NULL, + counts_as_seller_recharge BOOLEAN NOT NULL, + coin_amount BIGINT NOT NULL, + paid_currency_code VARCHAR(8) NOT NULL DEFAULT '', + paid_amount_micro BIGINT NOT NULL DEFAULT 0, + payment_ref VARCHAR(128) NULL, + evidence_ref VARCHAR(256) NOT NULL DEFAULT '', + operator_user_id BIGINT NOT NULL, + reason VARCHAR(512) NOT NULL DEFAULT '', + balance_after BIGINT NOT NULL, + metadata_json JSON NULL, + created_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, stock_id), + UNIQUE KEY uk_coin_seller_stock_command (app_code, command_id), + UNIQUE KEY uk_coin_seller_stock_payment_ref (app_code, stock_type, payment_ref), + KEY idx_coin_seller_stock_seller_time (app_code, seller_user_id, created_at_ms), + KEY idx_coin_seller_stock_type_time (app_code, stock_type, created_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE IF NOT EXISTS resources ( app_code VARCHAR(32) NOT NULL DEFAULT 'lalu', resource_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, diff --git a/services/wallet-service/internal/config/config.go b/services/wallet-service/internal/config/config.go index 1b9311bf..c4d79cef 100644 --- a/services/wallet-service/internal/config/config.go +++ b/services/wallet-service/internal/config/config.go @@ -1,16 +1,23 @@ package config -import "hyapp/pkg/configx" +import ( + "strings" + + "hyapp/pkg/configx" + "hyapp/pkg/logx" +) // Config 描述 wallet-service 启动配置。 type Config struct { ServiceName string `yaml:"service_name"` NodeID string `yaml:"node_id"` + Environment string `yaml:"environment"` GRPCAddr string `yaml:"grpc_addr"` // MySQLDSN 是账户、交易、分录和 outbox 的必需存储连接串。 MySQLDSN string `yaml:"mysql_dsn"` // MySQLAutoMigrate 保留给显式本地迁移;线上 schema 由发布流程或 initdb 管理。 - MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"` + MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"` + Log logx.Config `yaml:"log"` } // Default 返回本地开发默认配置。 @@ -18,9 +25,15 @@ func Default() Config { return Config{ ServiceName: "wallet-service", NodeID: "wallet-local", + Environment: "local", GRPCAddr: ":13004", MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=Local", MySQLAutoMigrate: false, + Log: logx.Config{ + Level: "info", + Format: "json", + MaxPayloadBytes: 2048, + }, } } @@ -35,5 +48,18 @@ func Load(path string) (Config, error) { return Config{}, err } + cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) + if cfg.ServiceName == "" { + cfg.ServiceName = "wallet-service" + } + cfg.NodeID = strings.TrimSpace(cfg.NodeID) + if cfg.NodeID == "" { + cfg.NodeID = cfg.ServiceName + } + cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment)) + if cfg.Environment == "" { + cfg.Environment = "local" + } + return cfg, nil } diff --git a/services/wallet-service/internal/domain/ledger/ledger.go b/services/wallet-service/internal/domain/ledger/ledger.go index 15bd186f..9cd14552 100644 --- a/services/wallet-service/internal/domain/ledger/ledger.go +++ b/services/wallet-service/internal/domain/ledger/ledger.go @@ -13,6 +13,13 @@ const ( AssetDiamond = "DIAMOND" // AssetUSDBalance 是主播可提现美元余额,单位由后续提现策略固定。 AssetUSDBalance = "USD_BALANCE" + + // StockTypeUSDTPurchase 表示币商线下 USDT 进货后发放专用金币库存。 + StockTypeUSDTPurchase = "usdt_purchase" + // StockTypeCoinCompensation 表示平台人工补偿币商专用金币库存,不计入进货金额。 + StockTypeCoinCompensation = "coin_compensation" + // PaidCurrencyUSDT 是首版币商进货支持的唯一线下付款币种。 + PaidCurrencyUSDT = "USDT" ) // DebitGiftCommand 是 room-service 送礼扣费的账务命令。 @@ -67,6 +74,34 @@ type CoinSellerTransferReceipt struct { RechargePolicyUSDMinorUnit int64 } +// CoinSellerStockCreditCommand 是后台给币商专用金币库存入账的最小账务命令。 +type CoinSellerStockCreditCommand struct { + AppCode string + CommandID string + SellerUserID int64 + StockType string + CoinAmount int64 + PaidCurrencyCode string + PaidAmountMicro int64 + PaymentRef string + EvidenceRef string + OperatorUserID int64 + Reason string +} + +// CoinSellerStockCreditReceipt 是币商库存入账成功后的稳定回执。 +type CoinSellerStockCreditReceipt struct { + TransactionID string + SellerUserID int64 + StockType string + CoinAmount int64 + PaidCurrencyCode string + PaidAmountMicro int64 + CountsAsSellerRecharge bool + BalanceAfter int64 + CreatedAtMS int64 +} + // RechargeBill 是充值账单的只读投影;充值事实由 wallet_recharge_records 保存。 type RechargeBill struct { AppCode string @@ -154,3 +189,14 @@ func ValidGiftChargeAssetType(assetType string) bool { return false } } + +func NormalizeCoinSellerStockType(stockType string) string { + switch strings.ToLower(strings.TrimSpace(stockType)) { + case StockTypeUSDTPurchase: + return StockTypeUSDTPurchase + case StockTypeCoinCompensation: + return StockTypeCoinCompensation + default: + return "" + } +} diff --git a/services/wallet-service/internal/service/wallet/service.go b/services/wallet-service/internal/service/wallet/service.go index 0d0c4a79..070469d3 100644 --- a/services/wallet-service/internal/service/wallet/service.go +++ b/services/wallet-service/internal/service/wallet/service.go @@ -15,6 +15,7 @@ type Repository interface { DebitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error) GetBalances(ctx context.Context, userID int64, assetTypes []string) ([]ledger.AssetBalance, error) AdminCreditAsset(ctx context.Context, command ledger.AdminCreditAssetCommand) (ledger.AssetBalance, string, error) + AdminCreditCoinSellerStock(ctx context.Context, command ledger.CoinSellerStockCreditCommand) (ledger.CoinSellerStockCreditReceipt, error) TransferCoinFromSeller(ctx context.Context, command ledger.CoinSellerTransferCommand) (ledger.CoinSellerTransferReceipt, error) ListRechargeBills(ctx context.Context, query ledger.ListRechargeBillsQuery) ([]ledger.RechargeBill, int64, error) ListResources(ctx context.Context, query resourcedomain.ListResourcesQuery) ([]resourcedomain.Resource, int64, error) @@ -113,6 +114,52 @@ func (s *Service) AdminCreditAsset(ctx context.Context, command ledger.AdminCred return s.repository.AdminCreditAsset(ctx, command) } +// AdminCreditCoinSellerStock 只给 COIN_SELLER_COIN 库存入账;USDT 进货和金币补偿使用不同账务类型。 +func (s *Service) AdminCreditCoinSellerStock(ctx context.Context, command ledger.CoinSellerStockCreditCommand) (ledger.CoinSellerStockCreditReceipt, error) { + if command.CommandID == "" || command.SellerUserID <= 0 || command.OperatorUserID <= 0 { + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.InvalidArgument, "coin seller stock command is incomplete") + } + if len(command.CommandID) > 128 { + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.InvalidArgument, "command_id is too long") + } + if command.CoinAmount <= 0 { + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockAmountInvalid, "coin_amount must be positive") + } + command.StockType = ledger.NormalizeCoinSellerStockType(command.StockType) + if command.StockType == "" { + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockTypeInvalid, "stock_type is invalid") + } + command.PaidCurrencyCode = strings.ToUpper(strings.TrimSpace(command.PaidCurrencyCode)) + command.PaymentRef = strings.TrimSpace(command.PaymentRef) + command.EvidenceRef = strings.TrimSpace(command.EvidenceRef) + command.Reason = strings.TrimSpace(command.Reason) + if len(command.PaidCurrencyCode) > 8 || len(command.PaymentRef) > 128 || len(command.EvidenceRef) > 256 || len(command.Reason) > 512 { + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.InvalidArgument, "coin seller stock text fields are too long") + } + switch command.StockType { + case ledger.StockTypeUSDTPurchase: + if command.PaidCurrencyCode == "" { + command.PaidCurrencyCode = ledger.PaidCurrencyUSDT + } + if command.PaidCurrencyCode != ledger.PaidCurrencyUSDT || command.PaidAmountMicro <= 0 { + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockAmountInvalid, "usdt purchase requires paid amount") + } + case ledger.StockTypeCoinCompensation: + if command.PaidCurrencyCode != "" || command.PaidAmountMicro != 0 || command.PaymentRef != "" { + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockAmountInvalid, "coin compensation must not include paid amount") + } + default: + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockTypeInvalid, "stock_type is invalid") + } + if s.repository == nil { + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") + } + command.AppCode = appcode.Normalize(command.AppCode) + ctx = appcode.WithContext(ctx, command.AppCode) + + return s.repository.AdminCreditCoinSellerStock(ctx, command) +} + // TransferCoinFromSeller 执行币商向玩家转金币;身份和区域都由 gateway/user-service 校验并传入。 func (s *Service) TransferCoinFromSeller(ctx context.Context, command ledger.CoinSellerTransferCommand) (ledger.CoinSellerTransferReceipt, error) { if command.CommandID == "" || command.SellerUserID <= 0 || command.TargetUserID <= 0 || command.Amount <= 0 { diff --git a/services/wallet-service/internal/service/wallet/service_test.go b/services/wallet-service/internal/service/wallet/service_test.go index 83f709ed..93ee8b29 100644 --- a/services/wallet-service/internal/service/wallet/service_test.go +++ b/services/wallet-service/internal/service/wallet/service_test.go @@ -297,6 +297,150 @@ func TestTransferCoinFromSellerRequiresRechargePolicy(t *testing.T) { } } +// TestAdminCreditCoinSellerStockPurchaseCreditsDedicatedBalance 验证 USDT 进货只增加币商专用库存并写专用进货记录。 +func TestAdminCreditCoinSellerStockPurchaseCreditsDedicatedBalance(t *testing.T) { + repository := mysqltest.NewRepository(t) + svc := walletservice.New(repository) + command := ledger.CoinSellerStockCreditCommand{ + CommandID: "cmd-stock-usdt", + SellerUserID: 33001, + StockType: ledger.StockTypeUSDTPurchase, + CoinAmount: 8000000, + PaidAmountMicro: 100000000, + OperatorUserID: 90001, + } + + first, err := svc.AdminCreditCoinSellerStock(context.Background(), command) + if err != nil { + t.Fatalf("AdminCreditCoinSellerStock purchase failed: %v", err) + } + second, err := svc.AdminCreditCoinSellerStock(context.Background(), command) + if err != nil { + t.Fatalf("AdminCreditCoinSellerStock retry failed: %v", err) + } + if first.TransactionID == "" || first.TransactionID != second.TransactionID || first.BalanceAfter != 8000000 || !first.CountsAsSellerRecharge || first.PaidCurrencyCode != ledger.PaidCurrencyUSDT { + t.Fatalf("stock purchase receipt mismatch: first=%+v second=%+v", first, second) + } + + balances, err := svc.GetBalances(context.Background(), 33001, []string{ledger.AssetCoinSellerCoin, ledger.AssetCoin}) + if err != nil { + t.Fatalf("GetBalances seller failed: %v", err) + } + if balanceAmount(balances, ledger.AssetCoinSellerCoin) != 8000000 || balanceAmount(balances, ledger.AssetCoin) != 0 { + t.Fatalf("seller stock balances mismatch: %+v", balances) + } + if got := repository.CountRows("coin_seller_stock_records", "transaction_id = ? AND counts_as_seller_recharge = TRUE", first.TransactionID); got != 1 { + t.Fatalf("stock purchase should write one seller recharge record, got %d", got) + } + if got := repository.CountRows("wallet_entries", "transaction_id = ?", first.TransactionID); got != 1 { + t.Fatalf("stock purchase should write one wallet entry, got %d", got) + } + if got := repository.CountRows("wallet_recharge_records", "transaction_id = ?", first.TransactionID); got != 0 { + t.Fatalf("seller stock purchase must not write player recharge record, got %d", got) + } + if got := repository.CountRows("wallet_outbox", "transaction_id = ?", first.TransactionID); got != 2 { + t.Fatalf("stock purchase should write balance and stock outbox events, got %d", got) + } +} + +// TestAdminCreditCoinSellerStockCompensationRejectsPaidAmount 锁定补偿不能携带充值金额的边界。 +func TestAdminCreditCoinSellerStockCompensationRejectsPaidAmount(t *testing.T) { + repository := mysqltest.NewRepository(t) + svc := walletservice.New(repository) + _, err := svc.AdminCreditCoinSellerStock(context.Background(), ledger.CoinSellerStockCreditCommand{ + CommandID: "cmd-stock-comp-invalid", + SellerUserID: 34001, + StockType: ledger.StockTypeCoinCompensation, + CoinAmount: 50000, + PaidCurrencyCode: ledger.PaidCurrencyUSDT, + PaidAmountMicro: 1, + OperatorUserID: 90001, + }) + if !xerr.IsCode(err, xerr.CoinSellerStockAmountInvalid) { + t.Fatalf("expected COIN_SELLER_STOCK_AMOUNT_INVALID, got %v", err) + } + if got := repository.CountRows("coin_seller_stock_records", "command_id = ?", "cmd-stock-comp-invalid"); got != 0 { + t.Fatalf("invalid compensation must not write stock record, got %d", got) + } +} + +// TestAdminCreditCoinSellerStockCompensationCreditsWithoutRechargeAmount 验证金币补偿不计入币商进货金额。 +func TestAdminCreditCoinSellerStockCompensationCreditsWithoutRechargeAmount(t *testing.T) { + repository := mysqltest.NewRepository(t) + svc := walletservice.New(repository) + receipt, err := svc.AdminCreditCoinSellerStock(context.Background(), ledger.CoinSellerStockCreditCommand{ + CommandID: "cmd-stock-comp", + SellerUserID: 35001, + StockType: ledger.StockTypeCoinCompensation, + CoinAmount: 50000, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("AdminCreditCoinSellerStock compensation failed: %v", err) + } + if receipt.CountsAsSellerRecharge || receipt.PaidAmountMicro != 0 || receipt.PaidCurrencyCode != "" || receipt.BalanceAfter != 50000 { + t.Fatalf("stock compensation receipt mismatch: %+v", receipt) + } + if got := repository.CountRows("coin_seller_stock_records", "transaction_id = ? AND counts_as_seller_recharge = FALSE AND paid_amount_micro = 0", receipt.TransactionID); got != 1 { + t.Fatalf("compensation should write one non-recharge stock record, got %d", got) + } +} + +// TestAdminCreditCoinSellerStockRejectsDuplicatePaymentRef 防止同一 USDT 凭证重复给库存入账。 +func TestAdminCreditCoinSellerStockRejectsDuplicatePaymentRef(t *testing.T) { + repository := mysqltest.NewRepository(t) + svc := walletservice.New(repository) + command := ledger.CoinSellerStockCreditCommand{ + CommandID: "cmd-stock-ref-1", + SellerUserID: 36001, + StockType: ledger.StockTypeUSDTPurchase, + CoinAmount: 100, + PaidAmountMicro: 1000000, + PaymentRef: "usdt-ref-dup", + EvidenceRef: "oss://receipt-dup-1", + OperatorUserID: 90001, + Reason: "first purchase", + } + if _, err := svc.AdminCreditCoinSellerStock(context.Background(), command); err != nil { + t.Fatalf("first stock purchase failed: %v", err) + } + command.CommandID = "cmd-stock-ref-2" + command.SellerUserID = 36002 + command.EvidenceRef = "oss://receipt-dup-2" + _, err := svc.AdminCreditCoinSellerStock(context.Background(), command) + if !xerr.IsCode(err, xerr.CoinSellerPaymentRefDuplicated) { + t.Fatalf("expected COIN_SELLER_PAYMENT_REF_DUPLICATED, got %v", err) + } + if got := repository.CountRows("coin_seller_stock_records", "payment_ref = ?", "usdt-ref-dup"); got != 1 { + t.Fatalf("duplicate payment_ref should keep one stock record, got %d", got) + } +} + +// TestAdminCreditCoinSellerStockRejectsIdempotencyConflict 锁定 command_id 相同但 payload 不同必须 fail-closed。 +func TestAdminCreditCoinSellerStockRejectsIdempotencyConflict(t *testing.T) { + repository := mysqltest.NewRepository(t) + svc := walletservice.New(repository) + command := ledger.CoinSellerStockCreditCommand{ + CommandID: "cmd-stock-conflict", + SellerUserID: 37001, + StockType: ledger.StockTypeUSDTPurchase, + CoinAmount: 100, + PaidAmountMicro: 1000000, + PaymentRef: "usdt-ref-conflict", + EvidenceRef: "oss://receipt-conflict", + OperatorUserID: 90001, + Reason: "first purchase", + } + if _, err := svc.AdminCreditCoinSellerStock(context.Background(), command); err != nil { + t.Fatalf("first stock purchase failed: %v", err) + } + command.CoinAmount = 200 + _, err := svc.AdminCreditCoinSellerStock(context.Background(), command) + if !xerr.IsCode(err, xerr.IdempotencyConflict) { + t.Fatalf("expected IDEMPOTENCY_CONFLICT, got %v", err) + } +} + // TestGiftConfigMustSelectGiftResource 验证后台新增礼物只能绑定 resource_type=gift 的资源。 func TestGiftConfigMustSelectGiftResource(t *testing.T) { repository := mysqltest.NewRepository(t) diff --git a/services/wallet-service/internal/storage/mysql/repository.go b/services/wallet-service/internal/storage/mysql/repository.go index 2001bae9..c353a1df 100644 --- a/services/wallet-service/internal/storage/mysql/repository.go +++ b/services/wallet-service/internal/storage/mysql/repository.go @@ -17,15 +17,17 @@ import ( "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" - _ "github.com/go-sql-driver/mysql" + mysqlDriver "github.com/go-sql-driver/mysql" ) const ( - transactionStatusSucceeded = "succeeded" - bizTypeGiftDebit = "gift_debit" - bizTypeManualCredit = "manual_credit" - bizTypeCoinSellerTransfer = "coin_seller_transfer" - outboxStatusPending = "pending" + transactionStatusSucceeded = "succeeded" + bizTypeGiftDebit = "gift_debit" + bizTypeManualCredit = "manual_credit" + bizTypeCoinSellerTransfer = "coin_seller_transfer" + bizTypeCoinSellerStockPurchase = "coin_seller_stock_purchase" + bizTypeCoinSellerCoinCompensation = "coin_seller_coin_compensation" + outboxStatusPending = "pending" ) // Repository 是 wallet-service 的 MySQL v2 账本入口。 @@ -345,6 +347,106 @@ func (r *Repository) AdminCreditAsset(ctx context.Context, command ledger.AdminC return balance, transactionID, nil } +// AdminCreditCoinSellerStock 在同一事务里给币商 COIN_SELLER_COIN 库存入账,并记录专用进货明细。 +func (r *Repository) AdminCreditCoinSellerStock(ctx context.Context, command ledger.CoinSellerStockCreditCommand) (ledger.CoinSellerStockCreditReceipt, error) { + if r == nil || r.db == nil { + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + ctx = contextWithCommandApp(ctx, command.AppCode) + command.AppCode = appcode.FromContext(ctx) + + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return ledger.CoinSellerStockCreditReceipt{}, err + } + defer func() { _ = tx.Rollback() }() + + bizType := coinSellerStockBizType(command.StockType) + if bizType == "" { + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockTypeInvalid, "stock_type is invalid") + } + requestHash := coinSellerStockRequestHash(command) + if txRow, exists, err := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, requestHash, bizType, xerr.IdempotencyConflict); err != nil || exists { + if err != nil || !exists { + return ledger.CoinSellerStockCreditReceipt{}, err + } + return r.receiptForCoinSellerStockTransaction(ctx, tx, txRow.TransactionID) + } + if command.PaymentRef != "" { + if duplicated, err := r.coinSellerStockPaymentRefExists(ctx, tx, command.StockType, command.PaymentRef); err != nil || duplicated { + if err != nil { + return ledger.CoinSellerStockCreditReceipt{}, err + } + return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerPaymentRefDuplicated, "payment_ref is duplicated") + } + } + + nowMs := time.Now().UnixMilli() + account, err := r.lockAccount(ctx, tx, command.SellerUserID, ledger.AssetCoinSellerCoin, true, nowMs) + if err != nil { + return ledger.CoinSellerStockCreditReceipt{}, err + } + balanceAfter, err := checkedAdd(account.AvailableAmount, command.CoinAmount) + if err != nil { + return ledger.CoinSellerStockCreditReceipt{}, err + } + + transactionID := transactionID(command.AppCode, command.CommandID) + countsAsSellerRecharge := command.StockType == ledger.StockTypeUSDTPurchase + metadata := coinSellerStockMetadata{ + AppCode: command.AppCode, + SellerUserID: command.SellerUserID, + StockType: command.StockType, + CoinAmount: command.CoinAmount, + PaidCurrencyCode: command.PaidCurrencyCode, + PaidAmountMicro: command.PaidAmountMicro, + PaymentRef: command.PaymentRef, + EvidenceRef: command.EvidenceRef, + OperatorUserID: command.OperatorUserID, + Reason: command.Reason, + AssetType: ledger.AssetCoinSellerCoin, + CountsAsSellerRecharge: countsAsSellerRecharge, + BalanceAfter: balanceAfter, + CreatedAtMS: nowMs, + } + externalRef := command.EvidenceRef + if command.PaymentRef != "" { + externalRef = command.PaymentRef + } + if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizType, requestHash, externalRef, metadata, nowMs); err != nil { + return ledger.CoinSellerStockCreditReceipt{}, err + } + if err := r.applyAccountDelta(ctx, tx, account, command.CoinAmount, 0, nowMs); err != nil { + return ledger.CoinSellerStockCreditReceipt{}, err + } + if err := r.insertEntry(ctx, tx, walletEntry{ + TransactionID: transactionID, + UserID: command.SellerUserID, + AssetType: ledger.AssetCoinSellerCoin, + AvailableDelta: command.CoinAmount, + FrozenDelta: 0, + AvailableAfter: balanceAfter, + FrozenAfter: account.FrozenAmount, + CreatedAtMS: nowMs, + }); err != nil { + return ledger.CoinSellerStockCreditReceipt{}, err + } + if err := r.insertCoinSellerStockRecord(ctx, tx, transactionID, command.CommandID, metadata, nowMs); err != nil { + return ledger.CoinSellerStockCreditReceipt{}, err + } + if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ + balanceChangedEvent(transactionID, command.CommandID, command.SellerUserID, ledger.AssetCoinSellerCoin, command.CoinAmount, 0, balanceAfter, account.FrozenAmount, metadata, nowMs), + coinSellerStockCreditedEvent(transactionID, command.CommandID, metadata, nowMs), + }); err != nil { + return ledger.CoinSellerStockCreditReceipt{}, err + } + if err := tx.Commit(); err != nil { + return ledger.CoinSellerStockCreditReceipt{}, err + } + + return receiptFromCoinSellerStockMetadata(transactionID, metadata), nil +} + // TransferCoinFromSeller 在同一事务里扣币商专用金币、给玩家普通 COIN 入账,并记录充值口径。 func (r *Repository) TransferCoinFromSeller(ctx context.Context, command ledger.CoinSellerTransferCommand) (ledger.CoinSellerTransferReceipt, error) { if r == nil || r.db == nil { @@ -469,6 +571,10 @@ type transactionRow struct { } func (r *Repository) lookupTransaction(ctx context.Context, tx *sql.Tx, commandID string, requestHash string, bizType string) (transactionRow, bool, error) { + return r.lookupTransactionWithConflictCode(ctx, tx, commandID, requestHash, bizType, xerr.LedgerConflict) +} + +func (r *Repository) lookupTransactionWithConflictCode(ctx context.Context, tx *sql.Tx, commandID string, requestHash string, bizType string, conflictCode xerr.Code) (transactionRow, bool, error) { row := tx.QueryRowContext(ctx, `SELECT transaction_id, request_hash, biz_type, COALESCE(CAST(metadata_json AS CHAR), '{}') FROM wallet_transactions @@ -489,7 +595,7 @@ func (r *Repository) lookupTransaction(ctx context.Context, tx *sql.Tx, commandI } if storedHash != requestHash || storedBizType != bizType { // command_id 是钱包幂等主键,业务 payload 或命令类型变化必须 fail-closed。 - return transactionRow{}, true, xerr.New(xerr.LedgerConflict, "wallet command idempotency conflict") + return transactionRow{}, true, xerr.New(conflictCode, "wallet command idempotency conflict") } return txRow, true, nil } @@ -729,6 +835,65 @@ func (r *Repository) insertRechargeRecord(ctx context.Context, tx *sql.Tx, trans return err } +func (r *Repository) insertCoinSellerStockRecord(ctx context.Context, tx *sql.Tx, transactionID string, commandID string, metadata coinSellerStockMetadata, nowMs int64) error { + metadataJSON, err := json.Marshal(metadata) + if err != nil { + return err + } + var paymentRef any + if metadata.PaymentRef != "" { + paymentRef = metadata.PaymentRef + } + _, err = tx.ExecContext(ctx, + `INSERT INTO coin_seller_stock_records ( + app_code, stock_id, transaction_id, command_id, seller_user_id, stock_type, + counts_as_seller_recharge, coin_amount, paid_currency_code, paid_amount_micro, + payment_ref, evidence_ref, operator_user_id, reason, balance_after, metadata_json, created_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + normalizedEntryAppCode(ctx, metadata.AppCode), + coinSellerStockID(metadata.AppCode, commandID), + transactionID, + commandID, + metadata.SellerUserID, + metadata.StockType, + metadata.CountsAsSellerRecharge, + metadata.CoinAmount, + metadata.PaidCurrencyCode, + metadata.PaidAmountMicro, + paymentRef, + metadata.EvidenceRef, + metadata.OperatorUserID, + metadata.Reason, + metadata.BalanceAfter, + string(metadataJSON), + nowMs, + ) + if err != nil && isMySQLDuplicateError(err) && metadata.PaymentRef != "" { + return xerr.New(xerr.CoinSellerPaymentRefDuplicated, "payment_ref is duplicated") + } + return err +} + +func (r *Repository) coinSellerStockPaymentRefExists(ctx context.Context, tx *sql.Tx, stockType string, paymentRef string) (bool, error) { + var stockID string + err := tx.QueryRowContext(ctx, + `SELECT stock_id + FROM coin_seller_stock_records + WHERE app_code = ? AND stock_type = ? AND payment_ref = ? + LIMIT 1`, + appcode.FromContext(ctx), + stockType, + paymentRef, + ).Scan(&stockID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, err + } + return true, nil +} + type walletEntry struct { AppCode string TransactionID string @@ -895,6 +1060,23 @@ type coinSellerTransferMetadata struct { RechargePolicyUSDMinorAmount int64 `json:"recharge_policy_usd_minor_amount"` } +type coinSellerStockMetadata struct { + AppCode string `json:"app_code"` + SellerUserID int64 `json:"seller_user_id"` + StockType string `json:"stock_type"` + CoinAmount int64 `json:"coin_amount"` + PaidCurrencyCode string `json:"paid_currency_code"` + PaidAmountMicro int64 `json:"paid_amount_micro"` + PaymentRef string `json:"payment_ref"` + EvidenceRef string `json:"evidence_ref"` + OperatorUserID int64 `json:"operator_user_id"` + Reason string `json:"reason"` + AssetType string `json:"asset_type"` + CountsAsSellerRecharge bool `json:"counts_as_seller_recharge"` + BalanceAfter int64 `json:"balance_after"` + CreatedAtMS int64 `json:"created_at_ms"` +} + func receiptFromGiftMetadata(transactionID string, metadata giftMetadata) ledger.Receipt { chargeAssetType := ledger.NormalizeGiftChargeAssetType(metadata.ChargeAssetType) chargeAmount := metadata.ChargeAmount @@ -914,6 +1096,46 @@ func receiptFromGiftMetadata(transactionID string, metadata giftMetadata) ledger } } +func (r *Repository) receiptForCoinSellerStockTransaction(ctx context.Context, tx *sql.Tx, transactionID string) (ledger.CoinSellerStockCreditReceipt, error) { + row := tx.QueryRowContext(ctx, + `SELECT seller_user_id, stock_type, coin_amount, paid_currency_code, paid_amount_micro, + counts_as_seller_recharge, balance_after, created_at_ms + FROM coin_seller_stock_records + WHERE app_code = ? AND transaction_id = ?`, + appcode.FromContext(ctx), + transactionID, + ) + var receipt ledger.CoinSellerStockCreditReceipt + if err := row.Scan( + &receipt.SellerUserID, + &receipt.StockType, + &receipt.CoinAmount, + &receipt.PaidCurrencyCode, + &receipt.PaidAmountMicro, + &receipt.CountsAsSellerRecharge, + &receipt.BalanceAfter, + &receipt.CreatedAtMS, + ); err != nil { + return ledger.CoinSellerStockCreditReceipt{}, err + } + receipt.TransactionID = transactionID + return receipt, nil +} + +func receiptFromCoinSellerStockMetadata(transactionID string, metadata coinSellerStockMetadata) ledger.CoinSellerStockCreditReceipt { + return ledger.CoinSellerStockCreditReceipt{ + TransactionID: transactionID, + SellerUserID: metadata.SellerUserID, + StockType: metadata.StockType, + CoinAmount: metadata.CoinAmount, + PaidCurrencyCode: metadata.PaidCurrencyCode, + PaidAmountMicro: metadata.PaidAmountMicro, + CountsAsSellerRecharge: metadata.CountsAsSellerRecharge, + BalanceAfter: metadata.BalanceAfter, + CreatedAtMS: metadata.CreatedAtMS, + } +} + func (r *Repository) receiptForCoinSellerTransferTransaction(ctx context.Context, tx *sql.Tx, transactionID string) (ledger.CoinSellerTransferReceipt, error) { var metadataJSON string if err := tx.QueryRowContext(ctx, @@ -1003,6 +1225,36 @@ func rechargeRecordedEvent(transactionID string, commandID string, targetUserID } } +func coinSellerStockCreditedEvent(transactionID string, commandID string, metadata coinSellerStockMetadata, nowMs int64) walletOutboxEvent { + eventType := "WalletCoinSellerCoinCompensated" + if metadata.StockType == ledger.StockTypeUSDTPurchase { + eventType = "WalletCoinSellerStockPurchased" + } + return walletOutboxEvent{ + EventID: eventID(transactionID, eventType, metadata.SellerUserID, ledger.AssetCoinSellerCoin), + EventType: eventType, + TransactionID: transactionID, + CommandID: commandID, + UserID: metadata.SellerUserID, + AssetType: ledger.AssetCoinSellerCoin, + AvailableDelta: metadata.CoinAmount, + FrozenDelta: 0, + Payload: map[string]any{ + "transaction_id": transactionID, + "command_id": commandID, + "seller_user_id": metadata.SellerUserID, + "stock_type": metadata.StockType, + "coin_amount": metadata.CoinAmount, + "paid_currency_code": metadata.PaidCurrencyCode, + "paid_amount_micro": metadata.PaidAmountMicro, + "counts_as_seller_recharge": metadata.CountsAsSellerRecharge, + "operator_user_id": metadata.OperatorUserID, + "created_at_ms": nowMs, + }, + CreatedAtMS: nowMs, + } +} + func giftDebitedEvent(transactionID string, commandID string, userID int64, assetType string, availableDelta int64, frozenDelta int64, payload any, nowMs int64) walletOutboxEvent { return walletOutboxEvent{ EventID: eventID(transactionID, "WalletGiftDebited", userID, assetType), @@ -1058,6 +1310,13 @@ func checkedMul(unit int64, count int64) (int64, error) { return unit * count, nil } +func checkedAdd(left int64, right int64) (int64, error) { + if right < 0 || left > math.MaxInt64-right { + return 0, xerr.New(xerr.CoinSellerStockAmountInvalid, "coin amount overflow") + } + return left + right, nil +} + func debitRequestHash(command ledger.DebitGiftCommand) string { // 哈希覆盖所有账务语义字段;相同 command_id 只能重放完全相同的业务命令。 return stableHash(fmt.Sprintf("gift|%s|%s|%d|%d|%s|%d|%s", @@ -1083,6 +1342,21 @@ func adminCreditRequestHash(command ledger.AdminCreditAssetCommand) string { )) } +func coinSellerStockRequestHash(command ledger.CoinSellerStockCreditCommand) string { + return stableHash(fmt.Sprintf("coin_seller_stock|%s|%d|%s|%d|%s|%d|%s|%s|%d|%s", + appcode.Normalize(command.AppCode), + command.SellerUserID, + command.StockType, + command.CoinAmount, + command.PaidCurrencyCode, + command.PaidAmountMicro, + command.PaymentRef, + command.EvidenceRef, + command.OperatorUserID, + command.Reason, + )) +} + func coinSellerTransferRequestHash(command ledger.CoinSellerTransferCommand) string { return stableHash(fmt.Sprintf("coin_seller_transfer|%s|%d|%d|%d|%d|%d|%s", appcode.Normalize(command.AppCode), @@ -1095,6 +1369,17 @@ func coinSellerTransferRequestHash(command ledger.CoinSellerTransferCommand) str )) } +func coinSellerStockBizType(stockType string) string { + switch stockType { + case ledger.StockTypeUSDTPurchase: + return bizTypeCoinSellerStockPurchase + case ledger.StockTypeCoinCompensation: + return bizTypeCoinSellerCoinCompensation + default: + return "" + } +} + func stableHash(value string) string { sum := sha256.Sum256([]byte(value)) return hex.EncodeToString(sum[:]) @@ -1112,6 +1397,10 @@ func transactionID(appCode string, commandID string) string { return "wtx_" + stableHash(appcode.Normalize(appCode)+"|"+commandID) } +func coinSellerStockID(appCode string, commandID string) string { + return "cstock_" + stableHash(appcode.Normalize(appCode)+"|"+commandID) +} + func billingReceiptID(appCode string, commandID string) string { return "bill_" + stableHash(appcode.Normalize(appCode)+"|"+commandID) } @@ -1119,3 +1408,8 @@ func billingReceiptID(appCode string, commandID string) string { func eventID(transactionID string, eventType string, userID int64, assetType string) string { return "wev_" + stableHash(fmt.Sprintf("%s|%s|%d|%s", transactionID, eventType, userID, assetType)) } + +func isMySQLDuplicateError(err error) bool { + var mysqlErr *mysqlDriver.MySQLError + return errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 +} diff --git a/services/wallet-service/internal/testutil/mysqltest/mysqltest.go b/services/wallet-service/internal/testutil/mysqltest/mysqltest.go index 1addd006..96b97867 100644 --- a/services/wallet-service/internal/testutil/mysqltest/mysqltest.go +++ b/services/wallet-service/internal/testutil/mysqltest/mysqltest.go @@ -158,6 +158,7 @@ func initDBPath(t testing.TB) string { func validateTableName(table string) string { switch table { case "wallet_transactions", "wallet_entries", "wallet_outbox", "wallet_accounts", "wallet_recharge_records", + "coin_seller_stock_records", "resources", "resource_groups", "resource_group_items", "gift_configs", "gift_config_regions", "resource_grants", "resource_grant_items", "user_resource_entitlements", "user_resource_equipment": return table default: diff --git a/services/wallet-service/internal/transport/grpc/server.go b/services/wallet-service/internal/transport/grpc/server.go index 195b5f03..15fa6275 100644 --- a/services/wallet-service/internal/transport/grpc/server.go +++ b/services/wallet-service/internal/transport/grpc/server.go @@ -104,6 +104,39 @@ func (s *Server) AdminCreditAsset(ctx context.Context, req *walletv1.AdminCredit }, nil } +// AdminCreditCoinSellerStock 处理后台币商进货或金币补偿,币商身份由 admin-server 先行校验。 +func (s *Server) AdminCreditCoinSellerStock(ctx context.Context, req *walletv1.AdminCreditCoinSellerStockRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) { + ctx = appcode.WithContext(ctx, req.GetAppCode()) + receipt, err := s.svc.AdminCreditCoinSellerStock(ctx, ledger.CoinSellerStockCreditCommand{ + AppCode: req.GetAppCode(), + CommandID: req.GetCommandId(), + SellerUserID: req.GetSellerUserId(), + StockType: req.GetStockType(), + CoinAmount: req.GetCoinAmount(), + PaidCurrencyCode: req.GetPaidCurrencyCode(), + PaidAmountMicro: req.GetPaidAmountMicro(), + PaymentRef: req.GetPaymentRef(), + EvidenceRef: req.GetEvidenceRef(), + OperatorUserID: req.GetOperatorUserId(), + Reason: req.GetReason(), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + + return &walletv1.AdminCreditCoinSellerStockResponse{ + TransactionId: receipt.TransactionID, + SellerUserId: receipt.SellerUserID, + StockType: receipt.StockType, + CoinAmount: receipt.CoinAmount, + PaidCurrencyCode: receipt.PaidCurrencyCode, + PaidAmountMicro: receipt.PaidAmountMicro, + CountsAsSellerRecharge: receipt.CountsAsSellerRecharge, + BalanceAfter: receipt.BalanceAfter, + CreatedAtMs: receipt.CreatedAtMS, + }, nil +} + // TransferCoinFromSeller 处理币商转玩家金币,调用方必须先完成币商身份校验。 func (s *Server) TransferCoinFromSeller(ctx context.Context, req *walletv1.TransferCoinFromSellerRequest) (*walletv1.TransferCoinFromSellerResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode())