From 47a7df339439a86b669285d400567166ee43669b Mon Sep 17 00:00:00 2001 From: zhx Date: Sat, 9 May 2026 21:47:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 + Makefile | 16 +- api/proto/activity/v1/activity.pb.go | 2123 +++++++++++++++- api/proto/activity/v1/activity.proto | 195 ++ api/proto/activity/v1/activity_grpc.pb.go | 472 +++- api/proto/events/room/v1/events.pb.go | 2 +- api/proto/room/v1/room.pb.go | 2 +- api/proto/room/v1/room_grpc.pb.go | 2 +- api/proto/user/v1/auth.pb.go | 293 ++- api/proto/user/v1/auth.proto | 16 + api/proto/user/v1/auth_grpc.pb.go | 50 +- api/proto/user/v1/host.pb.go | 2 +- api/proto/user/v1/host_grpc.pb.go | 2 +- api/proto/user/v1/user.pb.go | 2229 +++++++++-------- api/proto/user/v1/user.proto | 30 + api/proto/user/v1/user_grpc.pb.go | 184 +- api/proto/wallet/v1/wallet.pb.go | 653 +++-- api/proto/wallet/v1/wallet.proto | 21 + api/proto/wallet/v1/wallet_grpc.pb.go | 40 +- deploy/mysql/initdb/999_local_grants.sql | 1 + docker-compose.yml | 26 + docs/app-message-tab-architecture.md | 26 +- docs/cron-service-architecture.md | 301 +++ docs/daily-task-system-architecture.md | 579 +++++ docs/login-region-risk-architecture.md | 583 +++++ docs/user-mic-online-time-architecture.md | 22 +- pkg/xerr/catalog.go | 1 + pkg/xerr/errors.go | 2 + scripts/apply-local-mysql-initdb.sh | 1 + scripts/print-compose-addresses.sh | 1 + server/admin/cmd/server/main.go | 9 + server/admin/configs/config.yaml | 3 + server/admin/internal/config/config.go | 62 +- .../integration/activityclient/client.go | 36 + .../admin/internal/modules/appuser/handler.go | 80 + .../internal/modules/appuser/login_logs.go | 231 ++ .../admin/internal/modules/appuser/request.go | 18 + .../admin/internal/modules/appuser/routes.go | 2 + .../admin/internal/modules/appuser/service.go | 4 +- .../internal/modules/dailytask/handler.go | 224 ++ .../internal/modules/dailytask/routes.go | 18 + server/admin/internal/repository/seed.go | 28 +- server/admin/internal/router/router.go | 3 + .../configs/config.docker.yaml | 8 +- .../configs/config.tencent.example.yaml | 8 +- services/activity-service/configs/config.yaml | 8 +- .../mysql/initdb/001_activity_service.sql | 93 + services/activity-service/internal/app/app.go | 81 +- .../internal/config/config.go | 47 +- .../internal/domain/task/task.go | 166 ++ .../internal/service/message/fanout_worker.go | 20 - .../internal/service/task/service.go | 403 +++ .../internal/service/task/service_test.go | 116 + .../service/task/service_timezone_test.go | 14 + .../storage/mysql/message_repository.go | 4 +- .../internal/storage/mysql/task_repository.go | 792 ++++++ .../internal/transport/grpc/cron_server.go | 55 + .../internal/transport/grpc/task_server.go | 219 ++ services/cron-service/Dockerfile | 30 + services/cron-service/cmd/server/main.go | 55 + .../cron-service/configs/config.docker.yaml | 45 + .../configs/config.tencent.example.yaml | 45 + services/cron-service/configs/config.yaml | 45 + .../deploy/mysql/initdb/001_cron_service.sql | 35 + services/cron-service/internal/app/app.go | 140 ++ .../cron-service/internal/config/config.go | 218 ++ .../internal/integration/activity.go | 45 + .../cron-service/internal/integration/user.go | 70 + .../internal/scheduler/scheduler.go | 201 ++ .../internal/storage/mysql/repository.go | 143 ++ .../configs/config.docker.yaml | 11 + .../configs/config.tencent.example.yaml | 11 + services/gateway-service/configs/config.yaml | 11 + services/gateway-service/internal/app/app.go | 62 + .../internal/client/task_client.go | 32 + .../internal/client/user_client.go | 5 + .../gateway-service/internal/config/config.go | 34 + .../internal/transport/http/auth_handler.go | 73 +- .../transport/http/auth_rate_limiter_test.go | 2 +- .../internal/transport/http/handler.go | 12 +- .../internal/transport/http/login_risk.go | 281 +++ .../internal/transport/http/middleware.go | 27 +- .../internal/transport/http/response.go | 1 + .../internal/transport/http/response_test.go | 185 +- .../internal/transport/http/router.go | 2 + .../internal/transport/http/task_handler.go | 186 ++ .../user-service/configs/config.docker.yaml | 23 +- .../configs/config.tencent.example.yaml | 26 +- services/user-service/configs/config.yaml | 23 +- .../deploy/mysql/initdb/001_user_service.sql | 60 +- services/user-service/internal/app/app.go | 99 +- .../user-service/internal/config/config.go | 83 +- .../user-service/internal/domain/auth/auth.go | 16 + .../user-service/internal/domain/auth/risk.go | 61 + .../internal/service/auth/audit.go | 145 +- .../internal/service/auth/ip_geo_provider.go | 73 + .../internal/service/auth/password.go | 7 +- .../internal/service/auth/risk_cache.go | 68 + .../internal/service/auth/risk_job.go | 59 + .../internal/service/auth/risk_worker.go | 247 ++ .../internal/service/auth/service.go | 105 + .../internal/service/auth/service_test.go | 122 +- .../internal/service/auth/third_party.go | 23 +- .../internal/service/mictime/service.go | 49 +- .../service/user/region_rebuild_worker.go | 36 +- .../internal/storage/mysql/auth/audit.go | 20 +- .../internal/storage/mysql/auth/risk.go | 139 + .../internal/storage/mysql/auth/sessions.go | 32 +- .../storage/mysql/mictime/repository.go | 7 +- .../storage/mysql/region/rebuild_tasks.go | 15 +- .../internal/testutil/mysqltest/mysqltest.go | 30 + .../internal/transport/grpc/convert.go | 3 + .../internal/transport/grpc/cron.go | 97 + .../internal/transport/grpc/server.go | 9 + .../internal/domain/ledger/ledger.go | 20 + .../internal/service/wallet/service.go | 23 + .../internal/service/wallet/service_test.go | 43 + .../internal/storage/mysql/repository.go | 155 ++ .../internal/transport/grpc/server.go | 31 + 119 files changed, 12761 insertions(+), 1824 deletions(-) create mode 100644 docs/cron-service-architecture.md create mode 100644 docs/daily-task-system-architecture.md create mode 100644 docs/login-region-risk-architecture.md create mode 100644 server/admin/internal/integration/activityclient/client.go create mode 100644 server/admin/internal/modules/appuser/login_logs.go create mode 100644 server/admin/internal/modules/dailytask/handler.go create mode 100644 server/admin/internal/modules/dailytask/routes.go create mode 100644 services/activity-service/internal/domain/task/task.go create mode 100644 services/activity-service/internal/service/task/service.go create mode 100644 services/activity-service/internal/service/task/service_test.go create mode 100644 services/activity-service/internal/service/task/service_timezone_test.go create mode 100644 services/activity-service/internal/storage/mysql/task_repository.go create mode 100644 services/activity-service/internal/transport/grpc/cron_server.go create mode 100644 services/activity-service/internal/transport/grpc/task_server.go create mode 100644 services/cron-service/Dockerfile create mode 100644 services/cron-service/cmd/server/main.go create mode 100644 services/cron-service/configs/config.docker.yaml create mode 100644 services/cron-service/configs/config.tencent.example.yaml create mode 100644 services/cron-service/configs/config.yaml create mode 100644 services/cron-service/deploy/mysql/initdb/001_cron_service.sql create mode 100644 services/cron-service/internal/app/app.go create mode 100644 services/cron-service/internal/config/config.go create mode 100644 services/cron-service/internal/integration/activity.go create mode 100644 services/cron-service/internal/integration/user.go create mode 100644 services/cron-service/internal/scheduler/scheduler.go create mode 100644 services/cron-service/internal/storage/mysql/repository.go create mode 100644 services/gateway-service/internal/client/task_client.go create mode 100644 services/gateway-service/internal/transport/http/login_risk.go create mode 100644 services/gateway-service/internal/transport/http/task_handler.go create mode 100644 services/user-service/internal/domain/auth/risk.go create mode 100644 services/user-service/internal/service/auth/ip_geo_provider.go create mode 100644 services/user-service/internal/service/auth/risk_cache.go create mode 100644 services/user-service/internal/service/auth/risk_job.go create mode 100644 services/user-service/internal/service/auth/risk_worker.go create mode 100644 services/user-service/internal/storage/mysql/auth/risk.go create mode 100644 services/user-service/internal/transport/grpc/cron.go diff --git a/AGENTS.md b/AGENTS.md index d8ace880..b4267b88 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,7 @@ - `wallet-service` 负责账务语义。`SendGift` 必须先扣费成功,再落房间表现。 - `activity-service` 通过 room outbox 事件消费,不侵入 Room Cell 主链路。 - `user-service` 负责用户主数据,不应该成为房间高频流程的同步中心。 +- `cron-service` 只负责后台调度、任务运行记录和通过内部 gRPC 触发 owner service 批处理;不能直接拥有房间、账务、用户或活动业务状态。 - `server/admin` 是后台管理服务独立 module,负责管理员、RBAC、菜单、审计和后台 HTTP API;访问 app 后端只能通过 `hyapp.local/api` 生成的 protobuf client 或明确的集成接口。 - `api` 是独立 Go module,module path 为 `hyapp.local/api`,只放 protobuf contract 和生成代码,不放服务实现或业务工具包。 @@ -51,6 +52,7 @@ - `13004`: wallet gRPC - `13005`: user gRPC - `13006`: activity gRPC +- `13007`: cron gRPC - `23306`: local Docker MySQL host port - `13379`: local Docker Redis host port diff --git a/Makefile b/Makefile index 6abd376f..ae616dac 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -COMPOSE_SERVICES := mysql redis gateway-service room-service wallet-service user-service activity-service -SERVICE_ALIASES := gs gateway rs room ws wallet us user as activity mysql redis gateway-service room-service wallet-service user-service activity-service +COMPOSE_SERVICES := mysql redis gateway-service room-service wallet-service user-service activity-service cron-service +SERVICE_ALIASES := gs gateway rs room ws wallet us user as activity cs cron mysql redis gateway-service room-service wallet-service user-service activity-service cron-service SERVICE_TOKEN := $(firstword $(filter $(SERVICE_ALIASES),$(MAKECMDGOALS))) -SERVICE_ID := $(strip $(or $(SERVICE),$(if $(filter gs gateway,$(SERVICE_TOKEN)),gateway-service,$(if $(filter rs room,$(SERVICE_TOKEN)),room-service,$(if $(filter ws wallet,$(SERVICE_TOKEN)),wallet-service,$(if $(filter us user,$(SERVICE_TOKEN)),user-service,$(if $(filter as activity,$(SERVICE_TOKEN)),activity-service,$(SERVICE_TOKEN)))))))) +SERVICE_ID := $(strip $(or $(SERVICE),$(if $(filter gs gateway,$(SERVICE_TOKEN)),gateway-service,$(if $(filter rs room,$(SERVICE_TOKEN)),room-service,$(if $(filter ws wallet,$(SERVICE_TOKEN)),wallet-service,$(if $(filter us user,$(SERVICE_TOKEN)),user-service,$(if $(filter as activity,$(SERVICE_TOKEN)),activity-service,$(if $(filter cs cron,$(SERVICE_TOKEN)),cron-service,$(SERVICE_TOKEN))))))))) ADMIN_CONFIG ?= configs/config.yaml .PHONY: proto test build admin admin-up admin-deps admin-bootstrap admin-test admin-build up db-init rebuild restart stop logs ps addr down up-service check-service resolve-compose-conflicts $(SERVICE_ALIASES) @@ -65,12 +65,13 @@ admin-build: go build ./server/admin/... # `up` 后台拉起本地服务;带 SERVICE 时只启动单个服务。 +# 完整 up 结束后自动启动 admin 前台进程。 up: ./scripts/resolve-compose-container-conflicts.sh @if [ -z "$(SERVICE_ID)" ]; then \ docker compose up -d mysql redis; \ ./scripts/apply-local-mysql-initdb.sh; \ - docker compose up --build -d gateway-service room-service wallet-service user-service activity-service; \ + docker compose up --build -d gateway-service room-service wallet-service user-service activity-service cron-service; \ elif [ "$(SERVICE_ID)" = "mysql" ] || [ "$(SERVICE_ID)" = "redis" ]; then \ docker compose up -d $(SERVICE_ID); \ else \ @@ -78,6 +79,9 @@ up: docker compose up -d $(SERVICE_ID); \ fi ./scripts/print-compose-addresses.sh + @if [ -z "$(SERVICE_ID)" ]; then \ + $(MAKE) admin; \ + fi # `db-init` 对当前 compose MySQL 补齐所有服务 schema 和本地授权。 db-init: @@ -129,13 +133,13 @@ down: check-service: @if [ -z "$(SERVICE_ID)" ]; then \ echo "Service is required. Example: make restart rs"; \ - echo "Aliases: gs/gateway rs/room ws/wallet us/user as/activity mysql redis"; \ + echo "Aliases: gs/gateway rs/room ws/wallet us/user as/activity cs/cron mysql redis"; \ echo "Services: $(COMPOSE_SERVICES)"; \ exit 1; \ fi @if ! printf '%s\n' $(COMPOSE_SERVICES) | grep -qx "$(SERVICE_ID)"; then \ echo "Unknown service: $(SERVICE_ID)"; \ - echo "Aliases: gs/gateway rs/room ws/wallet us/user as/activity mysql redis"; \ + echo "Aliases: gs/gateway rs/room ws/wallet us/user as/activity cs/cron mysql redis"; \ echo "Services: $(COMPOSE_SERVICES)"; \ exit 1; \ fi diff --git a/api/proto/activity/v1/activity.pb.go b/api/proto/activity/v1/activity.pb.go index 5e525387..9d88173f 100644 --- a/api/proto/activity/v1/activity.pb.go +++ b/api/proto/activity/v1/activity.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.1 -// protoc v5.27.3 +// protoc v4.25.3 // source: proto/activity/v1/activity.proto package activityv1 @@ -1522,6 +1522,1537 @@ func (x *CreateFanoutJobResponse) GetCreated() bool { return false } +// CronBatchRequest 是 cron-service 触发 activity-service 后台批处理的统一请求。 +type CronBatchRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + WorkerId string `protobuf:"bytes,3,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + BatchSize int32 `protobuf:"varint,4,opt,name=batch_size,json=batchSize,proto3" json:"batch_size,omitempty"` + LockTtlMs int64 `protobuf:"varint,5,opt,name=lock_ttl_ms,json=lockTtlMs,proto3" json:"lock_ttl_ms,omitempty"` +} + +func (x *CronBatchRequest) Reset() { + *x = CronBatchRequest{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CronBatchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CronBatchRequest) ProtoMessage() {} + +func (x *CronBatchRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[21] + 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 CronBatchRequest.ProtoReflect.Descriptor instead. +func (*CronBatchRequest) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{21} +} + +func (x *CronBatchRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *CronBatchRequest) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *CronBatchRequest) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + +func (x *CronBatchRequest) GetBatchSize() int32 { + if x != nil { + return x.BatchSize + } + return 0 +} + +func (x *CronBatchRequest) GetLockTtlMs() int64 { + if x != nil { + return x.LockTtlMs + } + return 0 +} + +// CronBatchResponse 返回一个批处理 run 的执行摘要。 +type CronBatchResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ClaimedCount int32 `protobuf:"varint,1,opt,name=claimed_count,json=claimedCount,proto3" json:"claimed_count,omitempty"` + ProcessedCount int32 `protobuf:"varint,2,opt,name=processed_count,json=processedCount,proto3" json:"processed_count,omitempty"` + SuccessCount int32 `protobuf:"varint,3,opt,name=success_count,json=successCount,proto3" json:"success_count,omitempty"` + FailureCount int32 `protobuf:"varint,4,opt,name=failure_count,json=failureCount,proto3" json:"failure_count,omitempty"` + HasMore bool `protobuf:"varint,5,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` +} + +func (x *CronBatchResponse) Reset() { + *x = CronBatchResponse{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CronBatchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CronBatchResponse) ProtoMessage() {} + +func (x *CronBatchResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[22] + 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 CronBatchResponse.ProtoReflect.Descriptor instead. +func (*CronBatchResponse) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{22} +} + +func (x *CronBatchResponse) GetClaimedCount() int32 { + if x != nil { + return x.ClaimedCount + } + return 0 +} + +func (x *CronBatchResponse) GetProcessedCount() int32 { + if x != nil { + return x.ProcessedCount + } + return 0 +} + +func (x *CronBatchResponse) GetSuccessCount() int32 { + if x != nil { + return x.SuccessCount + } + return 0 +} + +func (x *CronBatchResponse) GetFailureCount() int32 { + if x != nil { + return x.FailureCount + } + return 0 +} + +func (x *CronBatchResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +// TaskItem 是 App 每日任务/专属任务列表里的单个可展示任务快照。 +type TaskItem struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + TaskType string `protobuf:"bytes,2,opt,name=task_type,json=taskType,proto3" json:"task_type,omitempty"` + Category string `protobuf:"bytes,3,opt,name=category,proto3" json:"category,omitempty"` + MetricType string `protobuf:"bytes,4,opt,name=metric_type,json=metricType,proto3" json:"metric_type,omitempty"` + Title string `protobuf:"bytes,5,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` + TargetValue int64 `protobuf:"varint,7,opt,name=target_value,json=targetValue,proto3" json:"target_value,omitempty"` + TargetUnit string `protobuf:"bytes,8,opt,name=target_unit,json=targetUnit,proto3" json:"target_unit,omitempty"` + ProgressValue int64 `protobuf:"varint,9,opt,name=progress_value,json=progressValue,proto3" json:"progress_value,omitempty"` + RewardCoinAmount int64 `protobuf:"varint,10,opt,name=reward_coin_amount,json=rewardCoinAmount,proto3" json:"reward_coin_amount,omitempty"` + Status string `protobuf:"bytes,11,opt,name=status,proto3" json:"status,omitempty"` + Claimable bool `protobuf:"varint,12,opt,name=claimable,proto3" json:"claimable,omitempty"` + TaskDay string `protobuf:"bytes,13,opt,name=task_day,json=taskDay,proto3" json:"task_day,omitempty"` + ServerTimeMs int64 `protobuf:"varint,14,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"` + NextRefreshAtMs int64 `protobuf:"varint,15,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` + SortOrder int32 `protobuf:"varint,16,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + Version int64 `protobuf:"varint,17,opt,name=version,proto3" json:"version,omitempty"` +} + +func (x *TaskItem) Reset() { + *x = TaskItem{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskItem) ProtoMessage() {} + +func (x *TaskItem) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[23] + 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 TaskItem.ProtoReflect.Descriptor instead. +func (*TaskItem) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{23} +} + +func (x *TaskItem) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *TaskItem) GetTaskType() string { + if x != nil { + return x.TaskType + } + return "" +} + +func (x *TaskItem) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *TaskItem) GetMetricType() string { + if x != nil { + return x.MetricType + } + return "" +} + +func (x *TaskItem) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *TaskItem) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *TaskItem) GetTargetValue() int64 { + if x != nil { + return x.TargetValue + } + return 0 +} + +func (x *TaskItem) GetTargetUnit() string { + if x != nil { + return x.TargetUnit + } + return "" +} + +func (x *TaskItem) GetProgressValue() int64 { + if x != nil { + return x.ProgressValue + } + return 0 +} + +func (x *TaskItem) GetRewardCoinAmount() int64 { + if x != nil { + return x.RewardCoinAmount + } + return 0 +} + +func (x *TaskItem) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *TaskItem) GetClaimable() bool { + if x != nil { + return x.Claimable + } + return false +} + +func (x *TaskItem) GetTaskDay() string { + if x != nil { + return x.TaskDay + } + return "" +} + +func (x *TaskItem) GetServerTimeMs() int64 { + if x != nil { + return x.ServerTimeMs + } + return 0 +} + +func (x *TaskItem) GetNextRefreshAtMs() int64 { + if x != nil { + return x.NextRefreshAtMs + } + return 0 +} + +func (x *TaskItem) GetSortOrder() int32 { + if x != nil { + return x.SortOrder + } + return 0 +} + +func (x *TaskItem) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + +// TaskSection 固定表达 App 任务页的 daily/exclusive 两个分区。 +type TaskSection struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Section string `protobuf:"bytes,1,opt,name=section,proto3" json:"section,omitempty"` + Items []*TaskItem `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` + ServerTimeMs int64 `protobuf:"varint,3,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"` + NextRefreshAtMs int64 `protobuf:"varint,4,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` +} + +func (x *TaskSection) Reset() { + *x = TaskSection{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskSection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskSection) ProtoMessage() {} + +func (x *TaskSection) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[24] + 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 TaskSection.ProtoReflect.Descriptor instead. +func (*TaskSection) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{24} +} + +func (x *TaskSection) GetSection() string { + if x != nil { + return x.Section + } + return "" +} + +func (x *TaskSection) GetItems() []*TaskItem { + if x != nil { + return x.Items + } + return nil +} + +func (x *TaskSection) GetServerTimeMs() int64 { + if x != nil { + return x.ServerTimeMs + } + return 0 +} + +func (x *TaskSection) GetNextRefreshAtMs() int64 { + if x != nil { + return x.NextRefreshAtMs + } + return 0 +} + +// ListUserTasksRequest 查询当前用户任务进度;进度必须来自服务端事实事件。 +type ListUserTasksRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` +} + +func (x *ListUserTasksRequest) Reset() { + *x = ListUserTasksRequest{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUserTasksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUserTasksRequest) ProtoMessage() {} + +func (x *ListUserTasksRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[25] + 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 ListUserTasksRequest.ProtoReflect.Descriptor instead. +func (*ListUserTasksRequest) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{25} +} + +func (x *ListUserTasksRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *ListUserTasksRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +// ListUserTasksResponse 返回 daily/exclusive 分区和服务器刷新时间。 +type ListUserTasksResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Sections []*TaskSection `protobuf:"bytes,1,rep,name=sections,proto3" json:"sections,omitempty"` + ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"` + NextRefreshAtMs int64 `protobuf:"varint,3,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` +} + +func (x *ListUserTasksResponse) Reset() { + *x = ListUserTasksResponse{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUserTasksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUserTasksResponse) ProtoMessage() {} + +func (x *ListUserTasksResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[26] + 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 ListUserTasksResponse.ProtoReflect.Descriptor instead. +func (*ListUserTasksResponse) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{26} +} + +func (x *ListUserTasksResponse) GetSections() []*TaskSection { + if x != nil { + return x.Sections + } + return nil +} + +func (x *ListUserTasksResponse) GetServerTimeMs() int64 { + if x != nil { + return x.ServerTimeMs + } + return 0 +} + +func (x *ListUserTasksResponse) GetNextRefreshAtMs() int64 { + if x != nil { + return x.NextRefreshAtMs + } + return 0 +} + +// ClaimTaskRewardRequest 领取已完成任务奖励;command_id 是领奖幂等键。 +type ClaimTaskRewardRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + TaskId string `protobuf:"bytes,3,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + TaskType string `protobuf:"bytes,4,opt,name=task_type,json=taskType,proto3" json:"task_type,omitempty"` + TaskDay string `protobuf:"bytes,5,opt,name=task_day,json=taskDay,proto3" json:"task_day,omitempty"` + CommandId string `protobuf:"bytes,6,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"` +} + +func (x *ClaimTaskRewardRequest) Reset() { + *x = ClaimTaskRewardRequest{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClaimTaskRewardRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClaimTaskRewardRequest) ProtoMessage() {} + +func (x *ClaimTaskRewardRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[27] + 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 ClaimTaskRewardRequest.ProtoReflect.Descriptor instead. +func (*ClaimTaskRewardRequest) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{27} +} + +func (x *ClaimTaskRewardRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *ClaimTaskRewardRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *ClaimTaskRewardRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *ClaimTaskRewardRequest) GetTaskType() string { + if x != nil { + return x.TaskType + } + return "" +} + +func (x *ClaimTaskRewardRequest) GetTaskDay() string { + if x != nil { + return x.TaskDay + } + return "" +} + +func (x *ClaimTaskRewardRequest) GetCommandId() string { + if x != nil { + return x.CommandId + } + return "" +} + +// ClaimTaskRewardResponse 返回 activity 领奖状态和 wallet 入账回执。 +type ClaimTaskRewardResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ClaimId string `protobuf:"bytes,1,opt,name=claim_id,json=claimId,proto3" json:"claim_id,omitempty"` + TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + TaskType string `protobuf:"bytes,3,opt,name=task_type,json=taskType,proto3" json:"task_type,omitempty"` + TaskDay string `protobuf:"bytes,4,opt,name=task_day,json=taskDay,proto3" json:"task_day,omitempty"` + RewardCoinAmount int64 `protobuf:"varint,5,opt,name=reward_coin_amount,json=rewardCoinAmount,proto3" json:"reward_coin_amount,omitempty"` + Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"` + WalletTransactionId string `protobuf:"bytes,7,opt,name=wallet_transaction_id,json=walletTransactionId,proto3" json:"wallet_transaction_id,omitempty"` + GrantedAtMs int64 `protobuf:"varint,8,opt,name=granted_at_ms,json=grantedAtMs,proto3" json:"granted_at_ms,omitempty"` + Claimed bool `protobuf:"varint,9,opt,name=claimed,proto3" json:"claimed,omitempty"` +} + +func (x *ClaimTaskRewardResponse) Reset() { + *x = ClaimTaskRewardResponse{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClaimTaskRewardResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClaimTaskRewardResponse) ProtoMessage() {} + +func (x *ClaimTaskRewardResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[28] + 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 ClaimTaskRewardResponse.ProtoReflect.Descriptor instead. +func (*ClaimTaskRewardResponse) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{28} +} + +func (x *ClaimTaskRewardResponse) GetClaimId() string { + if x != nil { + return x.ClaimId + } + return "" +} + +func (x *ClaimTaskRewardResponse) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *ClaimTaskRewardResponse) GetTaskType() string { + if x != nil { + return x.TaskType + } + return "" +} + +func (x *ClaimTaskRewardResponse) GetTaskDay() string { + if x != nil { + return x.TaskDay + } + return "" +} + +func (x *ClaimTaskRewardResponse) GetRewardCoinAmount() int64 { + if x != nil { + return x.RewardCoinAmount + } + return 0 +} + +func (x *ClaimTaskRewardResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ClaimTaskRewardResponse) GetWalletTransactionId() string { + if x != nil { + return x.WalletTransactionId + } + return "" +} + +func (x *ClaimTaskRewardResponse) GetGrantedAtMs() int64 { + if x != nil { + return x.GrantedAtMs + } + return 0 +} + +func (x *ClaimTaskRewardResponse) GetClaimed() bool { + if x != nil { + return x.Claimed + } + return false +} + +// ConsumeTaskEventRequest 是异步 outbox consumer 投递给任务系统的事实事件。 +type ConsumeTaskEventRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + EventId string `protobuf:"bytes,2,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` + EventType string `protobuf:"bytes,3,opt,name=event_type,json=eventType,proto3" json:"event_type,omitempty"` + SourceService string `protobuf:"bytes,4,opt,name=source_service,json=sourceService,proto3" json:"source_service,omitempty"` + UserId int64 `protobuf:"varint,5,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + MetricType string `protobuf:"bytes,6,opt,name=metric_type,json=metricType,proto3" json:"metric_type,omitempty"` + Value int64 `protobuf:"varint,7,opt,name=value,proto3" json:"value,omitempty"` + OccurredAtMs int64 `protobuf:"varint,8,opt,name=occurred_at_ms,json=occurredAtMs,proto3" json:"occurred_at_ms,omitempty"` +} + +func (x *ConsumeTaskEventRequest) Reset() { + *x = ConsumeTaskEventRequest{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConsumeTaskEventRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConsumeTaskEventRequest) ProtoMessage() {} + +func (x *ConsumeTaskEventRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[29] + 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 ConsumeTaskEventRequest.ProtoReflect.Descriptor instead. +func (*ConsumeTaskEventRequest) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{29} +} + +func (x *ConsumeTaskEventRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *ConsumeTaskEventRequest) GetEventId() string { + if x != nil { + return x.EventId + } + return "" +} + +func (x *ConsumeTaskEventRequest) GetEventType() string { + if x != nil { + return x.EventType + } + return "" +} + +func (x *ConsumeTaskEventRequest) GetSourceService() string { + if x != nil { + return x.SourceService + } + return "" +} + +func (x *ConsumeTaskEventRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *ConsumeTaskEventRequest) GetMetricType() string { + if x != nil { + return x.MetricType + } + return "" +} + +func (x *ConsumeTaskEventRequest) GetValue() int64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *ConsumeTaskEventRequest) GetOccurredAtMs() int64 { + if x != nil { + return x.OccurredAtMs + } + return 0 +} + +// ConsumeTaskEventResponse 返回事件消费幂等结果;matched_task_count=0 表示合法跳过。 +type ConsumeTaskEventResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + EventId string `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + MatchedTaskCount int32 `protobuf:"varint,3,opt,name=matched_task_count,json=matchedTaskCount,proto3" json:"matched_task_count,omitempty"` +} + +func (x *ConsumeTaskEventResponse) Reset() { + *x = ConsumeTaskEventResponse{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConsumeTaskEventResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConsumeTaskEventResponse) ProtoMessage() {} + +func (x *ConsumeTaskEventResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[30] + 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 ConsumeTaskEventResponse.ProtoReflect.Descriptor instead. +func (*ConsumeTaskEventResponse) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{30} +} + +func (x *ConsumeTaskEventResponse) GetEventId() string { + if x != nil { + return x.EventId + } + return "" +} + +func (x *ConsumeTaskEventResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ConsumeTaskEventResponse) GetMatchedTaskCount() int32 { + if x != nil { + return x.MatchedTaskCount + } + return 0 +} + +// TaskDefinition 是后台管理任务配置的当前版本读模型。 +type TaskDefinition struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + TaskType string `protobuf:"bytes,2,opt,name=task_type,json=taskType,proto3" json:"task_type,omitempty"` + Category string `protobuf:"bytes,3,opt,name=category,proto3" json:"category,omitempty"` + MetricType string `protobuf:"bytes,4,opt,name=metric_type,json=metricType,proto3" json:"metric_type,omitempty"` + Title string `protobuf:"bytes,5,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` + TargetValue int64 `protobuf:"varint,7,opt,name=target_value,json=targetValue,proto3" json:"target_value,omitempty"` + TargetUnit string `protobuf:"bytes,8,opt,name=target_unit,json=targetUnit,proto3" json:"target_unit,omitempty"` + RewardCoinAmount int64 `protobuf:"varint,9,opt,name=reward_coin_amount,json=rewardCoinAmount,proto3" json:"reward_coin_amount,omitempty"` + Status string `protobuf:"bytes,10,opt,name=status,proto3" json:"status,omitempty"` + SortOrder int32 `protobuf:"varint,11,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + Version int64 `protobuf:"varint,12,opt,name=version,proto3" json:"version,omitempty"` + EffectiveFromMs int64 `protobuf:"varint,13,opt,name=effective_from_ms,json=effectiveFromMs,proto3" json:"effective_from_ms,omitempty"` + EffectiveToMs int64 `protobuf:"varint,14,opt,name=effective_to_ms,json=effectiveToMs,proto3" json:"effective_to_ms,omitempty"` + CreatedByAdminId int64 `protobuf:"varint,15,opt,name=created_by_admin_id,json=createdByAdminId,proto3" json:"created_by_admin_id,omitempty"` + UpdatedByAdminId int64 `protobuf:"varint,16,opt,name=updated_by_admin_id,json=updatedByAdminId,proto3" json:"updated_by_admin_id,omitempty"` + CreatedAtMs int64 `protobuf:"varint,17,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` + UpdatedAtMs int64 `protobuf:"varint,18,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` +} + +func (x *TaskDefinition) Reset() { + *x = TaskDefinition{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskDefinition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskDefinition) ProtoMessage() {} + +func (x *TaskDefinition) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[31] + 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 TaskDefinition.ProtoReflect.Descriptor instead. +func (*TaskDefinition) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{31} +} + +func (x *TaskDefinition) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *TaskDefinition) GetTaskType() string { + if x != nil { + return x.TaskType + } + return "" +} + +func (x *TaskDefinition) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *TaskDefinition) GetMetricType() string { + if x != nil { + return x.MetricType + } + return "" +} + +func (x *TaskDefinition) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *TaskDefinition) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *TaskDefinition) GetTargetValue() int64 { + if x != nil { + return x.TargetValue + } + return 0 +} + +func (x *TaskDefinition) GetTargetUnit() string { + if x != nil { + return x.TargetUnit + } + return "" +} + +func (x *TaskDefinition) GetRewardCoinAmount() int64 { + if x != nil { + return x.RewardCoinAmount + } + return 0 +} + +func (x *TaskDefinition) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *TaskDefinition) GetSortOrder() int32 { + if x != nil { + return x.SortOrder + } + return 0 +} + +func (x *TaskDefinition) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *TaskDefinition) GetEffectiveFromMs() int64 { + if x != nil { + return x.EffectiveFromMs + } + return 0 +} + +func (x *TaskDefinition) GetEffectiveToMs() int64 { + if x != nil { + return x.EffectiveToMs + } + return 0 +} + +func (x *TaskDefinition) GetCreatedByAdminId() int64 { + if x != nil { + return x.CreatedByAdminId + } + return 0 +} + +func (x *TaskDefinition) GetUpdatedByAdminId() int64 { + if x != nil { + return x.UpdatedByAdminId + } + return 0 +} + +func (x *TaskDefinition) GetCreatedAtMs() int64 { + if x != nil { + return x.CreatedAtMs + } + return 0 +} + +func (x *TaskDefinition) GetUpdatedAtMs() int64 { + if x != nil { + return x.UpdatedAtMs + } + return 0 +} + +// ListTaskDefinitionsRequest 是后台任务配置列表筛选条件。 +type ListTaskDefinitionsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + TaskType string `protobuf:"bytes,2,opt,name=task_type,json=taskType,proto3" json:"task_type,omitempty"` + Category string `protobuf:"bytes,3,opt,name=category,proto3" json:"category,omitempty"` + Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` + Keyword string `protobuf:"bytes,5,opt,name=keyword,proto3" json:"keyword,omitempty"` + Page int32 `protobuf:"varint,6,opt,name=page,proto3" json:"page,omitempty"` + PageSize int32 `protobuf:"varint,7,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` +} + +func (x *ListTaskDefinitionsRequest) Reset() { + *x = ListTaskDefinitionsRequest{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTaskDefinitionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTaskDefinitionsRequest) ProtoMessage() {} + +func (x *ListTaskDefinitionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[32] + 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 ListTaskDefinitionsRequest.ProtoReflect.Descriptor instead. +func (*ListTaskDefinitionsRequest) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{32} +} + +func (x *ListTaskDefinitionsRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *ListTaskDefinitionsRequest) GetTaskType() string { + if x != nil { + return x.TaskType + } + return "" +} + +func (x *ListTaskDefinitionsRequest) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *ListTaskDefinitionsRequest) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ListTaskDefinitionsRequest) GetKeyword() string { + if x != nil { + return x.Keyword + } + return "" +} + +func (x *ListTaskDefinitionsRequest) GetPage() int32 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ListTaskDefinitionsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +type ListTaskDefinitionsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tasks []*TaskDefinition `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` +} + +func (x *ListTaskDefinitionsResponse) Reset() { + *x = ListTaskDefinitionsResponse{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTaskDefinitionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTaskDefinitionsResponse) ProtoMessage() {} + +func (x *ListTaskDefinitionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[33] + 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 ListTaskDefinitionsResponse.ProtoReflect.Descriptor instead. +func (*ListTaskDefinitionsResponse) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{33} +} + +func (x *ListTaskDefinitionsResponse) GetTasks() []*TaskDefinition { + if x != nil { + return x.Tasks + } + return nil +} + +func (x *ListTaskDefinitionsResponse) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +// UpsertTaskDefinitionRequest 创建或编辑任务;active 关键字段变更由服务端生成新版本。 +type UpsertTaskDefinitionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + TaskType string `protobuf:"bytes,3,opt,name=task_type,json=taskType,proto3" json:"task_type,omitempty"` + Category string `protobuf:"bytes,4,opt,name=category,proto3" json:"category,omitempty"` + MetricType string `protobuf:"bytes,5,opt,name=metric_type,json=metricType,proto3" json:"metric_type,omitempty"` + Title string `protobuf:"bytes,6,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,7,opt,name=description,proto3" json:"description,omitempty"` + TargetValue int64 `protobuf:"varint,8,opt,name=target_value,json=targetValue,proto3" json:"target_value,omitempty"` + TargetUnit string `protobuf:"bytes,9,opt,name=target_unit,json=targetUnit,proto3" json:"target_unit,omitempty"` + RewardCoinAmount int64 `protobuf:"varint,10,opt,name=reward_coin_amount,json=rewardCoinAmount,proto3" json:"reward_coin_amount,omitempty"` + Status string `protobuf:"bytes,11,opt,name=status,proto3" json:"status,omitempty"` + SortOrder int32 `protobuf:"varint,12,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + EffectiveFromMs int64 `protobuf:"varint,13,opt,name=effective_from_ms,json=effectiveFromMs,proto3" json:"effective_from_ms,omitempty"` + EffectiveToMs int64 `protobuf:"varint,14,opt,name=effective_to_ms,json=effectiveToMs,proto3" json:"effective_to_ms,omitempty"` + OperatorAdminId int64 `protobuf:"varint,15,opt,name=operator_admin_id,json=operatorAdminId,proto3" json:"operator_admin_id,omitempty"` +} + +func (x *UpsertTaskDefinitionRequest) Reset() { + *x = UpsertTaskDefinitionRequest{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpsertTaskDefinitionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpsertTaskDefinitionRequest) ProtoMessage() {} + +func (x *UpsertTaskDefinitionRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[34] + 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 UpsertTaskDefinitionRequest.ProtoReflect.Descriptor instead. +func (*UpsertTaskDefinitionRequest) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{34} +} + +func (x *UpsertTaskDefinitionRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *UpsertTaskDefinitionRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *UpsertTaskDefinitionRequest) GetTaskType() string { + if x != nil { + return x.TaskType + } + return "" +} + +func (x *UpsertTaskDefinitionRequest) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *UpsertTaskDefinitionRequest) GetMetricType() string { + if x != nil { + return x.MetricType + } + return "" +} + +func (x *UpsertTaskDefinitionRequest) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *UpsertTaskDefinitionRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *UpsertTaskDefinitionRequest) GetTargetValue() int64 { + if x != nil { + return x.TargetValue + } + return 0 +} + +func (x *UpsertTaskDefinitionRequest) GetTargetUnit() string { + if x != nil { + return x.TargetUnit + } + return "" +} + +func (x *UpsertTaskDefinitionRequest) GetRewardCoinAmount() int64 { + if x != nil { + return x.RewardCoinAmount + } + return 0 +} + +func (x *UpsertTaskDefinitionRequest) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *UpsertTaskDefinitionRequest) GetSortOrder() int32 { + if x != nil { + return x.SortOrder + } + return 0 +} + +func (x *UpsertTaskDefinitionRequest) GetEffectiveFromMs() int64 { + if x != nil { + return x.EffectiveFromMs + } + return 0 +} + +func (x *UpsertTaskDefinitionRequest) GetEffectiveToMs() int64 { + if x != nil { + return x.EffectiveToMs + } + return 0 +} + +func (x *UpsertTaskDefinitionRequest) GetOperatorAdminId() int64 { + if x != nil { + return x.OperatorAdminId + } + return 0 +} + +type UpsertTaskDefinitionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Task *TaskDefinition `protobuf:"bytes,1,opt,name=task,proto3" json:"task,omitempty"` + Created bool `protobuf:"varint,2,opt,name=created,proto3" json:"created,omitempty"` +} + +func (x *UpsertTaskDefinitionResponse) Reset() { + *x = UpsertTaskDefinitionResponse{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpsertTaskDefinitionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpsertTaskDefinitionResponse) ProtoMessage() {} + +func (x *UpsertTaskDefinitionResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[35] + 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 UpsertTaskDefinitionResponse.ProtoReflect.Descriptor instead. +func (*UpsertTaskDefinitionResponse) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{35} +} + +func (x *UpsertTaskDefinitionResponse) GetTask() *TaskDefinition { + if x != nil { + return x.Task + } + return nil +} + +func (x *UpsertTaskDefinitionResponse) GetCreated() bool { + if x != nil { + return x.Created + } + return false +} + +// SetTaskDefinitionStatusRequest 后台启停或归档任务,不做物理删除。 +type SetTaskDefinitionStatusRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + TaskId string `protobuf:"bytes,2,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + OperatorAdminId int64 `protobuf:"varint,4,opt,name=operator_admin_id,json=operatorAdminId,proto3" json:"operator_admin_id,omitempty"` +} + +func (x *SetTaskDefinitionStatusRequest) Reset() { + *x = SetTaskDefinitionStatusRequest{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetTaskDefinitionStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetTaskDefinitionStatusRequest) ProtoMessage() {} + +func (x *SetTaskDefinitionStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[36] + 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 SetTaskDefinitionStatusRequest.ProtoReflect.Descriptor instead. +func (*SetTaskDefinitionStatusRequest) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{36} +} + +func (x *SetTaskDefinitionStatusRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *SetTaskDefinitionStatusRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *SetTaskDefinitionStatusRequest) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *SetTaskDefinitionStatusRequest) GetOperatorAdminId() int64 { + if x != nil { + return x.OperatorAdminId + } + return 0 +} + +type SetTaskDefinitionStatusResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Task *TaskDefinition `protobuf:"bytes,1,opt,name=task,proto3" json:"task,omitempty"` +} + +func (x *SetTaskDefinitionStatusResponse) Reset() { + *x = SetTaskDefinitionStatusResponse{} + mi := &file_proto_activity_v1_activity_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetTaskDefinitionStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetTaskDefinitionStatusResponse) ProtoMessage() {} + +func (x *SetTaskDefinitionStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_activity_v1_activity_proto_msgTypes[37] + 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 SetTaskDefinitionStatusResponse.ProtoReflect.Descriptor instead. +func (*SetTaskDefinitionStatusResponse) Descriptor() ([]byte, []int) { + return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{37} +} + +func (x *SetTaskDefinitionStatusResponse) GetTask() *TaskDefinition { + if x != nil { + return x.Task + } + return nil +} + var File_proto_activity_v1_activity_proto protoreflect.FileDescriptor var file_proto_activity_v1_activity_proto_rawDesc = []byte{ @@ -1745,76 +3276,395 @@ var file_proto_activity_v1_activity_proto_rawDesc = []byte{ 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, - 0x32, 0xe2, 0x01, 0x0a, 0x0f, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x41, 0x63, 0x74, 0x69, - 0x76, 0x69, 0x74, 0x79, 0x12, 0x26, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, - 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x41, 0x63, 0x74, - 0x69, 0x76, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x68, + 0x22, 0xb9, 0x01, 0x0a, 0x10, 0x43, 0x72, 0x6f, 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x15, 0x0a, 0x06, 0x72, 0x75, 0x6e, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x75, 0x6e, 0x49, 0x64, + 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, + 0x0a, 0x62, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x09, 0x62, 0x61, 0x74, 0x63, 0x68, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1e, 0x0a, 0x0b, + 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x74, 0x6c, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x74, 0x6c, 0x4d, 0x73, 0x22, 0xc6, 0x01, 0x0a, + 0x11, 0x43, 0x72, 0x6f, 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x65, 0x64, 0x5f, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x63, 0x6c, 0x61, 0x69, 0x6d, + 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x66, 0x61, + 0x69, 0x6c, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, + 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, + 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0xab, 0x04, 0x0a, 0x08, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x74, + 0x65, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x74, + 0x61, 0x73, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x74, 0x61, 0x73, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, + 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, + 0x67, 0x6f, 0x72, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x72, 0x69, + 0x63, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 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, 0x21, 0x0a, + 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x6e, 0x69, + 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x67, 0x72, + 0x65, 0x73, 0x73, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x72, 0x65, 0x77, 0x61, + 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, + 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, + 0x0a, 0x09, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x19, 0x0a, 0x08, + 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x64, 0x61, 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x74, 0x61, 0x73, 0x6b, 0x44, 0x61, 0x79, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x2b, 0x0a, + 0x12, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x61, 0x74, + 0x5f, 0x6d, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x52, + 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x41, 0x74, 0x4d, 0x73, 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, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x11, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x22, 0xad, 0x01, 0x0a, 0x0b, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x0a, + 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, - 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6e, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, - 0x76, 0x69, 0x74, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2b, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, - 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, - 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xb1, 0x06, 0x0a, 0x13, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x68, 0x0a, - 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x61, 0x62, 0x73, - 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, - 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x54, 0x61, 0x62, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, + 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, + 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x2b, 0x0a, 0x12, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x72, + 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x41, + 0x74, 0x4d, 0x73, 0x22, 0x63, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x54, + 0x61, 0x73, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x04, 0x6d, + 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, + 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xa6, 0x01, 0x0a, 0x15, 0x4c, 0x69, 0x73, + 0x74, 0x55, 0x73, 0x65, 0x72, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x08, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, + 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x73, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x24, + 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x54, 0x69, + 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x2b, 0x0a, 0x12, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x72, 0x65, 0x66, + 0x72, 0x65, 0x73, 0x68, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x41, 0x74, 0x4d, + 0x73, 0x22, 0xd5, 0x01, 0x0a, 0x16, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x52, + 0x65, 0x77, 0x61, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x04, + 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, + 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, + 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, + 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, + 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x73, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x19, 0x0a, 0x08, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x64, 0x61, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x44, 0x61, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x22, 0xbd, 0x02, 0x0a, 0x17, 0x43, 0x6c, + 0x61, 0x69, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x49, 0x64, + 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x73, + 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, + 0x73, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x64, + 0x61, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x44, 0x61, + 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, + 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x72, + 0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 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, 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, 0x07, 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, 0x22, 0x0a, 0x0d, 0x67, + 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, + 0x18, 0x0a, 0x07, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x65, 0x64, 0x22, 0xa4, 0x02, 0x0a, 0x17, 0x43, 0x6f, + 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x61, 0x73, 0x6b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x63, + 0x63, 0x75, 0x72, 0x72, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x63, 0x63, 0x75, 0x72, 0x72, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, + 0x22, 0x7b, 0x0a, 0x18, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x61, 0x73, 0x6b, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, + 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x65, 0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x10, 0x6d, 0x61, 0x74, + 0x63, 0x68, 0x65, 0x64, 0x54, 0x61, 0x73, 0x6b, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xf8, 0x04, + 0x0a, 0x0e, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x73, + 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, + 0x73, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, + 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, + 0x72, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 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, 0x21, 0x0a, 0x0c, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, + 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x6e, 0x69, 0x74, 0x12, + 0x2c, 0x0a, 0x12, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x61, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x72, 0x65, 0x77, + 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, + 0x72, 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2a, + 0x0a, 0x11, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x66, 0x72, 0x6f, 0x6d, + 0x5f, 0x6d, 0x73, 0x18, 0x0d, 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, 0x0e, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 0x6f, + 0x4d, 0x73, 0x12, 0x2d, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, + 0x5f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x10, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x49, + 0x64, 0x12, 0x2d, 0x0a, 0x13, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x5f, + 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x49, 0x64, + 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, + 0x73, 0x18, 0x11, 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, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xec, 0x01, 0x0a, 0x1a, 0x4c, 0x69, 0x73, + 0x74, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x74, + 0x61, 0x73, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x74, 0x61, 0x73, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, + 0x67, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, + 0x67, 0x6f, 0x72, 0x79, 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, 0x22, 0x6c, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x54, + 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, + 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x12, + 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0xa5, 0x04, 0x0a, 0x1b, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, + 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, + 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, + 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x73, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, + 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, + 0x6c, 0x65, 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, 0x21, 0x0a, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x61, 0x72, 0x67, + 0x65, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x61, + 0x72, 0x67, 0x65, 0x74, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x72, 0x65, 0x77, 0x61, + 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, + 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1d, + 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x09, 0x73, 0x6f, 0x72, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x2a, 0x0a, + 0x11, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, + 0x6d, 0x73, 0x18, 0x0d, 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, 0x0e, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x54, 0x6f, 0x4d, + 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x61, 0x64, + 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x49, 0x64, 0x22, 0x6f, 0x0a, + 0x1c, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, + 0x04, 0x74, 0x61, 0x73, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x61, 0x62, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6e, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x49, - 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x2e, 0x68, + 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x04, + 0x74, 0x61, 0x73, 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x22, 0xb1, + 0x01, 0x0a, 0x1e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x32, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, + 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, + 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 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, 0x2a, 0x0a, 0x11, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x6f, 0x72, 0x5f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, + 0x49, 0x64, 0x22, 0x58, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, + 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, + 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x32, 0xe2, 0x01, 0x0a, + 0x0f, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x5f, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, + 0x12, 0x26, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, + 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x69, 0x6e, + 0x67, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x6e, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, + 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, + 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x74, 0x69, 0x76, + 0x69, 0x74, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x32, 0xb1, 0x06, 0x0a, 0x13, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x62, + 0x6f, 0x78, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x68, 0x0a, 0x0f, 0x4c, 0x69, 0x73, + 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x61, 0x62, 0x73, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x14, 0x4d, 0x61, 0x72, 0x6b, 0x49, - 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x61, 0x64, 0x12, - 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, - 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, - 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x77, 0x0a, 0x14, 0x4d, 0x61, 0x72, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x64, 0x12, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, - 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x72, - 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, - 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, - 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x72, - 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, - 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x71, 0x0a, 0x12, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, - 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, - 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, - 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x71, 0x0a, 0x12, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x12, 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, - 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x62, - 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, - 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x78, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x68, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x46, 0x61, 0x6e, 0x6f, 0x75, 0x74, 0x4a, - 0x6f, 0x62, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, - 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x46, 0x61, 0x6e, - 0x6f, 0x75, 0x74, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, - 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x46, 0x61, 0x6e, 0x6f, 0x75, 0x74, 0x4a, 0x6f, - 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2e, 0x5a, 0x2c, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2f, 0x76, 0x31, 0x3b, 0x61, - 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x61, 0x62, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54, 0x61, 0x62, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x6e, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x62, 0x6f, 0x78, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, + 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x14, 0x4d, 0x61, 0x72, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x78, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x61, 0x64, 0x12, 0x2e, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x4d, 0x61, 0x72, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x4d, 0x61, 0x72, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x14, + 0x4d, 0x61, 0x72, 0x6b, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x61, 0x64, 0x12, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, + 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x49, 0x6e, 0x62, + 0x6f, 0x78, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, + 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x49, 0x6e, 0x62, + 0x6f, 0x78, 0x53, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x71, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, + 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2c, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x71, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2c, + 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x68, + 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x62, 0x6f, 0x78, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x0f, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x46, 0x61, 0x6e, 0x6f, 0x75, 0x74, 0x4a, 0x6f, 0x62, 0x12, 0x29, + 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x46, 0x61, 0x6e, 0x6f, 0x75, 0x74, 0x4a, + 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x46, 0x61, 0x6e, 0x6f, 0x75, 0x74, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x7d, 0x0a, 0x13, 0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, + 0x79, 0x43, 0x72, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x66, 0x0a, 0x19, + 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x46, 0x61, + 0x6e, 0x6f, 0x75, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, + 0x6f, 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, + 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x72, 0x6f, 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xc8, 0x02, 0x0a, 0x0b, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x62, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x27, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, + 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, + 0x65, 0x72, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, + 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, + 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x54, 0x61, 0x73, 0x6b, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x0f, 0x43, 0x6c, 0x61, 0x69, + 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x12, 0x29, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, + 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x61, 0x69, 0x6d, + 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x6b, 0x0a, 0x10, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x61, 0x73, + 0x6b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, + 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, + 0x6d, 0x65, 0x54, 0x61, 0x73, 0x6b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, + 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x61, + 0x73, 0x6b, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, + 0x84, 0x03, 0x0a, 0x10, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x54, 0x61, 0x73, 0x6b, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x74, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x61, 0x73, 0x6b, + 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2d, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x68, 0x79, 0x61, + 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x14, 0x55, 0x70, + 0x73, 0x65, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, + 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x54, 0x61, 0x73, + 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, + 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x54, 0x61, 0x73, + 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x44, + 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x31, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, 0x66, 0x69, 0x6e, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x76, + 0x69, 0x74, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x44, 0x65, + 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2e, 0x5a, 0x2c, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x69, 0x74, 0x79, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1829,29 +3679,46 @@ func file_proto_activity_v1_activity_proto_rawDescGZIP() []byte { return file_proto_activity_v1_activity_proto_rawDescData } -var file_proto_activity_v1_activity_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_proto_activity_v1_activity_proto_msgTypes = make([]protoimpl.MessageInfo, 38) var file_proto_activity_v1_activity_proto_goTypes = []any{ - (*RequestMeta)(nil), // 0: hyapp.activity.v1.RequestMeta - (*PingActivityRequest)(nil), // 1: hyapp.activity.v1.PingActivityRequest - (*PingActivityResponse)(nil), // 2: hyapp.activity.v1.PingActivityResponse - (*GetActivityStatusRequest)(nil), // 3: hyapp.activity.v1.GetActivityStatusRequest - (*GetActivityStatusResponse)(nil), // 4: hyapp.activity.v1.GetActivityStatusResponse - (*MessageTabSection)(nil), // 5: hyapp.activity.v1.MessageTabSection - (*ListMessageTabsRequest)(nil), // 6: hyapp.activity.v1.ListMessageTabsRequest - (*ListMessageTabsResponse)(nil), // 7: hyapp.activity.v1.ListMessageTabsResponse - (*InboxMessage)(nil), // 8: hyapp.activity.v1.InboxMessage - (*ListInboxMessagesRequest)(nil), // 9: hyapp.activity.v1.ListInboxMessagesRequest - (*ListInboxMessagesResponse)(nil), // 10: hyapp.activity.v1.ListInboxMessagesResponse - (*MarkInboxMessageReadRequest)(nil), // 11: hyapp.activity.v1.MarkInboxMessageReadRequest - (*MarkInboxMessageReadResponse)(nil), // 12: hyapp.activity.v1.MarkInboxMessageReadResponse - (*MarkInboxSectionReadRequest)(nil), // 13: hyapp.activity.v1.MarkInboxSectionReadRequest - (*MarkInboxSectionReadResponse)(nil), // 14: hyapp.activity.v1.MarkInboxSectionReadResponse - (*DeleteInboxMessageRequest)(nil), // 15: hyapp.activity.v1.DeleteInboxMessageRequest - (*DeleteInboxMessageResponse)(nil), // 16: hyapp.activity.v1.DeleteInboxMessageResponse - (*CreateInboxMessageRequest)(nil), // 17: hyapp.activity.v1.CreateInboxMessageRequest - (*CreateInboxMessageResponse)(nil), // 18: hyapp.activity.v1.CreateInboxMessageResponse - (*CreateFanoutJobRequest)(nil), // 19: hyapp.activity.v1.CreateFanoutJobRequest - (*CreateFanoutJobResponse)(nil), // 20: hyapp.activity.v1.CreateFanoutJobResponse + (*RequestMeta)(nil), // 0: hyapp.activity.v1.RequestMeta + (*PingActivityRequest)(nil), // 1: hyapp.activity.v1.PingActivityRequest + (*PingActivityResponse)(nil), // 2: hyapp.activity.v1.PingActivityResponse + (*GetActivityStatusRequest)(nil), // 3: hyapp.activity.v1.GetActivityStatusRequest + (*GetActivityStatusResponse)(nil), // 4: hyapp.activity.v1.GetActivityStatusResponse + (*MessageTabSection)(nil), // 5: hyapp.activity.v1.MessageTabSection + (*ListMessageTabsRequest)(nil), // 6: hyapp.activity.v1.ListMessageTabsRequest + (*ListMessageTabsResponse)(nil), // 7: hyapp.activity.v1.ListMessageTabsResponse + (*InboxMessage)(nil), // 8: hyapp.activity.v1.InboxMessage + (*ListInboxMessagesRequest)(nil), // 9: hyapp.activity.v1.ListInboxMessagesRequest + (*ListInboxMessagesResponse)(nil), // 10: hyapp.activity.v1.ListInboxMessagesResponse + (*MarkInboxMessageReadRequest)(nil), // 11: hyapp.activity.v1.MarkInboxMessageReadRequest + (*MarkInboxMessageReadResponse)(nil), // 12: hyapp.activity.v1.MarkInboxMessageReadResponse + (*MarkInboxSectionReadRequest)(nil), // 13: hyapp.activity.v1.MarkInboxSectionReadRequest + (*MarkInboxSectionReadResponse)(nil), // 14: hyapp.activity.v1.MarkInboxSectionReadResponse + (*DeleteInboxMessageRequest)(nil), // 15: hyapp.activity.v1.DeleteInboxMessageRequest + (*DeleteInboxMessageResponse)(nil), // 16: hyapp.activity.v1.DeleteInboxMessageResponse + (*CreateInboxMessageRequest)(nil), // 17: hyapp.activity.v1.CreateInboxMessageRequest + (*CreateInboxMessageResponse)(nil), // 18: hyapp.activity.v1.CreateInboxMessageResponse + (*CreateFanoutJobRequest)(nil), // 19: hyapp.activity.v1.CreateFanoutJobRequest + (*CreateFanoutJobResponse)(nil), // 20: hyapp.activity.v1.CreateFanoutJobResponse + (*CronBatchRequest)(nil), // 21: hyapp.activity.v1.CronBatchRequest + (*CronBatchResponse)(nil), // 22: hyapp.activity.v1.CronBatchResponse + (*TaskItem)(nil), // 23: hyapp.activity.v1.TaskItem + (*TaskSection)(nil), // 24: hyapp.activity.v1.TaskSection + (*ListUserTasksRequest)(nil), // 25: hyapp.activity.v1.ListUserTasksRequest + (*ListUserTasksResponse)(nil), // 26: hyapp.activity.v1.ListUserTasksResponse + (*ClaimTaskRewardRequest)(nil), // 27: hyapp.activity.v1.ClaimTaskRewardRequest + (*ClaimTaskRewardResponse)(nil), // 28: hyapp.activity.v1.ClaimTaskRewardResponse + (*ConsumeTaskEventRequest)(nil), // 29: hyapp.activity.v1.ConsumeTaskEventRequest + (*ConsumeTaskEventResponse)(nil), // 30: hyapp.activity.v1.ConsumeTaskEventResponse + (*TaskDefinition)(nil), // 31: hyapp.activity.v1.TaskDefinition + (*ListTaskDefinitionsRequest)(nil), // 32: hyapp.activity.v1.ListTaskDefinitionsRequest + (*ListTaskDefinitionsResponse)(nil), // 33: hyapp.activity.v1.ListTaskDefinitionsResponse + (*UpsertTaskDefinitionRequest)(nil), // 34: hyapp.activity.v1.UpsertTaskDefinitionRequest + (*UpsertTaskDefinitionResponse)(nil), // 35: hyapp.activity.v1.UpsertTaskDefinitionResponse + (*SetTaskDefinitionStatusRequest)(nil), // 36: hyapp.activity.v1.SetTaskDefinitionStatusRequest + (*SetTaskDefinitionStatusResponse)(nil), // 37: hyapp.activity.v1.SetTaskDefinitionStatusResponse } var file_proto_activity_v1_activity_proto_depIdxs = []int32{ 0, // 0: hyapp.activity.v1.PingActivityRequest.meta:type_name -> hyapp.activity.v1.RequestMeta @@ -1865,29 +3732,55 @@ var file_proto_activity_v1_activity_proto_depIdxs = []int32{ 0, // 8: hyapp.activity.v1.DeleteInboxMessageRequest.meta:type_name -> hyapp.activity.v1.RequestMeta 0, // 9: hyapp.activity.v1.CreateInboxMessageRequest.meta:type_name -> hyapp.activity.v1.RequestMeta 0, // 10: hyapp.activity.v1.CreateFanoutJobRequest.meta:type_name -> hyapp.activity.v1.RequestMeta - 1, // 11: hyapp.activity.v1.ActivityService.PingActivity:input_type -> hyapp.activity.v1.PingActivityRequest - 3, // 12: hyapp.activity.v1.ActivityService.GetActivityStatus:input_type -> hyapp.activity.v1.GetActivityStatusRequest - 6, // 13: hyapp.activity.v1.MessageInboxService.ListMessageTabs:input_type -> hyapp.activity.v1.ListMessageTabsRequest - 9, // 14: hyapp.activity.v1.MessageInboxService.ListInboxMessages:input_type -> hyapp.activity.v1.ListInboxMessagesRequest - 11, // 15: hyapp.activity.v1.MessageInboxService.MarkInboxMessageRead:input_type -> hyapp.activity.v1.MarkInboxMessageReadRequest - 13, // 16: hyapp.activity.v1.MessageInboxService.MarkInboxSectionRead:input_type -> hyapp.activity.v1.MarkInboxSectionReadRequest - 15, // 17: hyapp.activity.v1.MessageInboxService.DeleteInboxMessage:input_type -> hyapp.activity.v1.DeleteInboxMessageRequest - 17, // 18: hyapp.activity.v1.MessageInboxService.CreateInboxMessage:input_type -> hyapp.activity.v1.CreateInboxMessageRequest - 19, // 19: hyapp.activity.v1.MessageInboxService.CreateFanoutJob:input_type -> hyapp.activity.v1.CreateFanoutJobRequest - 2, // 20: hyapp.activity.v1.ActivityService.PingActivity:output_type -> hyapp.activity.v1.PingActivityResponse - 4, // 21: hyapp.activity.v1.ActivityService.GetActivityStatus:output_type -> hyapp.activity.v1.GetActivityStatusResponse - 7, // 22: hyapp.activity.v1.MessageInboxService.ListMessageTabs:output_type -> hyapp.activity.v1.ListMessageTabsResponse - 10, // 23: hyapp.activity.v1.MessageInboxService.ListInboxMessages:output_type -> hyapp.activity.v1.ListInboxMessagesResponse - 12, // 24: hyapp.activity.v1.MessageInboxService.MarkInboxMessageRead:output_type -> hyapp.activity.v1.MarkInboxMessageReadResponse - 14, // 25: hyapp.activity.v1.MessageInboxService.MarkInboxSectionRead:output_type -> hyapp.activity.v1.MarkInboxSectionReadResponse - 16, // 26: hyapp.activity.v1.MessageInboxService.DeleteInboxMessage:output_type -> hyapp.activity.v1.DeleteInboxMessageResponse - 18, // 27: hyapp.activity.v1.MessageInboxService.CreateInboxMessage:output_type -> hyapp.activity.v1.CreateInboxMessageResponse - 20, // 28: hyapp.activity.v1.MessageInboxService.CreateFanoutJob:output_type -> hyapp.activity.v1.CreateFanoutJobResponse - 20, // [20:29] is the sub-list for method output_type - 11, // [11:20] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 0, // 11: hyapp.activity.v1.CronBatchRequest.meta:type_name -> hyapp.activity.v1.RequestMeta + 23, // 12: hyapp.activity.v1.TaskSection.items:type_name -> hyapp.activity.v1.TaskItem + 0, // 13: hyapp.activity.v1.ListUserTasksRequest.meta:type_name -> hyapp.activity.v1.RequestMeta + 24, // 14: hyapp.activity.v1.ListUserTasksResponse.sections:type_name -> hyapp.activity.v1.TaskSection + 0, // 15: hyapp.activity.v1.ClaimTaskRewardRequest.meta:type_name -> hyapp.activity.v1.RequestMeta + 0, // 16: hyapp.activity.v1.ConsumeTaskEventRequest.meta:type_name -> hyapp.activity.v1.RequestMeta + 0, // 17: hyapp.activity.v1.ListTaskDefinitionsRequest.meta:type_name -> hyapp.activity.v1.RequestMeta + 31, // 18: hyapp.activity.v1.ListTaskDefinitionsResponse.tasks:type_name -> hyapp.activity.v1.TaskDefinition + 0, // 19: hyapp.activity.v1.UpsertTaskDefinitionRequest.meta:type_name -> hyapp.activity.v1.RequestMeta + 31, // 20: hyapp.activity.v1.UpsertTaskDefinitionResponse.task:type_name -> hyapp.activity.v1.TaskDefinition + 0, // 21: hyapp.activity.v1.SetTaskDefinitionStatusRequest.meta:type_name -> hyapp.activity.v1.RequestMeta + 31, // 22: hyapp.activity.v1.SetTaskDefinitionStatusResponse.task:type_name -> hyapp.activity.v1.TaskDefinition + 1, // 23: hyapp.activity.v1.ActivityService.PingActivity:input_type -> hyapp.activity.v1.PingActivityRequest + 3, // 24: hyapp.activity.v1.ActivityService.GetActivityStatus:input_type -> hyapp.activity.v1.GetActivityStatusRequest + 6, // 25: hyapp.activity.v1.MessageInboxService.ListMessageTabs:input_type -> hyapp.activity.v1.ListMessageTabsRequest + 9, // 26: hyapp.activity.v1.MessageInboxService.ListInboxMessages:input_type -> hyapp.activity.v1.ListInboxMessagesRequest + 11, // 27: hyapp.activity.v1.MessageInboxService.MarkInboxMessageRead:input_type -> hyapp.activity.v1.MarkInboxMessageReadRequest + 13, // 28: hyapp.activity.v1.MessageInboxService.MarkInboxSectionRead:input_type -> hyapp.activity.v1.MarkInboxSectionReadRequest + 15, // 29: hyapp.activity.v1.MessageInboxService.DeleteInboxMessage:input_type -> hyapp.activity.v1.DeleteInboxMessageRequest + 17, // 30: hyapp.activity.v1.MessageInboxService.CreateInboxMessage:input_type -> hyapp.activity.v1.CreateInboxMessageRequest + 19, // 31: hyapp.activity.v1.MessageInboxService.CreateFanoutJob:input_type -> hyapp.activity.v1.CreateFanoutJobRequest + 21, // 32: hyapp.activity.v1.ActivityCronService.ProcessMessageFanoutBatch:input_type -> hyapp.activity.v1.CronBatchRequest + 25, // 33: hyapp.activity.v1.TaskService.ListUserTasks:input_type -> hyapp.activity.v1.ListUserTasksRequest + 27, // 34: hyapp.activity.v1.TaskService.ClaimTaskReward:input_type -> hyapp.activity.v1.ClaimTaskRewardRequest + 29, // 35: hyapp.activity.v1.TaskService.ConsumeTaskEvent:input_type -> hyapp.activity.v1.ConsumeTaskEventRequest + 32, // 36: hyapp.activity.v1.AdminTaskService.ListTaskDefinitions:input_type -> hyapp.activity.v1.ListTaskDefinitionsRequest + 34, // 37: hyapp.activity.v1.AdminTaskService.UpsertTaskDefinition:input_type -> hyapp.activity.v1.UpsertTaskDefinitionRequest + 36, // 38: hyapp.activity.v1.AdminTaskService.SetTaskDefinitionStatus:input_type -> hyapp.activity.v1.SetTaskDefinitionStatusRequest + 2, // 39: hyapp.activity.v1.ActivityService.PingActivity:output_type -> hyapp.activity.v1.PingActivityResponse + 4, // 40: hyapp.activity.v1.ActivityService.GetActivityStatus:output_type -> hyapp.activity.v1.GetActivityStatusResponse + 7, // 41: hyapp.activity.v1.MessageInboxService.ListMessageTabs:output_type -> hyapp.activity.v1.ListMessageTabsResponse + 10, // 42: hyapp.activity.v1.MessageInboxService.ListInboxMessages:output_type -> hyapp.activity.v1.ListInboxMessagesResponse + 12, // 43: hyapp.activity.v1.MessageInboxService.MarkInboxMessageRead:output_type -> hyapp.activity.v1.MarkInboxMessageReadResponse + 14, // 44: hyapp.activity.v1.MessageInboxService.MarkInboxSectionRead:output_type -> hyapp.activity.v1.MarkInboxSectionReadResponse + 16, // 45: hyapp.activity.v1.MessageInboxService.DeleteInboxMessage:output_type -> hyapp.activity.v1.DeleteInboxMessageResponse + 18, // 46: hyapp.activity.v1.MessageInboxService.CreateInboxMessage:output_type -> hyapp.activity.v1.CreateInboxMessageResponse + 20, // 47: hyapp.activity.v1.MessageInboxService.CreateFanoutJob:output_type -> hyapp.activity.v1.CreateFanoutJobResponse + 22, // 48: hyapp.activity.v1.ActivityCronService.ProcessMessageFanoutBatch:output_type -> hyapp.activity.v1.CronBatchResponse + 26, // 49: hyapp.activity.v1.TaskService.ListUserTasks:output_type -> hyapp.activity.v1.ListUserTasksResponse + 28, // 50: hyapp.activity.v1.TaskService.ClaimTaskReward:output_type -> hyapp.activity.v1.ClaimTaskRewardResponse + 30, // 51: hyapp.activity.v1.TaskService.ConsumeTaskEvent:output_type -> hyapp.activity.v1.ConsumeTaskEventResponse + 33, // 52: hyapp.activity.v1.AdminTaskService.ListTaskDefinitions:output_type -> hyapp.activity.v1.ListTaskDefinitionsResponse + 35, // 53: hyapp.activity.v1.AdminTaskService.UpsertTaskDefinition:output_type -> hyapp.activity.v1.UpsertTaskDefinitionResponse + 37, // 54: hyapp.activity.v1.AdminTaskService.SetTaskDefinitionStatus:output_type -> hyapp.activity.v1.SetTaskDefinitionStatusResponse + 39, // [39:55] is the sub-list for method output_type + 23, // [23:39] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name } func init() { file_proto_activity_v1_activity_proto_init() } @@ -1901,9 +3794,9 @@ func file_proto_activity_v1_activity_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_activity_v1_activity_proto_rawDesc, NumEnums: 0, - NumMessages: 21, + NumMessages: 38, NumExtensions: 0, - NumServices: 2, + NumServices: 5, }, GoTypes: file_proto_activity_v1_activity_proto_goTypes, DependencyIndexes: file_proto_activity_v1_activity_proto_depIdxs, diff --git a/api/proto/activity/v1/activity.proto b/api/proto/activity/v1/activity.proto index 8b77b678..dfcd4706 100644 --- a/api/proto/activity/v1/activity.proto +++ b/api/proto/activity/v1/activity.proto @@ -176,6 +176,182 @@ message CreateFanoutJobResponse { bool created = 3; } +// CronBatchRequest 是 cron-service 触发 activity-service 后台批处理的统一请求。 +message CronBatchRequest { + RequestMeta meta = 1; + string run_id = 2; + string worker_id = 3; + int32 batch_size = 4; + int64 lock_ttl_ms = 5; +} + +// CronBatchResponse 返回一个批处理 run 的执行摘要。 +message CronBatchResponse { + int32 claimed_count = 1; + int32 processed_count = 2; + int32 success_count = 3; + int32 failure_count = 4; + bool has_more = 5; +} + +// TaskItem 是 App 每日任务/专属任务列表里的单个可展示任务快照。 +message TaskItem { + string task_id = 1; + string task_type = 2; + string category = 3; + string metric_type = 4; + string title = 5; + string description = 6; + int64 target_value = 7; + string target_unit = 8; + int64 progress_value = 9; + int64 reward_coin_amount = 10; + string status = 11; + bool claimable = 12; + string task_day = 13; + int64 server_time_ms = 14; + int64 next_refresh_at_ms = 15; + int32 sort_order = 16; + int64 version = 17; +} + +// TaskSection 固定表达 App 任务页的 daily/exclusive 两个分区。 +message TaskSection { + string section = 1; + repeated TaskItem items = 2; + int64 server_time_ms = 3; + int64 next_refresh_at_ms = 4; +} + +// ListUserTasksRequest 查询当前用户任务进度;进度必须来自服务端事实事件。 +message ListUserTasksRequest { + RequestMeta meta = 1; + int64 user_id = 2; +} + +// ListUserTasksResponse 返回 daily/exclusive 分区和服务器刷新时间。 +message ListUserTasksResponse { + repeated TaskSection sections = 1; + int64 server_time_ms = 2; + int64 next_refresh_at_ms = 3; +} + +// ClaimTaskRewardRequest 领取已完成任务奖励;command_id 是领奖幂等键。 +message ClaimTaskRewardRequest { + RequestMeta meta = 1; + int64 user_id = 2; + string task_id = 3; + string task_type = 4; + string task_day = 5; + string command_id = 6; +} + +// ClaimTaskRewardResponse 返回 activity 领奖状态和 wallet 入账回执。 +message ClaimTaskRewardResponse { + string claim_id = 1; + string task_id = 2; + string task_type = 3; + string task_day = 4; + int64 reward_coin_amount = 5; + string status = 6; + string wallet_transaction_id = 7; + int64 granted_at_ms = 8; + bool claimed = 9; +} + +// ConsumeTaskEventRequest 是异步 outbox consumer 投递给任务系统的事实事件。 +message ConsumeTaskEventRequest { + RequestMeta meta = 1; + string event_id = 2; + string event_type = 3; + string source_service = 4; + int64 user_id = 5; + string metric_type = 6; + int64 value = 7; + int64 occurred_at_ms = 8; +} + +// ConsumeTaskEventResponse 返回事件消费幂等结果;matched_task_count=0 表示合法跳过。 +message ConsumeTaskEventResponse { + string event_id = 1; + string status = 2; + int32 matched_task_count = 3; +} + +// TaskDefinition 是后台管理任务配置的当前版本读模型。 +message TaskDefinition { + string task_id = 1; + string task_type = 2; + string category = 3; + string metric_type = 4; + string title = 5; + string description = 6; + int64 target_value = 7; + string target_unit = 8; + int64 reward_coin_amount = 9; + string status = 10; + int32 sort_order = 11; + int64 version = 12; + int64 effective_from_ms = 13; + int64 effective_to_ms = 14; + int64 created_by_admin_id = 15; + int64 updated_by_admin_id = 16; + int64 created_at_ms = 17; + int64 updated_at_ms = 18; +} + +// ListTaskDefinitionsRequest 是后台任务配置列表筛选条件。 +message ListTaskDefinitionsRequest { + RequestMeta meta = 1; + string task_type = 2; + string category = 3; + string status = 4; + string keyword = 5; + int32 page = 6; + int32 page_size = 7; +} + +message ListTaskDefinitionsResponse { + repeated TaskDefinition tasks = 1; + int64 total = 2; +} + +// UpsertTaskDefinitionRequest 创建或编辑任务;active 关键字段变更由服务端生成新版本。 +message UpsertTaskDefinitionRequest { + RequestMeta meta = 1; + string task_id = 2; + string task_type = 3; + string category = 4; + string metric_type = 5; + string title = 6; + string description = 7; + int64 target_value = 8; + string target_unit = 9; + int64 reward_coin_amount = 10; + string status = 11; + int32 sort_order = 12; + int64 effective_from_ms = 13; + int64 effective_to_ms = 14; + int64 operator_admin_id = 15; +} + +message UpsertTaskDefinitionResponse { + TaskDefinition task = 1; + bool created = 2; +} + +// SetTaskDefinitionStatusRequest 后台启停或归档任务,不做物理删除。 +message SetTaskDefinitionStatusRequest { + RequestMeta meta = 1; + string task_id = 2; + string status = 3; + int64 operator_admin_id = 4; +} + +message SetTaskDefinitionStatusResponse { + TaskDefinition task = 1; +} + // ActivityService 只暴露同步查询,不把事件消费伪装成 RPC。 service ActivityService { rpc PingActivity(PingActivityRequest) returns (PingActivityResponse); @@ -192,3 +368,22 @@ service MessageInboxService { rpc CreateInboxMessage(CreateInboxMessageRequest) returns (CreateInboxMessageResponse); rpc CreateFanoutJob(CreateFanoutJobRequest) returns (CreateFanoutJobResponse); } + +// ActivityCronService 只给 cron-service 调用,活动和消息状态仍由 activity-service owner 修改。 +service ActivityCronService { + rpc ProcessMessageFanoutBatch(CronBatchRequest) returns (CronBatchResponse); +} + +// TaskService 拥有 App 任务查询、事件进度消费和领奖状态。 +service TaskService { + rpc ListUserTasks(ListUserTasksRequest) returns (ListUserTasksResponse); + rpc ClaimTaskReward(ClaimTaskRewardRequest) returns (ClaimTaskRewardResponse); + rpc ConsumeTaskEvent(ConsumeTaskEventRequest) returns (ConsumeTaskEventResponse); +} + +// AdminTaskService 是后台活动管理访问 activity-service 的唯一任务配置入口。 +service AdminTaskService { + rpc ListTaskDefinitions(ListTaskDefinitionsRequest) returns (ListTaskDefinitionsResponse); + rpc UpsertTaskDefinition(UpsertTaskDefinitionRequest) returns (UpsertTaskDefinitionResponse); + rpc SetTaskDefinitionStatus(SetTaskDefinitionStatusRequest) returns (SetTaskDefinitionStatusResponse); +} diff --git a/api/proto/activity/v1/activity_grpc.pb.go b/api/proto/activity/v1/activity_grpc.pb.go index e4404141..dfe4b8fc 100644 --- a/api/proto/activity/v1/activity_grpc.pb.go +++ b/api/proto/activity/v1/activity_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.27.3 +// - protoc v4.25.3 // source: proto/activity/v1/activity.proto package activityv1 @@ -495,3 +495,473 @@ var MessageInboxService_ServiceDesc = grpc.ServiceDesc{ Streams: []grpc.StreamDesc{}, Metadata: "proto/activity/v1/activity.proto", } + +const ( + ActivityCronService_ProcessMessageFanoutBatch_FullMethodName = "/hyapp.activity.v1.ActivityCronService/ProcessMessageFanoutBatch" +) + +// ActivityCronServiceClient is the client API for ActivityCronService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// ActivityCronService 只给 cron-service 调用,活动和消息状态仍由 activity-service owner 修改。 +type ActivityCronServiceClient interface { + ProcessMessageFanoutBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error) +} + +type activityCronServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewActivityCronServiceClient(cc grpc.ClientConnInterface) ActivityCronServiceClient { + return &activityCronServiceClient{cc} +} + +func (c *activityCronServiceClient) ProcessMessageFanoutBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CronBatchResponse) + err := c.cc.Invoke(ctx, ActivityCronService_ProcessMessageFanoutBatch_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ActivityCronServiceServer is the server API for ActivityCronService service. +// All implementations must embed UnimplementedActivityCronServiceServer +// for forward compatibility. +// +// ActivityCronService 只给 cron-service 调用,活动和消息状态仍由 activity-service owner 修改。 +type ActivityCronServiceServer interface { + ProcessMessageFanoutBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) + mustEmbedUnimplementedActivityCronServiceServer() +} + +// UnimplementedActivityCronServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedActivityCronServiceServer struct{} + +func (UnimplementedActivityCronServiceServer) ProcessMessageFanoutBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ProcessMessageFanoutBatch not implemented") +} +func (UnimplementedActivityCronServiceServer) mustEmbedUnimplementedActivityCronServiceServer() {} +func (UnimplementedActivityCronServiceServer) testEmbeddedByValue() {} + +// UnsafeActivityCronServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ActivityCronServiceServer will +// result in compilation errors. +type UnsafeActivityCronServiceServer interface { + mustEmbedUnimplementedActivityCronServiceServer() +} + +func RegisterActivityCronServiceServer(s grpc.ServiceRegistrar, srv ActivityCronServiceServer) { + // If the following call pancis, it indicates UnimplementedActivityCronServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ActivityCronService_ServiceDesc, srv) +} + +func _ActivityCronService_ProcessMessageFanoutBatch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CronBatchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ActivityCronServiceServer).ProcessMessageFanoutBatch(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ActivityCronService_ProcessMessageFanoutBatch_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ActivityCronServiceServer).ProcessMessageFanoutBatch(ctx, req.(*CronBatchRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ActivityCronService_ServiceDesc is the grpc.ServiceDesc for ActivityCronService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ActivityCronService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "hyapp.activity.v1.ActivityCronService", + HandlerType: (*ActivityCronServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ProcessMessageFanoutBatch", + Handler: _ActivityCronService_ProcessMessageFanoutBatch_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/activity/v1/activity.proto", +} + +const ( + TaskService_ListUserTasks_FullMethodName = "/hyapp.activity.v1.TaskService/ListUserTasks" + TaskService_ClaimTaskReward_FullMethodName = "/hyapp.activity.v1.TaskService/ClaimTaskReward" + TaskService_ConsumeTaskEvent_FullMethodName = "/hyapp.activity.v1.TaskService/ConsumeTaskEvent" +) + +// TaskServiceClient is the client API for TaskService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// TaskService 拥有 App 任务查询、事件进度消费和领奖状态。 +type TaskServiceClient interface { + ListUserTasks(ctx context.Context, in *ListUserTasksRequest, opts ...grpc.CallOption) (*ListUserTasksResponse, error) + ClaimTaskReward(ctx context.Context, in *ClaimTaskRewardRequest, opts ...grpc.CallOption) (*ClaimTaskRewardResponse, error) + ConsumeTaskEvent(ctx context.Context, in *ConsumeTaskEventRequest, opts ...grpc.CallOption) (*ConsumeTaskEventResponse, error) +} + +type taskServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewTaskServiceClient(cc grpc.ClientConnInterface) TaskServiceClient { + return &taskServiceClient{cc} +} + +func (c *taskServiceClient) ListUserTasks(ctx context.Context, in *ListUserTasksRequest, opts ...grpc.CallOption) (*ListUserTasksResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListUserTasksResponse) + err := c.cc.Invoke(ctx, TaskService_ListUserTasks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *taskServiceClient) ClaimTaskReward(ctx context.Context, in *ClaimTaskRewardRequest, opts ...grpc.CallOption) (*ClaimTaskRewardResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ClaimTaskRewardResponse) + err := c.cc.Invoke(ctx, TaskService_ClaimTaskReward_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *taskServiceClient) ConsumeTaskEvent(ctx context.Context, in *ConsumeTaskEventRequest, opts ...grpc.CallOption) (*ConsumeTaskEventResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ConsumeTaskEventResponse) + err := c.cc.Invoke(ctx, TaskService_ConsumeTaskEvent_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// TaskServiceServer is the server API for TaskService service. +// All implementations must embed UnimplementedTaskServiceServer +// for forward compatibility. +// +// TaskService 拥有 App 任务查询、事件进度消费和领奖状态。 +type TaskServiceServer interface { + ListUserTasks(context.Context, *ListUserTasksRequest) (*ListUserTasksResponse, error) + ClaimTaskReward(context.Context, *ClaimTaskRewardRequest) (*ClaimTaskRewardResponse, error) + ConsumeTaskEvent(context.Context, *ConsumeTaskEventRequest) (*ConsumeTaskEventResponse, error) + mustEmbedUnimplementedTaskServiceServer() +} + +// UnimplementedTaskServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedTaskServiceServer struct{} + +func (UnimplementedTaskServiceServer) ListUserTasks(context.Context, *ListUserTasksRequest) (*ListUserTasksResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListUserTasks not implemented") +} +func (UnimplementedTaskServiceServer) ClaimTaskReward(context.Context, *ClaimTaskRewardRequest) (*ClaimTaskRewardResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ClaimTaskReward not implemented") +} +func (UnimplementedTaskServiceServer) ConsumeTaskEvent(context.Context, *ConsumeTaskEventRequest) (*ConsumeTaskEventResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ConsumeTaskEvent not implemented") +} +func (UnimplementedTaskServiceServer) mustEmbedUnimplementedTaskServiceServer() {} +func (UnimplementedTaskServiceServer) testEmbeddedByValue() {} + +// UnsafeTaskServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TaskServiceServer will +// result in compilation errors. +type UnsafeTaskServiceServer interface { + mustEmbedUnimplementedTaskServiceServer() +} + +func RegisterTaskServiceServer(s grpc.ServiceRegistrar, srv TaskServiceServer) { + // If the following call pancis, it indicates UnimplementedTaskServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&TaskService_ServiceDesc, srv) +} + +func _TaskService_ListUserTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListUserTasksRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TaskServiceServer).ListUserTasks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TaskService_ListUserTasks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TaskServiceServer).ListUserTasks(ctx, req.(*ListUserTasksRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TaskService_ClaimTaskReward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ClaimTaskRewardRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TaskServiceServer).ClaimTaskReward(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TaskService_ClaimTaskReward_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TaskServiceServer).ClaimTaskReward(ctx, req.(*ClaimTaskRewardRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TaskService_ConsumeTaskEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ConsumeTaskEventRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TaskServiceServer).ConsumeTaskEvent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TaskService_ConsumeTaskEvent_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TaskServiceServer).ConsumeTaskEvent(ctx, req.(*ConsumeTaskEventRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// TaskService_ServiceDesc is the grpc.ServiceDesc for TaskService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var TaskService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "hyapp.activity.v1.TaskService", + HandlerType: (*TaskServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListUserTasks", + Handler: _TaskService_ListUserTasks_Handler, + }, + { + MethodName: "ClaimTaskReward", + Handler: _TaskService_ClaimTaskReward_Handler, + }, + { + MethodName: "ConsumeTaskEvent", + Handler: _TaskService_ConsumeTaskEvent_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/activity/v1/activity.proto", +} + +const ( + AdminTaskService_ListTaskDefinitions_FullMethodName = "/hyapp.activity.v1.AdminTaskService/ListTaskDefinitions" + AdminTaskService_UpsertTaskDefinition_FullMethodName = "/hyapp.activity.v1.AdminTaskService/UpsertTaskDefinition" + AdminTaskService_SetTaskDefinitionStatus_FullMethodName = "/hyapp.activity.v1.AdminTaskService/SetTaskDefinitionStatus" +) + +// AdminTaskServiceClient is the client API for AdminTaskService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// AdminTaskService 是后台活动管理访问 activity-service 的唯一任务配置入口。 +type AdminTaskServiceClient interface { + ListTaskDefinitions(ctx context.Context, in *ListTaskDefinitionsRequest, opts ...grpc.CallOption) (*ListTaskDefinitionsResponse, error) + UpsertTaskDefinition(ctx context.Context, in *UpsertTaskDefinitionRequest, opts ...grpc.CallOption) (*UpsertTaskDefinitionResponse, error) + SetTaskDefinitionStatus(ctx context.Context, in *SetTaskDefinitionStatusRequest, opts ...grpc.CallOption) (*SetTaskDefinitionStatusResponse, error) +} + +type adminTaskServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAdminTaskServiceClient(cc grpc.ClientConnInterface) AdminTaskServiceClient { + return &adminTaskServiceClient{cc} +} + +func (c *adminTaskServiceClient) ListTaskDefinitions(ctx context.Context, in *ListTaskDefinitionsRequest, opts ...grpc.CallOption) (*ListTaskDefinitionsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListTaskDefinitionsResponse) + err := c.cc.Invoke(ctx, AdminTaskService_ListTaskDefinitions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *adminTaskServiceClient) UpsertTaskDefinition(ctx context.Context, in *UpsertTaskDefinitionRequest, opts ...grpc.CallOption) (*UpsertTaskDefinitionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpsertTaskDefinitionResponse) + err := c.cc.Invoke(ctx, AdminTaskService_UpsertTaskDefinition_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *adminTaskServiceClient) SetTaskDefinitionStatus(ctx context.Context, in *SetTaskDefinitionStatusRequest, opts ...grpc.CallOption) (*SetTaskDefinitionStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SetTaskDefinitionStatusResponse) + err := c.cc.Invoke(ctx, AdminTaskService_SetTaskDefinitionStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AdminTaskServiceServer is the server API for AdminTaskService service. +// All implementations must embed UnimplementedAdminTaskServiceServer +// for forward compatibility. +// +// AdminTaskService 是后台活动管理访问 activity-service 的唯一任务配置入口。 +type AdminTaskServiceServer interface { + ListTaskDefinitions(context.Context, *ListTaskDefinitionsRequest) (*ListTaskDefinitionsResponse, error) + UpsertTaskDefinition(context.Context, *UpsertTaskDefinitionRequest) (*UpsertTaskDefinitionResponse, error) + SetTaskDefinitionStatus(context.Context, *SetTaskDefinitionStatusRequest) (*SetTaskDefinitionStatusResponse, error) + mustEmbedUnimplementedAdminTaskServiceServer() +} + +// UnimplementedAdminTaskServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAdminTaskServiceServer struct{} + +func (UnimplementedAdminTaskServiceServer) ListTaskDefinitions(context.Context, *ListTaskDefinitionsRequest) (*ListTaskDefinitionsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListTaskDefinitions not implemented") +} +func (UnimplementedAdminTaskServiceServer) UpsertTaskDefinition(context.Context, *UpsertTaskDefinitionRequest) (*UpsertTaskDefinitionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpsertTaskDefinition not implemented") +} +func (UnimplementedAdminTaskServiceServer) SetTaskDefinitionStatus(context.Context, *SetTaskDefinitionStatusRequest) (*SetTaskDefinitionStatusResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetTaskDefinitionStatus not implemented") +} +func (UnimplementedAdminTaskServiceServer) mustEmbedUnimplementedAdminTaskServiceServer() {} +func (UnimplementedAdminTaskServiceServer) testEmbeddedByValue() {} + +// UnsafeAdminTaskServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AdminTaskServiceServer will +// result in compilation errors. +type UnsafeAdminTaskServiceServer interface { + mustEmbedUnimplementedAdminTaskServiceServer() +} + +func RegisterAdminTaskServiceServer(s grpc.ServiceRegistrar, srv AdminTaskServiceServer) { + // If the following call pancis, it indicates UnimplementedAdminTaskServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&AdminTaskService_ServiceDesc, srv) +} + +func _AdminTaskService_ListTaskDefinitions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTaskDefinitionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminTaskServiceServer).ListTaskDefinitions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminTaskService_ListTaskDefinitions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminTaskServiceServer).ListTaskDefinitions(ctx, req.(*ListTaskDefinitionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AdminTaskService_UpsertTaskDefinition_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpsertTaskDefinitionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminTaskServiceServer).UpsertTaskDefinition(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminTaskService_UpsertTaskDefinition_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminTaskServiceServer).UpsertTaskDefinition(ctx, req.(*UpsertTaskDefinitionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AdminTaskService_SetTaskDefinitionStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetTaskDefinitionStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminTaskServiceServer).SetTaskDefinitionStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminTaskService_SetTaskDefinitionStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminTaskServiceServer).SetTaskDefinitionStatus(ctx, req.(*SetTaskDefinitionStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AdminTaskService_ServiceDesc is the grpc.ServiceDesc for AdminTaskService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AdminTaskService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "hyapp.activity.v1.AdminTaskService", + HandlerType: (*AdminTaskServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListTaskDefinitions", + Handler: _AdminTaskService_ListTaskDefinitions_Handler, + }, + { + MethodName: "UpsertTaskDefinition", + Handler: _AdminTaskService_UpsertTaskDefinition_Handler, + }, + { + MethodName: "SetTaskDefinitionStatus", + Handler: _AdminTaskService_SetTaskDefinitionStatus_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/activity/v1/activity.proto", +} diff --git a/api/proto/events/room/v1/events.pb.go b/api/proto/events/room/v1/events.pb.go index 86067e1c..19355143 100644 --- a/api/proto/events/room/v1/events.pb.go +++ b/api/proto/events/room/v1/events.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.1 -// protoc v5.27.3 +// protoc v4.25.3 // source: proto/events/room/v1/events.proto package roomeventsv1 diff --git a/api/proto/room/v1/room.pb.go b/api/proto/room/v1/room.pb.go index 68279e73..adc82f41 100644 --- a/api/proto/room/v1/room.pb.go +++ b/api/proto/room/v1/room.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.1 -// protoc v5.27.3 +// protoc v4.25.3 // source: proto/room/v1/room.proto package roomv1 diff --git a/api/proto/room/v1/room_grpc.pb.go b/api/proto/room/v1/room_grpc.pb.go index 97351c21..be8fe87b 100644 --- a/api/proto/room/v1/room_grpc.pb.go +++ b/api/proto/room/v1/room_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.27.3 +// - protoc v4.25.3 // source: proto/room/v1/room.proto package roomv1 diff --git a/api/proto/user/v1/auth.pb.go b/api/proto/user/v1/auth.pb.go index 5ebee983..46a71686 100644 --- a/api/proto/user/v1/auth.pb.go +++ b/api/proto/user/v1/auth.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.1 -// protoc v5.27.3 +// protoc v4.25.3 // source: proto/user/v1/auth.proto package userv1 @@ -666,6 +666,138 @@ func (x *LogoutResponse) GetRevoked() bool { return false } +// RecordLoginBlockedRequest 记录 gateway 入口快拦拒绝的登录审计。 +type RecordLoginBlockedRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + Channel string `protobuf:"bytes,2,opt,name=channel,proto3" json:"channel,omitempty"` + Platform string `protobuf:"bytes,3,opt,name=platform,proto3" json:"platform,omitempty"` + Language string `protobuf:"bytes,4,opt,name=language,proto3" json:"language,omitempty"` + Timezone string `protobuf:"bytes,5,opt,name=timezone,proto3" json:"timezone,omitempty"` + BlockReason string `protobuf:"bytes,6,opt,name=block_reason,json=blockReason,proto3" json:"block_reason,omitempty"` +} + +func (x *RecordLoginBlockedRequest) Reset() { + *x = RecordLoginBlockedRequest{} + mi := &file_proto_user_v1_auth_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RecordLoginBlockedRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RecordLoginBlockedRequest) ProtoMessage() {} + +func (x *RecordLoginBlockedRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_user_v1_auth_proto_msgTypes[9] + 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 RecordLoginBlockedRequest.ProtoReflect.Descriptor instead. +func (*RecordLoginBlockedRequest) Descriptor() ([]byte, []int) { + return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{9} +} + +func (x *RecordLoginBlockedRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *RecordLoginBlockedRequest) GetChannel() string { + if x != nil { + return x.Channel + } + return "" +} + +func (x *RecordLoginBlockedRequest) GetPlatform() string { + if x != nil { + return x.Platform + } + return "" +} + +func (x *RecordLoginBlockedRequest) GetLanguage() string { + if x != nil { + return x.Language + } + return "" +} + +func (x *RecordLoginBlockedRequest) GetTimezone() string { + if x != nil { + return x.Timezone + } + return "" +} + +func (x *RecordLoginBlockedRequest) GetBlockReason() string { + if x != nil { + return x.BlockReason + } + return "" +} + +// RecordLoginBlockedResponse 返回审计写入是否成功。 +type RecordLoginBlockedResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Recorded bool `protobuf:"varint,1,opt,name=recorded,proto3" json:"recorded,omitempty"` +} + +func (x *RecordLoginBlockedResponse) Reset() { + *x = RecordLoginBlockedResponse{} + mi := &file_proto_user_v1_auth_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RecordLoginBlockedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RecordLoginBlockedResponse) ProtoMessage() {} + +func (x *RecordLoginBlockedResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_user_v1_auth_proto_msgTypes[10] + 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 RecordLoginBlockedResponse.ProtoReflect.Descriptor instead. +func (*RecordLoginBlockedResponse) Descriptor() ([]byte, []int) { + return file_proto_user_v1_auth_proto_rawDescGZIP(), []int{10} +} + +func (x *RecordLoginBlockedResponse) GetRecorded() bool { + if x != nil { + return x.Recorded + } + return false +} + var File_proto_user_v1_auth_proto protoreflect.FileDescriptor var file_proto_user_v1_auth_proto_rawDesc = []byte{ @@ -766,37 +898,61 @@ var file_proto_user_v1_auth_proto_rawDesc = []byte{ 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x2a, 0x0a, 0x0e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x76, 0x6f, - 0x6b, 0x65, 0x64, 0x32, 0xad, 0x03, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, - 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x6b, 0x65, 0x64, 0x22, 0xdc, 0x01, 0x0a, 0x19, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, + 0x61, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x70, + 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, + 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, + 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, + 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x12, + 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x22, 0x38, 0x0a, 0x1a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, 0x64, 0x32, 0x98, 0x04, 0x0a, + 0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x0d, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x23, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, + 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x55, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, + 0x74, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, + 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, - 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, 0x74, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, - 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, - 0x0b, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x21, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, - 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, - 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, - 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, - 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, - 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, - 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x75, 0x73, 0x65, - 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, + 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x2e, 0x68, + 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66, + 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, + 0x1c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, + 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x12, + 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, + 0x65, 0x64, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68, + 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, + 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -811,43 +967,48 @@ func file_proto_user_v1_auth_proto_rawDescGZIP() []byte { return file_proto_user_v1_auth_proto_rawDescData } -var file_proto_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_proto_user_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_proto_user_v1_auth_proto_goTypes = []any{ - (*LoginPasswordRequest)(nil), // 0: hyapp.user.v1.LoginPasswordRequest - (*LoginThirdPartyRequest)(nil), // 1: hyapp.user.v1.LoginThirdPartyRequest - (*AuthResponse)(nil), // 2: hyapp.user.v1.AuthResponse - (*SetPasswordRequest)(nil), // 3: hyapp.user.v1.SetPasswordRequest - (*SetPasswordResponse)(nil), // 4: hyapp.user.v1.SetPasswordResponse - (*RefreshTokenRequest)(nil), // 5: hyapp.user.v1.RefreshTokenRequest - (*RefreshTokenResponse)(nil), // 6: hyapp.user.v1.RefreshTokenResponse - (*LogoutRequest)(nil), // 7: hyapp.user.v1.LogoutRequest - (*LogoutResponse)(nil), // 8: hyapp.user.v1.LogoutResponse - (*RequestMeta)(nil), // 9: hyapp.user.v1.RequestMeta - (*AuthToken)(nil), // 10: hyapp.user.v1.AuthToken + (*LoginPasswordRequest)(nil), // 0: hyapp.user.v1.LoginPasswordRequest + (*LoginThirdPartyRequest)(nil), // 1: hyapp.user.v1.LoginThirdPartyRequest + (*AuthResponse)(nil), // 2: hyapp.user.v1.AuthResponse + (*SetPasswordRequest)(nil), // 3: hyapp.user.v1.SetPasswordRequest + (*SetPasswordResponse)(nil), // 4: hyapp.user.v1.SetPasswordResponse + (*RefreshTokenRequest)(nil), // 5: hyapp.user.v1.RefreshTokenRequest + (*RefreshTokenResponse)(nil), // 6: hyapp.user.v1.RefreshTokenResponse + (*LogoutRequest)(nil), // 7: hyapp.user.v1.LogoutRequest + (*LogoutResponse)(nil), // 8: hyapp.user.v1.LogoutResponse + (*RecordLoginBlockedRequest)(nil), // 9: hyapp.user.v1.RecordLoginBlockedRequest + (*RecordLoginBlockedResponse)(nil), // 10: hyapp.user.v1.RecordLoginBlockedResponse + (*RequestMeta)(nil), // 11: hyapp.user.v1.RequestMeta + (*AuthToken)(nil), // 12: hyapp.user.v1.AuthToken } var file_proto_user_v1_auth_proto_depIdxs = []int32{ - 9, // 0: hyapp.user.v1.LoginPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 9, // 1: hyapp.user.v1.LoginThirdPartyRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 10, // 2: hyapp.user.v1.AuthResponse.token:type_name -> hyapp.user.v1.AuthToken - 9, // 3: hyapp.user.v1.SetPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 9, // 4: hyapp.user.v1.RefreshTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 10, // 5: hyapp.user.v1.RefreshTokenResponse.token:type_name -> hyapp.user.v1.AuthToken - 9, // 6: hyapp.user.v1.LogoutRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 0, // 7: hyapp.user.v1.AuthService.LoginPassword:input_type -> hyapp.user.v1.LoginPasswordRequest - 1, // 8: hyapp.user.v1.AuthService.LoginThirdParty:input_type -> hyapp.user.v1.LoginThirdPartyRequest - 3, // 9: hyapp.user.v1.AuthService.SetPassword:input_type -> hyapp.user.v1.SetPasswordRequest - 5, // 10: hyapp.user.v1.AuthService.RefreshToken:input_type -> hyapp.user.v1.RefreshTokenRequest - 7, // 11: hyapp.user.v1.AuthService.Logout:input_type -> hyapp.user.v1.LogoutRequest - 2, // 12: hyapp.user.v1.AuthService.LoginPassword:output_type -> hyapp.user.v1.AuthResponse - 2, // 13: hyapp.user.v1.AuthService.LoginThirdParty:output_type -> hyapp.user.v1.AuthResponse - 4, // 14: hyapp.user.v1.AuthService.SetPassword:output_type -> hyapp.user.v1.SetPasswordResponse - 6, // 15: hyapp.user.v1.AuthService.RefreshToken:output_type -> hyapp.user.v1.RefreshTokenResponse - 8, // 16: hyapp.user.v1.AuthService.Logout:output_type -> hyapp.user.v1.LogoutResponse - 12, // [12:17] is the sub-list for method output_type - 7, // [7:12] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 11, // 0: hyapp.user.v1.LoginPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 11, // 1: hyapp.user.v1.LoginThirdPartyRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 12, // 2: hyapp.user.v1.AuthResponse.token:type_name -> hyapp.user.v1.AuthToken + 11, // 3: hyapp.user.v1.SetPasswordRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 11, // 4: hyapp.user.v1.RefreshTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 12, // 5: hyapp.user.v1.RefreshTokenResponse.token:type_name -> hyapp.user.v1.AuthToken + 11, // 6: hyapp.user.v1.LogoutRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 11, // 7: hyapp.user.v1.RecordLoginBlockedRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 0, // 8: hyapp.user.v1.AuthService.LoginPassword:input_type -> hyapp.user.v1.LoginPasswordRequest + 1, // 9: hyapp.user.v1.AuthService.LoginThirdParty:input_type -> hyapp.user.v1.LoginThirdPartyRequest + 3, // 10: hyapp.user.v1.AuthService.SetPassword:input_type -> hyapp.user.v1.SetPasswordRequest + 5, // 11: hyapp.user.v1.AuthService.RefreshToken:input_type -> hyapp.user.v1.RefreshTokenRequest + 7, // 12: hyapp.user.v1.AuthService.Logout:input_type -> hyapp.user.v1.LogoutRequest + 9, // 13: hyapp.user.v1.AuthService.RecordLoginBlocked:input_type -> hyapp.user.v1.RecordLoginBlockedRequest + 2, // 14: hyapp.user.v1.AuthService.LoginPassword:output_type -> hyapp.user.v1.AuthResponse + 2, // 15: hyapp.user.v1.AuthService.LoginThirdParty:output_type -> hyapp.user.v1.AuthResponse + 4, // 16: hyapp.user.v1.AuthService.SetPassword:output_type -> hyapp.user.v1.SetPasswordResponse + 6, // 17: hyapp.user.v1.AuthService.RefreshToken:output_type -> hyapp.user.v1.RefreshTokenResponse + 8, // 18: hyapp.user.v1.AuthService.Logout:output_type -> hyapp.user.v1.LogoutResponse + 10, // 19: hyapp.user.v1.AuthService.RecordLoginBlocked:output_type -> hyapp.user.v1.RecordLoginBlockedResponse + 14, // [14:20] is the sub-list for method output_type + 8, // [8:14] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_proto_user_v1_auth_proto_init() } @@ -862,7 +1023,7 @@ func file_proto_user_v1_auth_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_user_v1_auth_proto_rawDesc, NumEnums: 0, - NumMessages: 9, + NumMessages: 11, NumExtensions: 0, NumServices: 1, }, diff --git a/api/proto/user/v1/auth.proto b/api/proto/user/v1/auth.proto index 689a8d45..9540a956 100644 --- a/api/proto/user/v1/auth.proto +++ b/api/proto/user/v1/auth.proto @@ -80,6 +80,21 @@ message LogoutResponse { bool revoked = 1; } +// RecordLoginBlockedRequest 记录 gateway 入口快拦拒绝的登录审计。 +message RecordLoginBlockedRequest { + RequestMeta meta = 1; + string channel = 2; + string platform = 3; + string language = 4; + string timezone = 5; + string block_reason = 6; +} + +// RecordLoginBlockedResponse 返回审计写入是否成功。 +message RecordLoginBlockedResponse { + bool recorded = 1; +} + // AuthService 承载登录注册和 token 生命周期能力。 service AuthService { rpc LoginPassword(LoginPasswordRequest) returns (AuthResponse); @@ -87,4 +102,5 @@ service AuthService { rpc SetPassword(SetPasswordRequest) returns (SetPasswordResponse); rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse); rpc Logout(LogoutRequest) returns (LogoutResponse); + rpc RecordLoginBlocked(RecordLoginBlockedRequest) returns (RecordLoginBlockedResponse); } diff --git a/api/proto/user/v1/auth_grpc.pb.go b/api/proto/user/v1/auth_grpc.pb.go index f1366f5e..052bc3b9 100644 --- a/api/proto/user/v1/auth_grpc.pb.go +++ b/api/proto/user/v1/auth_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.27.3 +// - protoc v4.25.3 // source: proto/user/v1/auth.proto package userv1 @@ -19,11 +19,12 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - AuthService_LoginPassword_FullMethodName = "/hyapp.user.v1.AuthService/LoginPassword" - AuthService_LoginThirdParty_FullMethodName = "/hyapp.user.v1.AuthService/LoginThirdParty" - AuthService_SetPassword_FullMethodName = "/hyapp.user.v1.AuthService/SetPassword" - AuthService_RefreshToken_FullMethodName = "/hyapp.user.v1.AuthService/RefreshToken" - AuthService_Logout_FullMethodName = "/hyapp.user.v1.AuthService/Logout" + AuthService_LoginPassword_FullMethodName = "/hyapp.user.v1.AuthService/LoginPassword" + AuthService_LoginThirdParty_FullMethodName = "/hyapp.user.v1.AuthService/LoginThirdParty" + AuthService_SetPassword_FullMethodName = "/hyapp.user.v1.AuthService/SetPassword" + AuthService_RefreshToken_FullMethodName = "/hyapp.user.v1.AuthService/RefreshToken" + AuthService_Logout_FullMethodName = "/hyapp.user.v1.AuthService/Logout" + AuthService_RecordLoginBlocked_FullMethodName = "/hyapp.user.v1.AuthService/RecordLoginBlocked" ) // AuthServiceClient is the client API for AuthService service. @@ -37,6 +38,7 @@ type AuthServiceClient interface { SetPassword(ctx context.Context, in *SetPasswordRequest, opts ...grpc.CallOption) (*SetPasswordResponse, error) RefreshToken(ctx context.Context, in *RefreshTokenRequest, opts ...grpc.CallOption) (*RefreshTokenResponse, error) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) + RecordLoginBlocked(ctx context.Context, in *RecordLoginBlockedRequest, opts ...grpc.CallOption) (*RecordLoginBlockedResponse, error) } type authServiceClient struct { @@ -97,6 +99,16 @@ func (c *authServiceClient) Logout(ctx context.Context, in *LogoutRequest, opts return out, nil } +func (c *authServiceClient) RecordLoginBlocked(ctx context.Context, in *RecordLoginBlockedRequest, opts ...grpc.CallOption) (*RecordLoginBlockedResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RecordLoginBlockedResponse) + err := c.cc.Invoke(ctx, AuthService_RecordLoginBlocked_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AuthServiceServer is the server API for AuthService service. // All implementations must embed UnimplementedAuthServiceServer // for forward compatibility. @@ -108,6 +120,7 @@ type AuthServiceServer interface { SetPassword(context.Context, *SetPasswordRequest) (*SetPasswordResponse, error) RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) + RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error) mustEmbedUnimplementedAuthServiceServer() } @@ -133,6 +146,9 @@ func (UnimplementedAuthServiceServer) RefreshToken(context.Context, *RefreshToke func (UnimplementedAuthServiceServer) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented") } +func (UnimplementedAuthServiceServer) RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RecordLoginBlocked not implemented") +} func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {} func (UnimplementedAuthServiceServer) testEmbeddedByValue() {} @@ -244,6 +260,24 @@ func _AuthService_Logout_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _AuthService_RecordLoginBlocked_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RecordLoginBlockedRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).RecordLoginBlocked(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_RecordLoginBlocked_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).RecordLoginBlocked(ctx, req.(*RecordLoginBlockedRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -271,6 +305,10 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Logout", Handler: _AuthService_Logout_Handler, }, + { + MethodName: "RecordLoginBlocked", + Handler: _AuthService_RecordLoginBlocked_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "proto/user/v1/auth.proto", diff --git a/api/proto/user/v1/host.pb.go b/api/proto/user/v1/host.pb.go index c2445e4b..bf600401 100644 --- a/api/proto/user/v1/host.pb.go +++ b/api/proto/user/v1/host.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.1 -// protoc v5.27.3 +// protoc v4.25.3 // source: proto/user/v1/host.proto package userv1 diff --git a/api/proto/user/v1/host_grpc.pb.go b/api/proto/user/v1/host_grpc.pb.go index 3002e68e..b0c77cff 100644 --- a/api/proto/user/v1/host_grpc.pb.go +++ b/api/proto/user/v1/host_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.27.3 +// - protoc v4.25.3 // source: proto/user/v1/host.proto package userv1 diff --git a/api/proto/user/v1/user.pb.go b/api/proto/user/v1/user.pb.go index 7ad14893..ea9a1e4d 100644 --- a/api/proto/user/v1/user.pb.go +++ b/api/proto/user/v1/user.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.1 -// protoc v5.27.3 +// protoc v4.25.3 // source: proto/user/v1/user.proto package userv1 @@ -89,6 +89,9 @@ type RequestMeta struct { CountryByIp string `protobuf:"bytes,8,opt,name=country_by_ip,json=countryByIp,proto3" json:"country_by_ip,omitempty"` SessionId string `protobuf:"bytes,9,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` AppCode string `protobuf:"bytes,10,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` + Platform string `protobuf:"bytes,11,opt,name=platform,proto3" json:"platform,omitempty"` + Language string `protobuf:"bytes,12,opt,name=language,proto3" json:"language,omitempty"` + Timezone string `protobuf:"bytes,13,opt,name=timezone,proto3" json:"timezone,omitempty"` } func (x *RequestMeta) Reset() { @@ -191,6 +194,27 @@ func (x *RequestMeta) GetAppCode() string { return "" } +func (x *RequestMeta) GetPlatform() string { + if x != nil { + return x.Platform + } + return "" +} + +func (x *RequestMeta) GetLanguage() string { + if x != nil { + return x.Language + } + return "" +} + +func (x *RequestMeta) GetTimezone() string { + if x != nil { + return x.Timezone + } + return "" +} + // App 是 user-service 维护的 App 注册表投影,用于把客户端包名映射到内部租户键。 type App struct { state protoimpl.MessageState @@ -1011,6 +1035,178 @@ func (x *GetUserMicLifetimeStatsResponse) GetStats() *UserMicLifetimeStats { return nil } +// CronBatchRequest 是 cron-service 触发 user-service 后台批处理的统一请求。 +type CronBatchRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + WorkerId string `protobuf:"bytes,3,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"` + BatchSize int32 `protobuf:"varint,4,opt,name=batch_size,json=batchSize,proto3" json:"batch_size,omitempty"` + LockTtlMs int64 `protobuf:"varint,5,opt,name=lock_ttl_ms,json=lockTtlMs,proto3" json:"lock_ttl_ms,omitempty"` + PendingPublishMaxAgeMs int64 `protobuf:"varint,6,opt,name=pending_publish_max_age_ms,json=pendingPublishMaxAgeMs,proto3" json:"pending_publish_max_age_ms,omitempty"` + PublishingSessionMaxAgeMs int64 `protobuf:"varint,7,opt,name=publishing_session_max_age_ms,json=publishingSessionMaxAgeMs,proto3" json:"publishing_session_max_age_ms,omitempty"` +} + +func (x *CronBatchRequest) Reset() { + *x = CronBatchRequest{} + mi := &file_proto_user_v1_user_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CronBatchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CronBatchRequest) ProtoMessage() {} + +func (x *CronBatchRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_user_v1_user_proto_msgTypes[10] + 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 CronBatchRequest.ProtoReflect.Descriptor instead. +func (*CronBatchRequest) Descriptor() ([]byte, []int) { + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{10} +} + +func (x *CronBatchRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *CronBatchRequest) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *CronBatchRequest) GetWorkerId() string { + if x != nil { + return x.WorkerId + } + return "" +} + +func (x *CronBatchRequest) GetBatchSize() int32 { + if x != nil { + return x.BatchSize + } + return 0 +} + +func (x *CronBatchRequest) GetLockTtlMs() int64 { + if x != nil { + return x.LockTtlMs + } + return 0 +} + +func (x *CronBatchRequest) GetPendingPublishMaxAgeMs() int64 { + if x != nil { + return x.PendingPublishMaxAgeMs + } + return 0 +} + +func (x *CronBatchRequest) GetPublishingSessionMaxAgeMs() int64 { + if x != nil { + return x.PublishingSessionMaxAgeMs + } + return 0 +} + +// CronBatchResponse 返回一个批处理 run 的执行摘要。 +type CronBatchResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ClaimedCount int32 `protobuf:"varint,1,opt,name=claimed_count,json=claimedCount,proto3" json:"claimed_count,omitempty"` + ProcessedCount int32 `protobuf:"varint,2,opt,name=processed_count,json=processedCount,proto3" json:"processed_count,omitempty"` + SuccessCount int32 `protobuf:"varint,3,opt,name=success_count,json=successCount,proto3" json:"success_count,omitempty"` + FailureCount int32 `protobuf:"varint,4,opt,name=failure_count,json=failureCount,proto3" json:"failure_count,omitempty"` + HasMore bool `protobuf:"varint,5,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` +} + +func (x *CronBatchResponse) Reset() { + *x = CronBatchResponse{} + mi := &file_proto_user_v1_user_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CronBatchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CronBatchResponse) ProtoMessage() {} + +func (x *CronBatchResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_user_v1_user_proto_msgTypes[11] + 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 CronBatchResponse.ProtoReflect.Descriptor instead. +func (*CronBatchResponse) Descriptor() ([]byte, []int) { + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{11} +} + +func (x *CronBatchResponse) GetClaimedCount() int32 { + if x != nil { + return x.ClaimedCount + } + return 0 +} + +func (x *CronBatchResponse) GetProcessedCount() int32 { + if x != nil { + return x.ProcessedCount + } + return 0 +} + +func (x *CronBatchResponse) GetSuccessCount() int32 { + if x != nil { + return x.SuccessCount + } + return 0 +} + +func (x *CronBatchResponse) GetFailureCount() int32 { + if x != nil { + return x.FailureCount + } + return 0 +} + +func (x *CronBatchResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + // AuthToken 是登录、刷新和注册完成后返回给 gateway 的访问令牌投影。 type AuthToken struct { state protoimpl.MessageState @@ -1034,7 +1230,7 @@ type AuthToken struct { func (x *AuthToken) Reset() { *x = AuthToken{} - mi := &file_proto_user_v1_user_proto_msgTypes[10] + mi := &file_proto_user_v1_user_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1046,7 +1242,7 @@ func (x *AuthToken) String() string { func (*AuthToken) ProtoMessage() {} func (x *AuthToken) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[10] + mi := &file_proto_user_v1_user_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1059,7 +1255,7 @@ func (x *AuthToken) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthToken.ProtoReflect.Descriptor instead. func (*AuthToken) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{10} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{12} } func (x *AuthToken) GetUserId() int64 { @@ -1165,7 +1361,7 @@ type GetUserRequest struct { func (x *GetUserRequest) Reset() { *x = GetUserRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[11] + mi := &file_proto_user_v1_user_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1177,7 +1373,7 @@ func (x *GetUserRequest) String() string { func (*GetUserRequest) ProtoMessage() {} func (x *GetUserRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[11] + mi := &file_proto_user_v1_user_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1190,7 +1386,7 @@ func (x *GetUserRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUserRequest.ProtoReflect.Descriptor instead. func (*GetUserRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{11} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{13} } func (x *GetUserRequest) GetMeta() *RequestMeta { @@ -1218,7 +1414,7 @@ type GetUserResponse struct { func (x *GetUserResponse) Reset() { *x = GetUserResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[12] + mi := &file_proto_user_v1_user_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1230,7 +1426,7 @@ func (x *GetUserResponse) String() string { func (*GetUserResponse) ProtoMessage() {} func (x *GetUserResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[12] + mi := &file_proto_user_v1_user_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1243,7 +1439,7 @@ func (x *GetUserResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUserResponse.ProtoReflect.Descriptor instead. func (*GetUserResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{12} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{14} } func (x *GetUserResponse) GetUser() *User { @@ -1265,7 +1461,7 @@ type BatchGetUsersRequest struct { func (x *BatchGetUsersRequest) Reset() { *x = BatchGetUsersRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[13] + mi := &file_proto_user_v1_user_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1277,7 +1473,7 @@ func (x *BatchGetUsersRequest) String() string { func (*BatchGetUsersRequest) ProtoMessage() {} func (x *BatchGetUsersRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[13] + mi := &file_proto_user_v1_user_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1290,7 +1486,7 @@ func (x *BatchGetUsersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchGetUsersRequest.ProtoReflect.Descriptor instead. func (*BatchGetUsersRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{13} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{15} } func (x *BatchGetUsersRequest) GetMeta() *RequestMeta { @@ -1318,7 +1514,7 @@ type BatchGetUsersResponse struct { func (x *BatchGetUsersResponse) Reset() { *x = BatchGetUsersResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[14] + mi := &file_proto_user_v1_user_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1330,7 +1526,7 @@ func (x *BatchGetUsersResponse) String() string { func (*BatchGetUsersResponse) ProtoMessage() {} func (x *BatchGetUsersResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[14] + mi := &file_proto_user_v1_user_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1343,7 +1539,7 @@ func (x *BatchGetUsersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchGetUsersResponse.ProtoReflect.Descriptor instead. func (*BatchGetUsersResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{14} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{16} } func (x *BatchGetUsersResponse) GetUsers() map[int64]*User { @@ -1369,7 +1565,7 @@ type ListUserIDsRequest struct { func (x *ListUserIDsRequest) Reset() { *x = ListUserIDsRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[15] + mi := &file_proto_user_v1_user_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1381,7 +1577,7 @@ func (x *ListUserIDsRequest) String() string { func (*ListUserIDsRequest) ProtoMessage() {} func (x *ListUserIDsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[15] + mi := &file_proto_user_v1_user_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1394,7 +1590,7 @@ func (x *ListUserIDsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUserIDsRequest.ProtoReflect.Descriptor instead. func (*ListUserIDsRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{15} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{17} } func (x *ListUserIDsRequest) GetMeta() *RequestMeta { @@ -1452,7 +1648,7 @@ type ListUserIDsResponse struct { func (x *ListUserIDsResponse) Reset() { *x = ListUserIDsResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[16] + mi := &file_proto_user_v1_user_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1464,7 +1660,7 @@ func (x *ListUserIDsResponse) String() string { func (*ListUserIDsResponse) ProtoMessage() {} func (x *ListUserIDsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[16] + mi := &file_proto_user_v1_user_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1477,7 +1673,7 @@ func (x *ListUserIDsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUserIDsResponse.ProtoReflect.Descriptor instead. func (*ListUserIDsResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{16} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{18} } func (x *ListUserIDsResponse) GetUserIds() []int64 { @@ -1517,7 +1713,7 @@ type UpdateUserProfileRequest struct { func (x *UpdateUserProfileRequest) Reset() { *x = UpdateUserProfileRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[17] + mi := &file_proto_user_v1_user_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1529,7 +1725,7 @@ func (x *UpdateUserProfileRequest) String() string { func (*UpdateUserProfileRequest) ProtoMessage() {} func (x *UpdateUserProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[17] + mi := &file_proto_user_v1_user_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1542,7 +1738,7 @@ func (x *UpdateUserProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateUserProfileRequest.ProtoReflect.Descriptor instead. func (*UpdateUserProfileRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{17} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{19} } func (x *UpdateUserProfileRequest) GetMeta() *RequestMeta { @@ -1598,7 +1794,7 @@ type UpdateUserProfileResponse struct { func (x *UpdateUserProfileResponse) Reset() { *x = UpdateUserProfileResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[18] + mi := &file_proto_user_v1_user_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1610,7 +1806,7 @@ func (x *UpdateUserProfileResponse) String() string { func (*UpdateUserProfileResponse) ProtoMessage() {} func (x *UpdateUserProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[18] + mi := &file_proto_user_v1_user_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1623,7 +1819,7 @@ func (x *UpdateUserProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateUserProfileResponse.ProtoReflect.Descriptor instead. func (*UpdateUserProfileResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{18} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{20} } func (x *UpdateUserProfileResponse) GetUser() *User { @@ -1646,7 +1842,7 @@ type ChangeUserCountryRequest struct { func (x *ChangeUserCountryRequest) Reset() { *x = ChangeUserCountryRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[19] + mi := &file_proto_user_v1_user_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1658,7 +1854,7 @@ func (x *ChangeUserCountryRequest) String() string { func (*ChangeUserCountryRequest) ProtoMessage() {} func (x *ChangeUserCountryRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[19] + mi := &file_proto_user_v1_user_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1671,7 +1867,7 @@ func (x *ChangeUserCountryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeUserCountryRequest.ProtoReflect.Descriptor instead. func (*ChangeUserCountryRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{19} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{21} } func (x *ChangeUserCountryRequest) GetMeta() *RequestMeta { @@ -1707,7 +1903,7 @@ type ChangeUserCountryResponse struct { func (x *ChangeUserCountryResponse) Reset() { *x = ChangeUserCountryResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[20] + mi := &file_proto_user_v1_user_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1719,7 +1915,7 @@ func (x *ChangeUserCountryResponse) String() string { func (*ChangeUserCountryResponse) ProtoMessage() {} func (x *ChangeUserCountryResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[20] + mi := &file_proto_user_v1_user_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1732,7 +1928,7 @@ func (x *ChangeUserCountryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeUserCountryResponse.ProtoReflect.Descriptor instead. func (*ChangeUserCountryResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{20} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{22} } func (x *ChangeUserCountryResponse) GetUser() *User { @@ -1766,7 +1962,7 @@ type CompleteOnboardingRequest struct { func (x *CompleteOnboardingRequest) Reset() { *x = CompleteOnboardingRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[21] + mi := &file_proto_user_v1_user_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1778,7 +1974,7 @@ func (x *CompleteOnboardingRequest) String() string { func (*CompleteOnboardingRequest) ProtoMessage() {} func (x *CompleteOnboardingRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[21] + mi := &file_proto_user_v1_user_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1791,7 +1987,7 @@ func (x *CompleteOnboardingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CompleteOnboardingRequest.ProtoReflect.Descriptor instead. func (*CompleteOnboardingRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{21} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{23} } func (x *CompleteOnboardingRequest) GetMeta() *RequestMeta { @@ -1859,7 +2055,7 @@ type CompleteOnboardingResponse struct { func (x *CompleteOnboardingResponse) Reset() { *x = CompleteOnboardingResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[22] + mi := &file_proto_user_v1_user_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1871,7 +2067,7 @@ func (x *CompleteOnboardingResponse) String() string { func (*CompleteOnboardingResponse) ProtoMessage() {} func (x *CompleteOnboardingResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[22] + mi := &file_proto_user_v1_user_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1884,7 +2080,7 @@ func (x *CompleteOnboardingResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CompleteOnboardingResponse.ProtoReflect.Descriptor instead. func (*CompleteOnboardingResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{22} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{24} } func (x *CompleteOnboardingResponse) GetUser() *User { @@ -1949,7 +2145,7 @@ type BindPushTokenRequest struct { func (x *BindPushTokenRequest) Reset() { *x = BindPushTokenRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[23] + mi := &file_proto_user_v1_user_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1961,7 +2157,7 @@ func (x *BindPushTokenRequest) String() string { func (*BindPushTokenRequest) ProtoMessage() {} func (x *BindPushTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[23] + mi := &file_proto_user_v1_user_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1974,7 +2170,7 @@ func (x *BindPushTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BindPushTokenRequest.ProtoReflect.Descriptor instead. func (*BindPushTokenRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{23} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{25} } func (x *BindPushTokenRequest) GetMeta() *RequestMeta { @@ -2051,7 +2247,7 @@ type BindPushTokenResponse struct { func (x *BindPushTokenResponse) Reset() { *x = BindPushTokenResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[24] + mi := &file_proto_user_v1_user_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2063,7 +2259,7 @@ func (x *BindPushTokenResponse) String() string { func (*BindPushTokenResponse) ProtoMessage() {} func (x *BindPushTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[24] + mi := &file_proto_user_v1_user_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2076,7 +2272,7 @@ func (x *BindPushTokenResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BindPushTokenResponse.ProtoReflect.Descriptor instead. func (*BindPushTokenResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{24} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{26} } func (x *BindPushTokenResponse) GetBound() bool { @@ -2108,7 +2304,7 @@ type DeletePushTokenRequest struct { func (x *DeletePushTokenRequest) Reset() { *x = DeletePushTokenRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[25] + mi := &file_proto_user_v1_user_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2120,7 +2316,7 @@ func (x *DeletePushTokenRequest) String() string { func (*DeletePushTokenRequest) ProtoMessage() {} func (x *DeletePushTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[25] + mi := &file_proto_user_v1_user_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2133,7 +2329,7 @@ func (x *DeletePushTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletePushTokenRequest.ProtoReflect.Descriptor instead. func (*DeletePushTokenRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{25} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{27} } func (x *DeletePushTokenRequest) GetMeta() *RequestMeta { @@ -2175,7 +2371,7 @@ type DeletePushTokenResponse struct { func (x *DeletePushTokenResponse) Reset() { *x = DeletePushTokenResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[26] + mi := &file_proto_user_v1_user_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2187,7 +2383,7 @@ func (x *DeletePushTokenResponse) String() string { func (*DeletePushTokenResponse) ProtoMessage() {} func (x *DeletePushTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[26] + mi := &file_proto_user_v1_user_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2200,7 +2396,7 @@ func (x *DeletePushTokenResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletePushTokenResponse.ProtoReflect.Descriptor instead. func (*DeletePushTokenResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{26} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{28} } func (x *DeletePushTokenResponse) GetDeleted() bool { @@ -2240,7 +2436,7 @@ type Country struct { func (x *Country) Reset() { *x = Country{} - mi := &file_proto_user_v1_user_proto_msgTypes[27] + mi := &file_proto_user_v1_user_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2252,7 +2448,7 @@ func (x *Country) String() string { func (*Country) ProtoMessage() {} func (x *Country) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[27] + mi := &file_proto_user_v1_user_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2265,7 +2461,7 @@ func (x *Country) ProtoReflect() protoreflect.Message { // Deprecated: Use Country.ProtoReflect.Descriptor instead. func (*Country) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{27} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{29} } func (x *Country) GetCountryId() int64 { @@ -2378,7 +2574,7 @@ type Region struct { func (x *Region) Reset() { *x = Region{} - mi := &file_proto_user_v1_user_proto_msgTypes[28] + mi := &file_proto_user_v1_user_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2390,7 +2586,7 @@ func (x *Region) String() string { func (*Region) ProtoMessage() {} func (x *Region) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[28] + mi := &file_proto_user_v1_user_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2403,7 +2599,7 @@ func (x *Region) ProtoReflect() protoreflect.Message { // Deprecated: Use Region.ProtoReflect.Descriptor instead. func (*Region) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{28} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{30} } func (x *Region) GetRegionId() int64 { @@ -2480,7 +2676,7 @@ type ListCountriesRequest struct { func (x *ListCountriesRequest) Reset() { *x = ListCountriesRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[29] + mi := &file_proto_user_v1_user_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2492,7 +2688,7 @@ func (x *ListCountriesRequest) String() string { func (*ListCountriesRequest) ProtoMessage() {} func (x *ListCountriesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[29] + mi := &file_proto_user_v1_user_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2505,7 +2701,7 @@ func (x *ListCountriesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListCountriesRequest.ProtoReflect.Descriptor instead. func (*ListCountriesRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{29} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{31} } func (x *ListCountriesRequest) GetMeta() *RequestMeta { @@ -2532,7 +2728,7 @@ type ListCountriesResponse struct { func (x *ListCountriesResponse) Reset() { *x = ListCountriesResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[30] + mi := &file_proto_user_v1_user_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2544,7 +2740,7 @@ func (x *ListCountriesResponse) String() string { func (*ListCountriesResponse) ProtoMessage() {} func (x *ListCountriesResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[30] + mi := &file_proto_user_v1_user_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2557,7 +2753,7 @@ func (x *ListCountriesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListCountriesResponse.ProtoReflect.Descriptor instead. func (*ListCountriesResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{30} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{32} } func (x *ListCountriesResponse) GetCountries() []*Country { @@ -2587,7 +2783,7 @@ type UpdateCountryRequest struct { func (x *UpdateCountryRequest) Reset() { *x = UpdateCountryRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[31] + mi := &file_proto_user_v1_user_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2599,7 +2795,7 @@ func (x *UpdateCountryRequest) String() string { func (*UpdateCountryRequest) ProtoMessage() {} func (x *UpdateCountryRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[31] + mi := &file_proto_user_v1_user_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2612,7 +2808,7 @@ func (x *UpdateCountryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateCountryRequest.ProtoReflect.Descriptor instead. func (*UpdateCountryRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{31} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{33} } func (x *UpdateCountryRequest) GetMeta() *RequestMeta { @@ -2695,7 +2891,7 @@ type CountryResponse struct { func (x *CountryResponse) Reset() { *x = CountryResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[32] + mi := &file_proto_user_v1_user_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2707,7 +2903,7 @@ func (x *CountryResponse) String() string { func (*CountryResponse) ProtoMessage() {} func (x *CountryResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[32] + mi := &file_proto_user_v1_user_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2720,7 +2916,7 @@ func (x *CountryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CountryResponse.ProtoReflect.Descriptor instead. func (*CountryResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{32} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{34} } func (x *CountryResponse) GetCountry() *Country { @@ -2740,7 +2936,7 @@ type ListRegistrationCountriesRequest struct { func (x *ListRegistrationCountriesRequest) Reset() { *x = ListRegistrationCountriesRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[33] + mi := &file_proto_user_v1_user_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2752,7 +2948,7 @@ func (x *ListRegistrationCountriesRequest) String() string { func (*ListRegistrationCountriesRequest) ProtoMessage() {} func (x *ListRegistrationCountriesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[33] + mi := &file_proto_user_v1_user_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2765,7 +2961,7 @@ func (x *ListRegistrationCountriesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRegistrationCountriesRequest.ProtoReflect.Descriptor instead. func (*ListRegistrationCountriesRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{33} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{35} } func (x *ListRegistrationCountriesRequest) GetMeta() *RequestMeta { @@ -2785,7 +2981,7 @@ type ListRegistrationCountriesResponse struct { func (x *ListRegistrationCountriesResponse) Reset() { *x = ListRegistrationCountriesResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[34] + mi := &file_proto_user_v1_user_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2797,7 +2993,7 @@ func (x *ListRegistrationCountriesResponse) String() string { func (*ListRegistrationCountriesResponse) ProtoMessage() {} func (x *ListRegistrationCountriesResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[34] + mi := &file_proto_user_v1_user_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2810,7 +3006,7 @@ func (x *ListRegistrationCountriesResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ListRegistrationCountriesResponse.ProtoReflect.Descriptor instead. func (*ListRegistrationCountriesResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{34} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{36} } func (x *ListRegistrationCountriesResponse) GetCountries() []*Country { @@ -2831,7 +3027,7 @@ type ListRegionsRequest struct { func (x *ListRegionsRequest) Reset() { *x = ListRegionsRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[35] + mi := &file_proto_user_v1_user_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2843,7 +3039,7 @@ func (x *ListRegionsRequest) String() string { func (*ListRegionsRequest) ProtoMessage() {} func (x *ListRegionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[35] + mi := &file_proto_user_v1_user_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2856,7 +3052,7 @@ func (x *ListRegionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRegionsRequest.ProtoReflect.Descriptor instead. func (*ListRegionsRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{35} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{37} } func (x *ListRegionsRequest) GetMeta() *RequestMeta { @@ -2883,7 +3079,7 @@ type ListRegionsResponse struct { func (x *ListRegionsResponse) Reset() { *x = ListRegionsResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[36] + mi := &file_proto_user_v1_user_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2895,7 +3091,7 @@ func (x *ListRegionsResponse) String() string { func (*ListRegionsResponse) ProtoMessage() {} func (x *ListRegionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[36] + mi := &file_proto_user_v1_user_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2908,7 +3104,7 @@ func (x *ListRegionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRegionsResponse.ProtoReflect.Descriptor instead. func (*ListRegionsResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{36} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{38} } func (x *ListRegionsResponse) GetRegions() []*Region { @@ -2929,7 +3125,7 @@ type GetRegionRequest struct { func (x *GetRegionRequest) Reset() { *x = GetRegionRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[37] + mi := &file_proto_user_v1_user_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2941,7 +3137,7 @@ func (x *GetRegionRequest) String() string { func (*GetRegionRequest) ProtoMessage() {} func (x *GetRegionRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[37] + mi := &file_proto_user_v1_user_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2954,7 +3150,7 @@ func (x *GetRegionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRegionRequest.ProtoReflect.Descriptor instead. func (*GetRegionRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{37} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{39} } func (x *GetRegionRequest) GetMeta() *RequestMeta { @@ -2987,7 +3183,7 @@ type UpdateRegionRequest struct { func (x *UpdateRegionRequest) Reset() { *x = UpdateRegionRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[38] + mi := &file_proto_user_v1_user_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2999,7 +3195,7 @@ func (x *UpdateRegionRequest) String() string { func (*UpdateRegionRequest) ProtoMessage() {} func (x *UpdateRegionRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[38] + mi := &file_proto_user_v1_user_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3012,7 +3208,7 @@ func (x *UpdateRegionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateRegionRequest.ProtoReflect.Descriptor instead. func (*UpdateRegionRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{38} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{40} } func (x *UpdateRegionRequest) GetMeta() *RequestMeta { @@ -3070,7 +3266,7 @@ type ReplaceRegionCountriesRequest struct { func (x *ReplaceRegionCountriesRequest) Reset() { *x = ReplaceRegionCountriesRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[39] + mi := &file_proto_user_v1_user_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3082,7 +3278,7 @@ func (x *ReplaceRegionCountriesRequest) String() string { func (*ReplaceRegionCountriesRequest) ProtoMessage() {} func (x *ReplaceRegionCountriesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[39] + mi := &file_proto_user_v1_user_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3095,7 +3291,7 @@ func (x *ReplaceRegionCountriesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReplaceRegionCountriesRequest.ProtoReflect.Descriptor instead. func (*ReplaceRegionCountriesRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{39} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{41} } func (x *ReplaceRegionCountriesRequest) GetMeta() *RequestMeta { @@ -3136,7 +3332,7 @@ type RegionResponse struct { func (x *RegionResponse) Reset() { *x = RegionResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[40] + mi := &file_proto_user_v1_user_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3148,7 +3344,7 @@ func (x *RegionResponse) String() string { func (*RegionResponse) ProtoMessage() {} func (x *RegionResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[40] + mi := &file_proto_user_v1_user_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3161,7 +3357,7 @@ func (x *RegionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RegionResponse.ProtoReflect.Descriptor instead. func (*RegionResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{40} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{42} } func (x *RegionResponse) GetRegion() *Region { @@ -3188,7 +3384,7 @@ type UserIdentity struct { func (x *UserIdentity) Reset() { *x = UserIdentity{} - mi := &file_proto_user_v1_user_proto_msgTypes[41] + mi := &file_proto_user_v1_user_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3200,7 +3396,7 @@ func (x *UserIdentity) String() string { func (*UserIdentity) ProtoMessage() {} func (x *UserIdentity) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[41] + mi := &file_proto_user_v1_user_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3213,7 +3409,7 @@ func (x *UserIdentity) ProtoReflect() protoreflect.Message { // Deprecated: Use UserIdentity.ProtoReflect.Descriptor instead. func (*UserIdentity) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{41} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{43} } func (x *UserIdentity) GetUserId() int64 { @@ -3277,7 +3473,7 @@ type GetUserIdentityRequest struct { func (x *GetUserIdentityRequest) Reset() { *x = GetUserIdentityRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[42] + mi := &file_proto_user_v1_user_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3289,7 +3485,7 @@ func (x *GetUserIdentityRequest) String() string { func (*GetUserIdentityRequest) ProtoMessage() {} func (x *GetUserIdentityRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[42] + mi := &file_proto_user_v1_user_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3302,7 +3498,7 @@ func (x *GetUserIdentityRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUserIdentityRequest.ProtoReflect.Descriptor instead. func (*GetUserIdentityRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{42} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{44} } func (x *GetUserIdentityRequest) GetMeta() *RequestMeta { @@ -3330,7 +3526,7 @@ type GetUserIdentityResponse struct { func (x *GetUserIdentityResponse) Reset() { *x = GetUserIdentityResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[43] + mi := &file_proto_user_v1_user_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3342,7 +3538,7 @@ func (x *GetUserIdentityResponse) String() string { func (*GetUserIdentityResponse) ProtoMessage() {} func (x *GetUserIdentityResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[43] + mi := &file_proto_user_v1_user_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3355,7 +3551,7 @@ func (x *GetUserIdentityResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetUserIdentityResponse.ProtoReflect.Descriptor instead. func (*GetUserIdentityResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{43} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{45} } func (x *GetUserIdentityResponse) GetIdentity() *UserIdentity { @@ -3377,7 +3573,7 @@ type ResolveDisplayUserIDRequest struct { func (x *ResolveDisplayUserIDRequest) Reset() { *x = ResolveDisplayUserIDRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[44] + mi := &file_proto_user_v1_user_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3389,7 +3585,7 @@ func (x *ResolveDisplayUserIDRequest) String() string { func (*ResolveDisplayUserIDRequest) ProtoMessage() {} func (x *ResolveDisplayUserIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[44] + mi := &file_proto_user_v1_user_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3402,7 +3598,7 @@ func (x *ResolveDisplayUserIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResolveDisplayUserIDRequest.ProtoReflect.Descriptor instead. func (*ResolveDisplayUserIDRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{44} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{46} } func (x *ResolveDisplayUserIDRequest) GetMeta() *RequestMeta { @@ -3430,7 +3626,7 @@ type ResolveDisplayUserIDResponse struct { func (x *ResolveDisplayUserIDResponse) Reset() { *x = ResolveDisplayUserIDResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[45] + mi := &file_proto_user_v1_user_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3442,7 +3638,7 @@ func (x *ResolveDisplayUserIDResponse) String() string { func (*ResolveDisplayUserIDResponse) ProtoMessage() {} func (x *ResolveDisplayUserIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[45] + mi := &file_proto_user_v1_user_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3455,7 +3651,7 @@ func (x *ResolveDisplayUserIDResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResolveDisplayUserIDResponse.ProtoReflect.Descriptor instead. func (*ResolveDisplayUserIDResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{45} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{47} } func (x *ResolveDisplayUserIDResponse) GetIdentity() *UserIdentity { @@ -3480,7 +3676,7 @@ type ChangeDisplayUserIDRequest struct { func (x *ChangeDisplayUserIDRequest) Reset() { *x = ChangeDisplayUserIDRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[46] + mi := &file_proto_user_v1_user_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3492,7 +3688,7 @@ func (x *ChangeDisplayUserIDRequest) String() string { func (*ChangeDisplayUserIDRequest) ProtoMessage() {} func (x *ChangeDisplayUserIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[46] + mi := &file_proto_user_v1_user_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3505,7 +3701,7 @@ func (x *ChangeDisplayUserIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeDisplayUserIDRequest.ProtoReflect.Descriptor instead. func (*ChangeDisplayUserIDRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{46} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{48} } func (x *ChangeDisplayUserIDRequest) GetMeta() *RequestMeta { @@ -3554,7 +3750,7 @@ type ChangeDisplayUserIDResponse struct { func (x *ChangeDisplayUserIDResponse) Reset() { *x = ChangeDisplayUserIDResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[47] + mi := &file_proto_user_v1_user_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3566,7 +3762,7 @@ func (x *ChangeDisplayUserIDResponse) String() string { func (*ChangeDisplayUserIDResponse) ProtoMessage() {} func (x *ChangeDisplayUserIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[47] + mi := &file_proto_user_v1_user_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3579,7 +3775,7 @@ func (x *ChangeDisplayUserIDResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeDisplayUserIDResponse.ProtoReflect.Descriptor instead. func (*ChangeDisplayUserIDResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{47} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{49} } func (x *ChangeDisplayUserIDResponse) GetIdentity() *UserIdentity { @@ -3604,7 +3800,7 @@ type ApplyPrettyDisplayUserIDRequest struct { func (x *ApplyPrettyDisplayUserIDRequest) Reset() { *x = ApplyPrettyDisplayUserIDRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[48] + mi := &file_proto_user_v1_user_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3616,7 +3812,7 @@ func (x *ApplyPrettyDisplayUserIDRequest) String() string { func (*ApplyPrettyDisplayUserIDRequest) ProtoMessage() {} func (x *ApplyPrettyDisplayUserIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[48] + mi := &file_proto_user_v1_user_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3629,7 +3825,7 @@ func (x *ApplyPrettyDisplayUserIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyPrettyDisplayUserIDRequest.ProtoReflect.Descriptor instead. func (*ApplyPrettyDisplayUserIDRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{48} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{50} } func (x *ApplyPrettyDisplayUserIDRequest) GetMeta() *RequestMeta { @@ -3679,7 +3875,7 @@ type ApplyPrettyDisplayUserIDResponse struct { func (x *ApplyPrettyDisplayUserIDResponse) Reset() { *x = ApplyPrettyDisplayUserIDResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[49] + mi := &file_proto_user_v1_user_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3691,7 +3887,7 @@ func (x *ApplyPrettyDisplayUserIDResponse) String() string { func (*ApplyPrettyDisplayUserIDResponse) ProtoMessage() {} func (x *ApplyPrettyDisplayUserIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[49] + mi := &file_proto_user_v1_user_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3704,7 +3900,7 @@ func (x *ApplyPrettyDisplayUserIDResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyPrettyDisplayUserIDResponse.ProtoReflect.Descriptor instead. func (*ApplyPrettyDisplayUserIDResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{49} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{51} } func (x *ApplyPrettyDisplayUserIDResponse) GetIdentity() *UserIdentity { @@ -3734,7 +3930,7 @@ type ExpirePrettyDisplayUserIDRequest struct { func (x *ExpirePrettyDisplayUserIDRequest) Reset() { *x = ExpirePrettyDisplayUserIDRequest{} - mi := &file_proto_user_v1_user_proto_msgTypes[50] + mi := &file_proto_user_v1_user_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3746,7 +3942,7 @@ func (x *ExpirePrettyDisplayUserIDRequest) String() string { func (*ExpirePrettyDisplayUserIDRequest) ProtoMessage() {} func (x *ExpirePrettyDisplayUserIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[50] + mi := &file_proto_user_v1_user_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3759,7 +3955,7 @@ func (x *ExpirePrettyDisplayUserIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ExpirePrettyDisplayUserIDRequest.ProtoReflect.Descriptor instead. func (*ExpirePrettyDisplayUserIDRequest) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{50} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{52} } func (x *ExpirePrettyDisplayUserIDRequest) GetMeta() *RequestMeta { @@ -3794,7 +3990,7 @@ type ExpirePrettyDisplayUserIDResponse struct { func (x *ExpirePrettyDisplayUserIDResponse) Reset() { *x = ExpirePrettyDisplayUserIDResponse{} - mi := &file_proto_user_v1_user_proto_msgTypes[51] + mi := &file_proto_user_v1_user_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3806,7 +4002,7 @@ func (x *ExpirePrettyDisplayUserIDResponse) String() string { func (*ExpirePrettyDisplayUserIDResponse) ProtoMessage() {} func (x *ExpirePrettyDisplayUserIDResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_user_v1_user_proto_msgTypes[51] + mi := &file_proto_user_v1_user_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3819,7 +4015,7 @@ func (x *ExpirePrettyDisplayUserIDResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ExpirePrettyDisplayUserIDResponse.ProtoReflect.Descriptor instead. func (*ExpirePrettyDisplayUserIDResponse) Descriptor() ([]byte, []int) { - return file_proto_user_v1_user_proto_rawDescGZIP(), []int{51} + return file_proto_user_v1_user_proto_rawDescGZIP(), []int{53} } func (x *ExpirePrettyDisplayUserIDResponse) GetIdentity() *UserIdentity { @@ -3834,7 +4030,7 @@ var File_proto_user_v1_user_proto protoreflect.FileDescriptor var file_proto_user_v1_user_proto_rawDesc = []byte{ 0x0a, 0x18, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x22, 0xc1, 0x02, 0x0a, 0x0b, 0x52, 0x65, + 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x22, 0x95, 0x03, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 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, 0x16, 0x0a, 0x06, 0x63, 0x61, 0x6c, 0x6c, @@ -3854,751 +4050,807 @@ var file_proto_user_v1_user_proto_rawDesc = []byte{ 0x79, 0x49, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x22, 0xf1, 0x01, - 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x15, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x61, 0x70, 0x70, 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, 0x61, 0x70, 0x70, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, - 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, - 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, - 0x6d, 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, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x07, 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, 0x08, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, - 0x73, 0x22, 0x9d, 0x01, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x41, 0x70, 0x70, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, + 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, + 0x67, 0x75, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, + 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, + 0x65, 0x22, 0xf1, 0x01, 0x0a, 0x03, 0x41, 0x70, 0x70, 0x12, 0x15, 0x0a, 0x06, 0x61, 0x70, 0x70, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x61, 0x70, 0x70, 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, 0x61, + 0x70, 0x70, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, + 0x70, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x61, + 0x63, 0x6b, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, + 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, + 0x74, 0x66, 0x6f, 0x72, 0x6d, 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, 0x22, 0x0a, + 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x07, + 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, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0x9d, 0x01, 0x0a, 0x11, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, + 0x65, 0x41, 0x70, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, + 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x70, + 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, + 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, + 0x70, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x3a, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, + 0x41, 0x70, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x61, + 0x70, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x52, 0x03, 0x61, 0x70, + 0x70, 0x22, 0xb1, 0x08, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x12, 0x31, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x03, 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, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x26, + 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x35, 0x0a, 0x17, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, + 0x74, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, + 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2f, 0x0a, + 0x14, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, + 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x64, 0x69, 0x73, + 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x3f, + 0x0a, 0x1d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x18, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, + 0x65, 0x72, 0x49, 0x64, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x4d, 0x73, 0x12, + 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x67, + 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x65, 0x6e, + 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x16, 0x0a, + 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, + 0x76, 0x61, 0x74, 0x61, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x69, 0x72, 0x74, 0x68, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x69, 0x72, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x6c, + 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, + 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, + 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x72, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, + 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, + 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, + 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x67, 0x69, + 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, + 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x67, + 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x70, 0x72, + 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, + 0x15, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x35, 0x0a, 0x17, 0x70, 0x72, 0x6f, 0x66, 0x69, + 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, + 0x6d, 0x73, 0x18, 0x16, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, + 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x2b, + 0x0a, 0x11, 0x6f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x17, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x6e, 0x62, 0x6f, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, + 0x73, 0x6f, 0x5f, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x18, 0x18, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x69, 0x73, 0x6f, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x12, 0x2c, 0x0a, 0x12, + 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x19, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x1a, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x45, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x1b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x35, + 0x0a, 0x06, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, + 0x6e, 0x76, 0x69, 0x74, 0x65, 0x4f, 0x76, 0x65, 0x72, 0x76, 0x69, 0x65, 0x77, 0x52, 0x06, 0x69, + 0x6e, 0x76, 0x69, 0x74, 0x65, 0x22, 0xed, 0x01, 0x0a, 0x0e, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, + 0x4f, 0x76, 0x65, 0x72, 0x76, 0x69, 0x65, 0x77, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x79, 0x5f, 0x69, + 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x6d, 0x79, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x25, + 0x0a, 0x0e, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x76, + 0x69, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x5f, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x49, 0x6e, 0x76, 0x69, 0x74, + 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x1b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, + 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, + 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x18, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, + 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x22, 0x6e, 0x0a, 0x0d, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x42, + 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x1f, 0x0a, 0x0b, + 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x26, 0x0a, + 0x0f, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x55, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xad, 0x02, 0x0a, 0x14, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x69, + 0x63, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x17, + 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x65, 0x61, 0x74, 0x5f, + 0x6f, 0x63, 0x63, 0x75, 0x70, 0x69, 0x65, 0x64, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0e, 0x73, 0x65, 0x61, 0x74, 0x4f, 0x63, 0x63, 0x75, 0x70, 0x69, 0x65, 0x64, 0x4d, + 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x6d, 0x69, 0x63, 0x5f, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x5f, + 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x69, 0x63, 0x4f, 0x6e, 0x6c, + 0x69, 0x6e, 0x65, 0x4d, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0f, 0x66, 0x69, + 0x72, 0x73, 0x74, 0x5f, 0x6d, 0x69, 0x63, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0c, 0x66, 0x69, 0x72, 0x73, 0x74, 0x4d, 0x69, 0x63, 0x41, 0x74, 0x4d, + 0x73, 0x12, 0x23, 0x0a, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x69, 0x63, 0x5f, 0x61, 0x74, + 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x4d, + 0x69, 0x63, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, + 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, + 0x70, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x69, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x4d, 0x69, 0x63, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, - 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x63, 0x6b, 0x61, - 0x67, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, - 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, - 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, - 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, - 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, - 0x65, 0x22, 0x3a, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x41, 0x70, 0x70, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x61, 0x70, 0x70, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, - 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x52, 0x03, 0x61, 0x70, 0x70, 0x22, 0xb1, 0x08, - 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, - 0x31, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x19, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, - 0x5f, 0x6d, 0x73, 0x18, 0x03, 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, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x69, - 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, - 0x49, 0x64, 0x12, 0x35, 0x0a, 0x17, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x64, 0x69, - 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x44, 0x69, 0x73, 0x70, - 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x14, 0x64, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x5f, 0x6b, 0x69, 0x6e, - 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, - 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x3f, 0x0a, 0x1d, 0x64, 0x69, - 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x5f, 0x65, 0x78, - 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x18, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, - 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x75, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, - 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, - 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x76, 0x61, - 0x74, 0x61, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, - 0x72, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x69, 0x72, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x62, 0x69, 0x72, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, - 0x61, 0x67, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, - 0x61, 0x67, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, - 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, - 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, - 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, - 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x11, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x12, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x44, 0x69, 0x73, 0x70, - 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, - 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x12, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, - 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x63, - 0x6f, 0x64, 0x65, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x6f, - 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x67, 0x69, - 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, - 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x15, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x64, 0x12, 0x35, 0x0a, 0x17, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x16, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x6e, - 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x17, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, - 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x6f, 0x5f, 0x6e, - 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x18, 0x18, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x73, - 0x6f, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x68, 0x6f, 0x6e, - 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x19, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, - 0x79, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0e, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, - 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x1b, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x35, 0x0a, 0x06, 0x69, 0x6e, - 0x76, 0x69, 0x74, 0x65, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x76, 0x69, 0x74, - 0x65, 0x4f, 0x76, 0x65, 0x72, 0x76, 0x69, 0x65, 0x77, 0x52, 0x06, 0x69, 0x6e, 0x76, 0x69, 0x74, - 0x65, 0x22, 0xed, 0x01, 0x0a, 0x0e, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x4f, 0x76, 0x65, 0x72, - 0x76, 0x69, 0x65, 0x77, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x79, 0x5f, 0x69, 0x6e, 0x76, 0x69, 0x74, - 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x79, - 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e, - 0x76, 0x69, 0x74, 0x65, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0d, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x69, 0x6e, - 0x76, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x10, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x1b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x69, 0x6e, 0x76, 0x69, - 0x74, 0x65, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x5f, 0x63, 0x6f, 0x69, - 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x18, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x49, 0x6e, - 0x76, 0x69, 0x74, 0x65, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x43, 0x6f, 0x69, - 0x6e, 0x22, 0x6e, 0x0a, 0x0d, 0x49, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, - 0x6e, 0x67, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x05, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x76, 0x69, - 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, - 0x6e, 0x76, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x6e, 0x76, - 0x69, 0x74, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0d, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, - 0x64, 0x22, 0xad, 0x02, 0x0a, 0x14, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x69, 0x63, 0x4c, 0x69, 0x66, - 0x65, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, - 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, - 0x72, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x65, 0x61, 0x74, 0x5f, 0x6f, 0x63, 0x63, 0x75, - 0x70, 0x69, 0x65, 0x64, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x73, - 0x65, 0x61, 0x74, 0x4f, 0x63, 0x63, 0x75, 0x70, 0x69, 0x65, 0x64, 0x4d, 0x73, 0x12, 0x22, 0x0a, - 0x0d, 0x6d, 0x69, 0x63, 0x5f, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x69, 0x63, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x4d, - 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0f, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, - 0x6d, 0x69, 0x63, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0c, 0x66, 0x69, 0x72, 0x73, 0x74, 0x4d, 0x69, 0x63, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x23, 0x0a, - 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x69, 0x63, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x4d, 0x69, 0x63, 0x41, 0x74, - 0x4d, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, - 0x5f, 0x6d, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, - 0x64, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, - 0x65, 0x22, 0x69, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x69, 0x63, 0x4c, - 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x22, 0x5c, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x69, 0x63, 0x4c, 0x69, + 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x69, 0x63, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, + 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0xb3, + 0x02, 0x0a, 0x10, 0x43, 0x72, 0x6f, 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, - 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x5c, 0x0a, 0x1f, - 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x69, 0x63, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, - 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x39, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, - 0x73, 0x65, 0x72, 0x4d, 0x69, 0x63, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x96, 0x04, 0x0a, 0x09, 0x41, - 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, - 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, - 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, - 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x65, 0x78, 0x70, 0x69, - 0x72, 0x65, 0x73, 0x5f, 0x69, 0x6e, 0x5f, 0x73, 0x65, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0c, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x49, 0x6e, 0x53, 0x65, 0x63, 0x12, 0x1d, - 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x26, 0x0a, - 0x0f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, - 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x35, 0x0a, 0x17, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, - 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x44, - 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x14, - 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x5f, - 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x64, 0x69, 0x73, 0x70, - 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x3f, 0x0a, - 0x1d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x18, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, - 0x72, 0x49, 0x64, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x2b, - 0x0a, 0x11, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x66, 0x69, - 0x6c, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x6f, - 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, - 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, - 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, - 0x6f, 0x64, 0x65, 0x22, 0x59, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, - 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x3a, - 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x27, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x13, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x61, 0x0a, 0x14, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, - 0x74, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, 0x22, 0xad, 0x01, - 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, - 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x47, 0x65, 0x74, 0x55, - 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x55, 0x73, 0x65, - 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x1a, 0x4d, - 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, - 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, - 0x65, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xe1, 0x01, - 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, - 0x6d, 0x65, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, - 0x63, 0x6f, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x74, 0x61, 0x72, 0x67, - 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x63, 0x75, 0x72, 0x73, 0x6f, - 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0c, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, - 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 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, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, - 0x79, 0x22, 0x73, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, - 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, - 0x49, 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x13, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x63, 0x75, 0x72, 0x73, - 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x10, 0x6e, 0x65, 0x78, 0x74, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, - 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x22, 0x86, 0x02, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, - 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x08, - 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, - 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, - 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, - 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x62, 0x69, - 0x72, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x62, 0x69, 0x72, - 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x88, - 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x42, - 0x09, 0x0a, 0x07, 0x5f, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x62, - 0x69, 0x72, 0x74, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x22, - 0x44, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, - 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x04, - 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x7d, 0x0a, 0x18, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x55, - 0x73, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, - 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x72, 0x79, 0x22, 0x7e, 0x0a, 0x19, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x55, 0x73, - 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x27, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x13, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x19, 0x6e, 0x65, - 0x78, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, - 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x6e, - 0x65, 0x78, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, - 0x41, 0x74, 0x4d, 0x73, 0x22, 0xeb, 0x01, 0x0a, 0x19, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, - 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x12, - 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x65, 0x6e, - 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, - 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x43, 0x6f, - 0x64, 0x65, 0x22, 0xbc, 0x02, 0x0a, 0x1a, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4f, - 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x27, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x13, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x11, 0x70, 0x72, - 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x43, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x35, 0x0a, 0x17, 0x70, 0x72, 0x6f, 0x66, 0x69, - 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, - 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, - 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x2b, + 0x65, 0x74, 0x61, 0x12, 0x15, 0x0a, 0x06, 0x72, 0x75, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x75, 0x6e, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, + 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x61, 0x74, 0x63, 0x68, + 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x62, 0x61, 0x74, + 0x63, 0x68, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1e, 0x0a, 0x0b, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, + 0x74, 0x6c, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6c, 0x6f, 0x63, + 0x6b, 0x54, 0x74, 0x6c, 0x4d, 0x73, 0x12, 0x3a, 0x0a, 0x1a, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, + 0x67, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, + 0x65, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x16, 0x70, 0x65, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4d, 0x61, 0x78, 0x41, 0x67, 0x65, + 0x4d, 0x73, 0x12, 0x40, 0x0a, 0x1d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x69, 0x6e, 0x67, + 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, + 0x5f, 0x6d, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x19, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x73, 0x68, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x78, 0x41, + 0x67, 0x65, 0x4d, 0x73, 0x22, 0xc6, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x6f, 0x6e, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6c, + 0x61, 0x69, 0x6d, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x0c, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x27, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, + 0x73, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x0c, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, + 0x0d, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0x96, 0x04, + 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, + 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, + 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, + 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x69, 0x6e, 0x5f, 0x73, 0x65, 0x63, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0c, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x49, 0x6e, 0x53, 0x65, + 0x63, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x35, 0x0a, 0x17, 0x64, 0x65, 0x66, 0x61, + 0x75, 0x6c, 0x74, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, + 0x6c, 0x74, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, + 0x2f, 0x0a, 0x14, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x64, + 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x4b, 0x69, 0x6e, 0x64, + 0x12, 0x3f, 0x0a, 0x1d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x74, 0x5f, 0x6d, + 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x18, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x4d, + 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, + 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x6e, 0x62, 0x6f, 0x61, - 0x72, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2e, 0x0a, 0x05, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x06, 0x69, - 0x6e, 0x76, 0x69, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x68, 0x79, - 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x76, 0x69, - 0x74, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x06, 0x69, 0x6e, 0x76, 0x69, 0x74, - 0x65, 0x22, 0xac, 0x02, 0x0a, 0x14, 0x42, 0x69, 0x6e, 0x64, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, - 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, - 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, - 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, - 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, - 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, - 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, - 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, - 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x70, 0x70, 0x5f, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x70, - 0x70, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, - 0x75, 0x61, 0x67, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, - 0x75, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, - 0x22, 0x51, 0x0a, 0x15, 0x42, 0x69, 0x6e, 0x64, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x6f, 0x75, - 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x12, - 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, - 0x74, 0x4d, 0x73, 0x22, 0x9d, 0x01, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x75, - 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, - 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, - 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x73, 0x68, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x22, 0x57, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x75, 0x73, - 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, - 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xbe, 0x03, 0x0a, - 0x07, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x72, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x30, 0x0a, - 0x14, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x72, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x06, 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, - 0x07, 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, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x6f, 0x5f, 0x61, 0x6c, - 0x70, 0x68, 0x61, 0x33, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x73, 0x6f, 0x41, - 0x6c, 0x70, 0x68, 0x61, 0x33, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x6f, 0x5f, 0x6e, 0x75, 0x6d, - 0x65, 0x72, 0x69, 0x63, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x73, 0x6f, 0x4e, - 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x5f, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0b, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x10, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, - 0x43, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, - 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x12, - 0x0a, 0x04, 0x66, 0x6c, 0x61, 0x67, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, - 0x61, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0e, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x92, 0x02, - 0x0a, 0x06, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x67, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, - 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x67, 0x69, - 0x6f, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 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, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, - 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, - 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x06, - 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, 0x07, 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, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, - 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, - 0x64, 0x65, 0x22, 0x71, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, - 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, - 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, - 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x07, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x65, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, - 0x0a, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x16, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x72, 0x69, 0x65, 0x73, 0x22, 0x85, 0x03, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, - 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, - 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1d, 0x0a, - 0x0a, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x30, 0x0a, 0x14, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, - 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, - 0x05, 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, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, - 0x6f, 0x5f, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x33, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x69, 0x73, 0x6f, 0x41, 0x6c, 0x70, 0x68, 0x61, 0x33, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x6f, - 0x5f, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, - 0x69, 0x73, 0x6f, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x68, - 0x6f, 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x61, 0x67, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x61, 0x67, 0x22, 0x43, 0x0a, 0x0f, - 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x30, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, - 0x79, 0x22, 0x52, 0x0a, 0x20, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, + 0x74, 0x75, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x6e, 0x62, 0x6f, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, + 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, + 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x59, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, + 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, + 0x64, 0x22, 0x3a, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x61, 0x0a, + 0x14, 0x42, 0x61, 0x74, 0x63, 0x68, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, - 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0x59, 0x0a, 0x21, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, - 0x22, 0x5c, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, - 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, - 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x46, - 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, - 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x72, - 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x5f, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, - 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, - 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, - 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, - 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, - 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xe0, 0x01, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, - 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, - 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 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, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x18, - 0x05, 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, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xb4, 0x01, 0x0a, 0x1d, 0x52, - 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, - 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, - 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, - 0x64, 0x22, 0x3f, 0x0a, 0x0e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, - 0x6f, 0x6e, 0x22, 0xab, 0x02, 0x0a, 0x0c, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, - 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, - 0x65, 0x72, 0x49, 0x64, 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, 0x35, 0x0a, 0x17, - 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, - 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x64, - 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, - 0x72, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x14, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, - 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x11, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, - 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x3f, 0x0a, 0x1d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, - 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, - 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x18, 0x64, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, - 0x73, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, - 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, - 0x22, 0x61, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, - 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, - 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, - 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, - 0x72, 0x49, 0x64, 0x22, 0x52, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, - 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x75, 0x0a, 0x1b, 0x52, 0x65, 0x73, 0x6f, 0x6c, - 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, - 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, - 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, - 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x57, - 0x0a, 0x1c, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, - 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, - 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x69, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xd6, 0x01, 0x0a, 0x1a, 0x43, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, + 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x19, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, + 0x22, 0xad, 0x01, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x75, 0x73, + 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x47, + 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, + 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, + 0x73, 0x1a, 0x4d, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x29, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x13, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x22, 0xe1, 0x01, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, + 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x63, 0x75, + 0x72, 0x73, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0c, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 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, 0x18, 0x0a, 0x07, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x72, 0x79, 0x22, 0x73, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x49, 0x44, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x75, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, 0x12, 0x2d, 0x0a, 0x13, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x63, + 0x75, 0x72, 0x73, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x10, 0x6e, 0x65, 0x78, 0x74, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x55, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x22, 0x86, 0x02, 0x0a, 0x18, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, - 0x2d, 0x0a, 0x13, 0x6e, 0x65, 0x77, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, - 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6e, 0x65, - 0x77, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, - 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 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, 0x05, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, - 0x22, 0x56, 0x0a, 0x1b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, - 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x37, 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, - 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xf9, 0x01, 0x0a, 0x1f, 0x41, 0x70, 0x70, - 0x6c, 0x79, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, - 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, - 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, - 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, - 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x33, 0x0a, 0x16, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x5f, - 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x65, - 0x61, 0x73, 0x65, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x10, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x63, 0x65, 0x69, - 0x70, 0x74, 0x49, 0x64, 0x22, 0x76, 0x0a, 0x20, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x50, 0x72, 0x65, - 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x64, 0x22, 0x86, 0x01, 0x0a, - 0x20, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x1f, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, + 0x12, 0x1b, 0x0a, 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x01, 0x52, 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, + 0x05, 0x62, 0x69, 0x72, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, + 0x62, 0x69, 0x72, 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x67, 0x65, 0x6e, 0x64, + 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x06, 0x67, 0x65, 0x6e, 0x64, + 0x65, 0x72, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x42, 0x08, 0x0a, + 0x06, 0x5f, 0x62, 0x69, 0x72, 0x74, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x67, 0x65, 0x6e, 0x64, + 0x65, 0x72, 0x22, 0x44, 0x0a, 0x19, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, + 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x27, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, + 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x7d, 0x0a, 0x18, 0x43, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x55, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, + 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, + 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x7e, 0x0a, 0x19, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x55, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x38, 0x0a, + 0x19, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x5f, 0x61, 0x6c, 0x6c, + 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x15, 0x6e, 0x65, 0x78, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x6c, 0x6c, 0x6f, + 0x77, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xeb, 0x01, 0x0a, 0x19, 0x43, 0x6f, 0x6d, 0x70, + 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, + 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1a, + 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x76, + 0x61, 0x74, 0x61, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x76, 0x61, 0x74, + 0x61, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, + 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x65, + 0x6e, 0x64, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f, 0x63, + 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x76, 0x69, 0x74, + 0x65, 0x43, 0x6f, 0x64, 0x65, 0x22, 0xbc, 0x02, 0x0a, 0x1a, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x2b, 0x0a, + 0x11, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, + 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x35, 0x0a, 0x17, 0x70, 0x72, + 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x70, 0x72, 0x6f, + 0x66, 0x69, 0x6c, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, + 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x6e, + 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2e, + 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, + 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, + 0x0a, 0x06, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, + 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x49, + 0x6e, 0x76, 0x69, 0x74, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x06, 0x69, 0x6e, + 0x76, 0x69, 0x74, 0x65, 0x22, 0xac, 0x02, 0x0a, 0x14, 0x42, 0x69, 0x6e, 0x64, 0x50, 0x75, 0x73, + 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, + 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, + 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, + 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x73, 0x68, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x70, + 0x70, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x61, 0x70, 0x70, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x6c, + 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, + 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, + 0x6f, 0x6e, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, + 0x6f, 0x6e, 0x65, 0x22, 0x51, 0x0a, 0x15, 0x42, 0x69, 0x6e, 0x64, 0x50, 0x75, 0x73, 0x68, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, + 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, 0x9d, 0x01, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x65, - 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x65, - 0x61, 0x73, 0x65, 0x49, 0x64, 0x22, 0x5c, 0x0a, 0x21, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, - 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, - 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x08, 0x69, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, - 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x74, 0x79, 0x2a, 0x73, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x1b, 0x0a, 0x17, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x16, - 0x0a, 0x12, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x41, 0x43, - 0x54, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x53, - 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x02, - 0x12, 0x16, 0x0a, 0x12, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, - 0x42, 0x41, 0x4e, 0x4e, 0x45, 0x44, 0x10, 0x03, 0x32, 0xbe, 0x05, 0x0a, 0x0b, 0x55, 0x73, 0x65, - 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x48, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, - 0x73, 0x65, 0x72, 0x12, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, - 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0d, 0x42, 0x61, 0x74, 0x63, 0x68, 0x47, 0x65, 0x74, 0x55, 0x73, - 0x65, 0x72, 0x73, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, - 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, - 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x47, 0x65, - 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, - 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x73, 0x12, 0x21, 0x2e, - 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x78, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, - 0x69, 0x63, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, - 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x69, 0x63, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, - 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x69, 0x63, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, - 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, - 0x69, 0x6c, 0x65, 0x12, 0x27, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, - 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, - 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x11, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x55, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x27, 0x2e, 0x68, 0x79, - 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x55, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, - 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x55, 0x73, 0x65, 0x72, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, - 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, - 0x64, 0x69, 0x6e, 0x67, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, - 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x6e, 0x62, - 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, - 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xd1, 0x01, 0x0a, 0x11, 0x55, 0x73, - 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, - 0x5a, 0x0a, 0x0d, 0x42, 0x69, 0x6e, 0x64, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x42, 0x69, 0x6e, 0x64, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x69, 0x6e, 0x64, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x0f, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x25, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x75, 0x73, 0x68, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x67, 0x0a, - 0x12, 0x41, 0x70, 0x70, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x0a, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x41, 0x70, - 0x70, 0x12, 0x20, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x41, 0x70, 0x70, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x41, 0x70, 0x70, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xc7, 0x01, 0x0a, 0x13, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x72, 0x79, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5a, - 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, - 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, - 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0d, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x23, 0x2e, 0x68, 0x79, - 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x73, 0x68, 0x5f, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x73, + 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x57, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22, + 0xbe, 0x03, 0x0a, 0x07, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, + 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, + 0x12, 0x30, 0x0a, 0x14, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x64, 0x69, 0x73, 0x70, + 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, 0x72, + 0x18, 0x06, 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, 0x07, 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, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x6f, + 0x5f, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x33, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, + 0x73, 0x6f, 0x41, 0x6c, 0x70, 0x68, 0x61, 0x33, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x6f, 0x5f, + 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, + 0x73, 0x6f, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x68, 0x6f, + 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x61, 0x67, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x66, 0x6c, 0x61, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, + 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, + 0x22, 0x92, 0x02, 0x0a, 0x06, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x72, + 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, + 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x67, 0x69, + 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, + 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 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, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, + 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, + 0x69, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, 0x65, + 0x72, 0x18, 0x06, 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, 0x07, 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, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, + 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, + 0x70, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x71, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, + 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1d, 0x0a, + 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, + 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, + 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x34, 0x0a, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x85, 0x03, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x32, 0x95, 0x01, 0x0a, 0x13, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x51, 0x75, 0x65, 0x72, - 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7e, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, - 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, - 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xf1, 0x02, 0x0a, 0x12, 0x52, 0x65, 0x67, - 0x69, 0x6f, 0x6e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, - 0x54, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x21, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, - 0x6f, 0x6e, 0x12, 0x1f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, - 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, - 0x6f, 0x6e, 0x12, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, - 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, - 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x65, 0x0a, 0x16, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, - 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, - 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, - 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, - 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xd3, 0x04, 0x0a, - 0x13, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x60, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, - 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, - 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x12, 0x2a, + 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, + 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x49, 0x64, 0x12, + 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x64, 0x69, + 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x12, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, + 0x65, 0x72, 0x18, 0x05, 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, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, + 0x0a, 0x69, 0x73, 0x6f, 0x5f, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x33, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x69, 0x73, 0x6f, 0x41, 0x6c, 0x70, 0x68, 0x61, 0x33, 0x12, 0x1f, 0x0a, 0x0b, + 0x69, 0x73, 0x6f, 0x5f, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x69, 0x73, 0x6f, 0x4e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x12, 0x2c, 0x0a, + 0x12, 0x70, 0x68, 0x6f, 0x6e, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, + 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x68, 0x6f, 0x6e, 0x65, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, + 0x6c, 0x61, 0x67, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x61, 0x67, 0x22, + 0x43, 0x0a, 0x0f, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x30, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x72, 0x79, 0x22, 0x52, 0x0a, 0x20, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, + 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x22, 0x59, 0x0a, 0x21, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, + 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, + 0x69, 0x65, 0x73, 0x22, 0x5c, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, + 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x22, 0x46, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x72, 0x65, 0x67, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, + 0x52, 0x07, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x5f, 0x0a, 0x10, 0x47, 0x65, 0x74, + 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, + 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1b, 0x0a, + 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xe0, 0x01, 0x0a, 0x13, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, + 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 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, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x64, + 0x65, 0x72, 0x18, 0x05, 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, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xb4, 0x01, + 0x0a, 0x1d, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, + 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x09, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, + 0x65, 0x72, 0x49, 0x64, 0x22, 0x3f, 0x0a, 0x0e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x72, + 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x22, 0xab, 0x02, 0x0a, 0x0c, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, + 0x26, 0x0a, 0x0f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, + 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 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, + 0x35, 0x0a, 0x17, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x14, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, + 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x3f, 0x0a, 0x1d, 0x64, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x73, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x18, + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x45, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, + 0x63, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, + 0x6f, 0x64, 0x65, 0x22, 0x61, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, + 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, + 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, + 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x52, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x37, 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x75, 0x0a, 0x1b, 0x52, 0x65, + 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, + 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, + 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x26, 0x0a, 0x0f, 0x64, 0x69, 0x73, + 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, + 0x64, 0x22, 0x57, 0x0a, 0x1c, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, + 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x37, 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xd6, 0x01, 0x0a, 0x1a, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, + 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, + 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, + 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x13, 0x6e, 0x65, 0x77, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, + 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x10, 0x6e, 0x65, 0x77, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, + 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 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, 0x05, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, + 0x72, 0x49, 0x64, 0x22, 0x56, 0x0a, 0x1b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x44, 0x69, 0x73, + 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x37, 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xf9, 0x01, 0x0a, 0x1f, + 0x41, 0x70, 0x70, 0x6c, 0x79, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, + 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x33, 0x0a, 0x16, 0x70, 0x72, 0x65, 0x74, + 0x74, 0x79, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, + 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2a, 0x0a, + 0x11, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x63, 0x65, 0x69, 0x70, 0x74, 0x49, 0x64, 0x22, 0x76, 0x0a, 0x20, 0x41, 0x70, 0x70, 0x6c, 0x79, + 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, + 0x72, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x08, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, + 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x64, 0x22, + 0x86, 0x01, 0x0a, 0x20, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, + 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, + 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x19, 0x0a, + 0x08, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x64, 0x22, 0x5c, 0x0a, 0x21, 0x45, 0x78, 0x70, 0x69, + 0x72, 0x65, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, + 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, + 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2a, 0x73, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x17, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x16, 0x0a, 0x12, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x55, 0x53, 0x45, + 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, + 0x44, 0x10, 0x02, 0x12, 0x16, 0x0a, 0x12, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x42, 0x41, 0x4e, 0x4e, 0x45, 0x44, 0x10, 0x03, 0x32, 0xbe, 0x05, 0x0a, 0x0b, + 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x48, 0x0a, 0x07, 0x47, + 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0d, 0x42, 0x61, 0x74, 0x63, 0x68, 0x47, 0x65, + 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x47, 0x65, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x54, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x73, + 0x12, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x78, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x55, 0x73, + 0x65, 0x72, 0x4d, 0x69, 0x63, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, + 0x74, 0x73, 0x12, 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x69, 0x63, 0x4c, 0x69, 0x66, + 0x65, 0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x69, 0x63, 0x4c, 0x69, 0x66, 0x65, + 0x74, 0x69, 0x6d, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x66, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, + 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x27, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, + 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x11, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x55, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x27, + 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x55, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x55, 0x73, + 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x69, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x6e, 0x62, + 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, + 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, + 0x64, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xaf, 0x02, 0x0a, + 0x0f, 0x55, 0x73, 0x65, 0x72, 0x43, 0x72, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x5c, 0x0a, 0x17, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x69, 0x6e, + 0x49, 0x50, 0x52, 0x69, 0x73, 0x6b, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x6f, 0x6e, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, + 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x6f, + 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, + 0x0a, 0x19, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x6f, 0x6e, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, + 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x6f, + 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, + 0x0a, 0x19, 0x43, 0x6f, 0x6d, 0x70, 0x65, 0x6e, 0x73, 0x61, 0x74, 0x65, 0x4d, 0x69, 0x63, 0x4f, + 0x70, 0x65, 0x6e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1f, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x6f, 0x6e, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x68, + 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x6f, + 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xd1, + 0x01, 0x0a, 0x11, 0x55, 0x73, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x5a, 0x0a, 0x0d, 0x42, 0x69, 0x6e, 0x64, 0x50, 0x75, 0x73, 0x68, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x69, 0x6e, 0x64, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x79, 0x61, + 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x69, 0x6e, 0x64, 0x50, + 0x75, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x60, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x79, 0x61, + 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x50, 0x75, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x32, 0x67, 0x0a, 0x12, 0x41, 0x70, 0x70, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x0a, 0x52, 0x65, 0x73, 0x6f, + 0x6c, 0x76, 0x65, 0x41, 0x70, 0x70, 0x12, 0x20, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x41, 0x70, + 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, + 0x41, 0x70, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xc7, 0x01, 0x0a, 0x13, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x5a, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x72, 0x69, 0x65, 0x73, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x54, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x95, 0x01, 0x0a, 0x13, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, + 0x79, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7e, 0x0a, + 0x19, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x12, 0x2f, 0x2e, 0x68, 0x79, 0x61, + 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xf1, 0x02, + 0x0a, 0x12, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x54, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x09, 0x47, 0x65, + 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x67, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x65, 0x0a, 0x16, 0x52, 0x65, + 0x70, 0x6c, 0x61, 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x72, 0x69, 0x65, 0x73, 0x12, 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x52, 0x65, 0x67, 0x69, + 0x6f, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x32, 0xd3, 0x04, 0x0a, 0x13, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x60, 0x0a, 0x0f, 0x47, 0x65, 0x74, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x25, 0x2e, 0x68, + 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, - 0x72, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, - 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x12, 0x29, + 0x72, 0x49, 0x44, 0x12, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, + 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, + 0x72, 0x49, 0x44, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, + 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, - 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7b, 0x0a, 0x18, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x50, 0x72, - 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, - 0x44, 0x12, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x19, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x74, - 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x12, - 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, - 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x30, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, - 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, - 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, - 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x75, 0x73, 0x65, 0x72, - 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x49, 0x44, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7b, 0x0a, 0x18, 0x41, 0x70, + 0x70, 0x6c, 0x79, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x12, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x50, 0x72, 0x65, 0x74, + 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, + 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x50, 0x72, 0x65, 0x74, + 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x19, 0x45, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, + 0x65, 0x72, 0x49, 0x44, 0x12, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x74, 0x74, + 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x50, 0x72, 0x65, 0x74, + 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -4614,7 +4866,7 @@ func file_proto_user_v1_user_proto_rawDescGZIP() []byte { } var file_proto_user_v1_user_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_proto_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 53) +var file_proto_user_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 55) var file_proto_user_v1_user_proto_goTypes = []any{ (UserStatus)(0), // 0: hyapp.user.v1.UserStatus (*RequestMeta)(nil), // 1: hyapp.user.v1.RequestMeta @@ -4627,49 +4879,51 @@ var file_proto_user_v1_user_proto_goTypes = []any{ (*UserMicLifetimeStats)(nil), // 8: hyapp.user.v1.UserMicLifetimeStats (*GetUserMicLifetimeStatsRequest)(nil), // 9: hyapp.user.v1.GetUserMicLifetimeStatsRequest (*GetUserMicLifetimeStatsResponse)(nil), // 10: hyapp.user.v1.GetUserMicLifetimeStatsResponse - (*AuthToken)(nil), // 11: hyapp.user.v1.AuthToken - (*GetUserRequest)(nil), // 12: hyapp.user.v1.GetUserRequest - (*GetUserResponse)(nil), // 13: hyapp.user.v1.GetUserResponse - (*BatchGetUsersRequest)(nil), // 14: hyapp.user.v1.BatchGetUsersRequest - (*BatchGetUsersResponse)(nil), // 15: hyapp.user.v1.BatchGetUsersResponse - (*ListUserIDsRequest)(nil), // 16: hyapp.user.v1.ListUserIDsRequest - (*ListUserIDsResponse)(nil), // 17: hyapp.user.v1.ListUserIDsResponse - (*UpdateUserProfileRequest)(nil), // 18: hyapp.user.v1.UpdateUserProfileRequest - (*UpdateUserProfileResponse)(nil), // 19: hyapp.user.v1.UpdateUserProfileResponse - (*ChangeUserCountryRequest)(nil), // 20: hyapp.user.v1.ChangeUserCountryRequest - (*ChangeUserCountryResponse)(nil), // 21: hyapp.user.v1.ChangeUserCountryResponse - (*CompleteOnboardingRequest)(nil), // 22: hyapp.user.v1.CompleteOnboardingRequest - (*CompleteOnboardingResponse)(nil), // 23: hyapp.user.v1.CompleteOnboardingResponse - (*BindPushTokenRequest)(nil), // 24: hyapp.user.v1.BindPushTokenRequest - (*BindPushTokenResponse)(nil), // 25: hyapp.user.v1.BindPushTokenResponse - (*DeletePushTokenRequest)(nil), // 26: hyapp.user.v1.DeletePushTokenRequest - (*DeletePushTokenResponse)(nil), // 27: hyapp.user.v1.DeletePushTokenResponse - (*Country)(nil), // 28: hyapp.user.v1.Country - (*Region)(nil), // 29: hyapp.user.v1.Region - (*ListCountriesRequest)(nil), // 30: hyapp.user.v1.ListCountriesRequest - (*ListCountriesResponse)(nil), // 31: hyapp.user.v1.ListCountriesResponse - (*UpdateCountryRequest)(nil), // 32: hyapp.user.v1.UpdateCountryRequest - (*CountryResponse)(nil), // 33: hyapp.user.v1.CountryResponse - (*ListRegistrationCountriesRequest)(nil), // 34: hyapp.user.v1.ListRegistrationCountriesRequest - (*ListRegistrationCountriesResponse)(nil), // 35: hyapp.user.v1.ListRegistrationCountriesResponse - (*ListRegionsRequest)(nil), // 36: hyapp.user.v1.ListRegionsRequest - (*ListRegionsResponse)(nil), // 37: hyapp.user.v1.ListRegionsResponse - (*GetRegionRequest)(nil), // 38: hyapp.user.v1.GetRegionRequest - (*UpdateRegionRequest)(nil), // 39: hyapp.user.v1.UpdateRegionRequest - (*ReplaceRegionCountriesRequest)(nil), // 40: hyapp.user.v1.ReplaceRegionCountriesRequest - (*RegionResponse)(nil), // 41: hyapp.user.v1.RegionResponse - (*UserIdentity)(nil), // 42: hyapp.user.v1.UserIdentity - (*GetUserIdentityRequest)(nil), // 43: hyapp.user.v1.GetUserIdentityRequest - (*GetUserIdentityResponse)(nil), // 44: hyapp.user.v1.GetUserIdentityResponse - (*ResolveDisplayUserIDRequest)(nil), // 45: hyapp.user.v1.ResolveDisplayUserIDRequest - (*ResolveDisplayUserIDResponse)(nil), // 46: hyapp.user.v1.ResolveDisplayUserIDResponse - (*ChangeDisplayUserIDRequest)(nil), // 47: hyapp.user.v1.ChangeDisplayUserIDRequest - (*ChangeDisplayUserIDResponse)(nil), // 48: hyapp.user.v1.ChangeDisplayUserIDResponse - (*ApplyPrettyDisplayUserIDRequest)(nil), // 49: hyapp.user.v1.ApplyPrettyDisplayUserIDRequest - (*ApplyPrettyDisplayUserIDResponse)(nil), // 50: hyapp.user.v1.ApplyPrettyDisplayUserIDResponse - (*ExpirePrettyDisplayUserIDRequest)(nil), // 51: hyapp.user.v1.ExpirePrettyDisplayUserIDRequest - (*ExpirePrettyDisplayUserIDResponse)(nil), // 52: hyapp.user.v1.ExpirePrettyDisplayUserIDResponse - nil, // 53: hyapp.user.v1.BatchGetUsersResponse.UsersEntry + (*CronBatchRequest)(nil), // 11: hyapp.user.v1.CronBatchRequest + (*CronBatchResponse)(nil), // 12: hyapp.user.v1.CronBatchResponse + (*AuthToken)(nil), // 13: hyapp.user.v1.AuthToken + (*GetUserRequest)(nil), // 14: hyapp.user.v1.GetUserRequest + (*GetUserResponse)(nil), // 15: hyapp.user.v1.GetUserResponse + (*BatchGetUsersRequest)(nil), // 16: hyapp.user.v1.BatchGetUsersRequest + (*BatchGetUsersResponse)(nil), // 17: hyapp.user.v1.BatchGetUsersResponse + (*ListUserIDsRequest)(nil), // 18: hyapp.user.v1.ListUserIDsRequest + (*ListUserIDsResponse)(nil), // 19: hyapp.user.v1.ListUserIDsResponse + (*UpdateUserProfileRequest)(nil), // 20: hyapp.user.v1.UpdateUserProfileRequest + (*UpdateUserProfileResponse)(nil), // 21: hyapp.user.v1.UpdateUserProfileResponse + (*ChangeUserCountryRequest)(nil), // 22: hyapp.user.v1.ChangeUserCountryRequest + (*ChangeUserCountryResponse)(nil), // 23: hyapp.user.v1.ChangeUserCountryResponse + (*CompleteOnboardingRequest)(nil), // 24: hyapp.user.v1.CompleteOnboardingRequest + (*CompleteOnboardingResponse)(nil), // 25: hyapp.user.v1.CompleteOnboardingResponse + (*BindPushTokenRequest)(nil), // 26: hyapp.user.v1.BindPushTokenRequest + (*BindPushTokenResponse)(nil), // 27: hyapp.user.v1.BindPushTokenResponse + (*DeletePushTokenRequest)(nil), // 28: hyapp.user.v1.DeletePushTokenRequest + (*DeletePushTokenResponse)(nil), // 29: hyapp.user.v1.DeletePushTokenResponse + (*Country)(nil), // 30: hyapp.user.v1.Country + (*Region)(nil), // 31: hyapp.user.v1.Region + (*ListCountriesRequest)(nil), // 32: hyapp.user.v1.ListCountriesRequest + (*ListCountriesResponse)(nil), // 33: hyapp.user.v1.ListCountriesResponse + (*UpdateCountryRequest)(nil), // 34: hyapp.user.v1.UpdateCountryRequest + (*CountryResponse)(nil), // 35: hyapp.user.v1.CountryResponse + (*ListRegistrationCountriesRequest)(nil), // 36: hyapp.user.v1.ListRegistrationCountriesRequest + (*ListRegistrationCountriesResponse)(nil), // 37: hyapp.user.v1.ListRegistrationCountriesResponse + (*ListRegionsRequest)(nil), // 38: hyapp.user.v1.ListRegionsRequest + (*ListRegionsResponse)(nil), // 39: hyapp.user.v1.ListRegionsResponse + (*GetRegionRequest)(nil), // 40: hyapp.user.v1.GetRegionRequest + (*UpdateRegionRequest)(nil), // 41: hyapp.user.v1.UpdateRegionRequest + (*ReplaceRegionCountriesRequest)(nil), // 42: hyapp.user.v1.ReplaceRegionCountriesRequest + (*RegionResponse)(nil), // 43: hyapp.user.v1.RegionResponse + (*UserIdentity)(nil), // 44: hyapp.user.v1.UserIdentity + (*GetUserIdentityRequest)(nil), // 45: hyapp.user.v1.GetUserIdentityRequest + (*GetUserIdentityResponse)(nil), // 46: hyapp.user.v1.GetUserIdentityResponse + (*ResolveDisplayUserIDRequest)(nil), // 47: hyapp.user.v1.ResolveDisplayUserIDRequest + (*ResolveDisplayUserIDResponse)(nil), // 48: hyapp.user.v1.ResolveDisplayUserIDResponse + (*ChangeDisplayUserIDRequest)(nil), // 49: hyapp.user.v1.ChangeDisplayUserIDRequest + (*ChangeDisplayUserIDResponse)(nil), // 50: hyapp.user.v1.ChangeDisplayUserIDResponse + (*ApplyPrettyDisplayUserIDRequest)(nil), // 51: hyapp.user.v1.ApplyPrettyDisplayUserIDRequest + (*ApplyPrettyDisplayUserIDResponse)(nil), // 52: hyapp.user.v1.ApplyPrettyDisplayUserIDResponse + (*ExpirePrettyDisplayUserIDRequest)(nil), // 53: hyapp.user.v1.ExpirePrettyDisplayUserIDRequest + (*ExpirePrettyDisplayUserIDResponse)(nil), // 54: hyapp.user.v1.ExpirePrettyDisplayUserIDResponse + nil, // 55: hyapp.user.v1.BatchGetUsersResponse.UsersEntry } var file_proto_user_v1_user_proto_depIdxs = []int32{ 1, // 0: hyapp.user.v1.ResolveAppRequest.meta:type_name -> hyapp.user.v1.RequestMeta @@ -4678,93 +4932,100 @@ var file_proto_user_v1_user_proto_depIdxs = []int32{ 6, // 3: hyapp.user.v1.User.invite:type_name -> hyapp.user.v1.InviteOverview 1, // 4: hyapp.user.v1.GetUserMicLifetimeStatsRequest.meta:type_name -> hyapp.user.v1.RequestMeta 8, // 5: hyapp.user.v1.GetUserMicLifetimeStatsResponse.stats:type_name -> hyapp.user.v1.UserMicLifetimeStats - 1, // 6: hyapp.user.v1.GetUserRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 5, // 7: hyapp.user.v1.GetUserResponse.user:type_name -> hyapp.user.v1.User - 1, // 8: hyapp.user.v1.BatchGetUsersRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 53, // 9: hyapp.user.v1.BatchGetUsersResponse.users:type_name -> hyapp.user.v1.BatchGetUsersResponse.UsersEntry - 1, // 10: hyapp.user.v1.ListUserIDsRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 1, // 11: hyapp.user.v1.UpdateUserProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 5, // 12: hyapp.user.v1.UpdateUserProfileResponse.user:type_name -> hyapp.user.v1.User - 1, // 13: hyapp.user.v1.ChangeUserCountryRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 5, // 14: hyapp.user.v1.ChangeUserCountryResponse.user:type_name -> hyapp.user.v1.User - 1, // 15: hyapp.user.v1.CompleteOnboardingRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 5, // 16: hyapp.user.v1.CompleteOnboardingResponse.user:type_name -> hyapp.user.v1.User - 11, // 17: hyapp.user.v1.CompleteOnboardingResponse.token:type_name -> hyapp.user.v1.AuthToken - 7, // 18: hyapp.user.v1.CompleteOnboardingResponse.invite:type_name -> hyapp.user.v1.InviteBinding - 1, // 19: hyapp.user.v1.BindPushTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 1, // 20: hyapp.user.v1.DeletePushTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 1, // 21: hyapp.user.v1.ListCountriesRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 28, // 22: hyapp.user.v1.ListCountriesResponse.countries:type_name -> hyapp.user.v1.Country - 1, // 23: hyapp.user.v1.UpdateCountryRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 28, // 24: hyapp.user.v1.CountryResponse.country:type_name -> hyapp.user.v1.Country - 1, // 25: hyapp.user.v1.ListRegistrationCountriesRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 28, // 26: hyapp.user.v1.ListRegistrationCountriesResponse.countries:type_name -> hyapp.user.v1.Country - 1, // 27: hyapp.user.v1.ListRegionsRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 29, // 28: hyapp.user.v1.ListRegionsResponse.regions:type_name -> hyapp.user.v1.Region - 1, // 29: hyapp.user.v1.GetRegionRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 1, // 30: hyapp.user.v1.UpdateRegionRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 1, // 31: hyapp.user.v1.ReplaceRegionCountriesRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 29, // 32: hyapp.user.v1.RegionResponse.region:type_name -> hyapp.user.v1.Region - 1, // 33: hyapp.user.v1.GetUserIdentityRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 42, // 34: hyapp.user.v1.GetUserIdentityResponse.identity:type_name -> hyapp.user.v1.UserIdentity - 1, // 35: hyapp.user.v1.ResolveDisplayUserIDRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 42, // 36: hyapp.user.v1.ResolveDisplayUserIDResponse.identity:type_name -> hyapp.user.v1.UserIdentity - 1, // 37: hyapp.user.v1.ChangeDisplayUserIDRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 42, // 38: hyapp.user.v1.ChangeDisplayUserIDResponse.identity:type_name -> hyapp.user.v1.UserIdentity - 1, // 39: hyapp.user.v1.ApplyPrettyDisplayUserIDRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 42, // 40: hyapp.user.v1.ApplyPrettyDisplayUserIDResponse.identity:type_name -> hyapp.user.v1.UserIdentity - 1, // 41: hyapp.user.v1.ExpirePrettyDisplayUserIDRequest.meta:type_name -> hyapp.user.v1.RequestMeta - 42, // 42: hyapp.user.v1.ExpirePrettyDisplayUserIDResponse.identity:type_name -> hyapp.user.v1.UserIdentity - 5, // 43: hyapp.user.v1.BatchGetUsersResponse.UsersEntry.value:type_name -> hyapp.user.v1.User - 12, // 44: hyapp.user.v1.UserService.GetUser:input_type -> hyapp.user.v1.GetUserRequest - 14, // 45: hyapp.user.v1.UserService.BatchGetUsers:input_type -> hyapp.user.v1.BatchGetUsersRequest - 16, // 46: hyapp.user.v1.UserService.ListUserIDs:input_type -> hyapp.user.v1.ListUserIDsRequest - 9, // 47: hyapp.user.v1.UserService.GetUserMicLifetimeStats:input_type -> hyapp.user.v1.GetUserMicLifetimeStatsRequest - 18, // 48: hyapp.user.v1.UserService.UpdateUserProfile:input_type -> hyapp.user.v1.UpdateUserProfileRequest - 20, // 49: hyapp.user.v1.UserService.ChangeUserCountry:input_type -> hyapp.user.v1.ChangeUserCountryRequest - 22, // 50: hyapp.user.v1.UserService.CompleteOnboarding:input_type -> hyapp.user.v1.CompleteOnboardingRequest - 24, // 51: hyapp.user.v1.UserDeviceService.BindPushToken:input_type -> hyapp.user.v1.BindPushTokenRequest - 26, // 52: hyapp.user.v1.UserDeviceService.DeletePushToken:input_type -> hyapp.user.v1.DeletePushTokenRequest - 3, // 53: hyapp.user.v1.AppRegistryService.ResolveApp:input_type -> hyapp.user.v1.ResolveAppRequest - 30, // 54: hyapp.user.v1.CountryAdminService.ListCountries:input_type -> hyapp.user.v1.ListCountriesRequest - 32, // 55: hyapp.user.v1.CountryAdminService.UpdateCountry:input_type -> hyapp.user.v1.UpdateCountryRequest - 34, // 56: hyapp.user.v1.CountryQueryService.ListRegistrationCountries:input_type -> hyapp.user.v1.ListRegistrationCountriesRequest - 36, // 57: hyapp.user.v1.RegionAdminService.ListRegions:input_type -> hyapp.user.v1.ListRegionsRequest - 38, // 58: hyapp.user.v1.RegionAdminService.GetRegion:input_type -> hyapp.user.v1.GetRegionRequest - 39, // 59: hyapp.user.v1.RegionAdminService.UpdateRegion:input_type -> hyapp.user.v1.UpdateRegionRequest - 40, // 60: hyapp.user.v1.RegionAdminService.ReplaceRegionCountries:input_type -> hyapp.user.v1.ReplaceRegionCountriesRequest - 43, // 61: hyapp.user.v1.UserIdentityService.GetUserIdentity:input_type -> hyapp.user.v1.GetUserIdentityRequest - 45, // 62: hyapp.user.v1.UserIdentityService.ResolveDisplayUserID:input_type -> hyapp.user.v1.ResolveDisplayUserIDRequest - 47, // 63: hyapp.user.v1.UserIdentityService.ChangeDisplayUserID:input_type -> hyapp.user.v1.ChangeDisplayUserIDRequest - 49, // 64: hyapp.user.v1.UserIdentityService.ApplyPrettyDisplayUserID:input_type -> hyapp.user.v1.ApplyPrettyDisplayUserIDRequest - 51, // 65: hyapp.user.v1.UserIdentityService.ExpirePrettyDisplayUserID:input_type -> hyapp.user.v1.ExpirePrettyDisplayUserIDRequest - 13, // 66: hyapp.user.v1.UserService.GetUser:output_type -> hyapp.user.v1.GetUserResponse - 15, // 67: hyapp.user.v1.UserService.BatchGetUsers:output_type -> hyapp.user.v1.BatchGetUsersResponse - 17, // 68: hyapp.user.v1.UserService.ListUserIDs:output_type -> hyapp.user.v1.ListUserIDsResponse - 10, // 69: hyapp.user.v1.UserService.GetUserMicLifetimeStats:output_type -> hyapp.user.v1.GetUserMicLifetimeStatsResponse - 19, // 70: hyapp.user.v1.UserService.UpdateUserProfile:output_type -> hyapp.user.v1.UpdateUserProfileResponse - 21, // 71: hyapp.user.v1.UserService.ChangeUserCountry:output_type -> hyapp.user.v1.ChangeUserCountryResponse - 23, // 72: hyapp.user.v1.UserService.CompleteOnboarding:output_type -> hyapp.user.v1.CompleteOnboardingResponse - 25, // 73: hyapp.user.v1.UserDeviceService.BindPushToken:output_type -> hyapp.user.v1.BindPushTokenResponse - 27, // 74: hyapp.user.v1.UserDeviceService.DeletePushToken:output_type -> hyapp.user.v1.DeletePushTokenResponse - 4, // 75: hyapp.user.v1.AppRegistryService.ResolveApp:output_type -> hyapp.user.v1.ResolveAppResponse - 31, // 76: hyapp.user.v1.CountryAdminService.ListCountries:output_type -> hyapp.user.v1.ListCountriesResponse - 33, // 77: hyapp.user.v1.CountryAdminService.UpdateCountry:output_type -> hyapp.user.v1.CountryResponse - 35, // 78: hyapp.user.v1.CountryQueryService.ListRegistrationCountries:output_type -> hyapp.user.v1.ListRegistrationCountriesResponse - 37, // 79: hyapp.user.v1.RegionAdminService.ListRegions:output_type -> hyapp.user.v1.ListRegionsResponse - 41, // 80: hyapp.user.v1.RegionAdminService.GetRegion:output_type -> hyapp.user.v1.RegionResponse - 41, // 81: hyapp.user.v1.RegionAdminService.UpdateRegion:output_type -> hyapp.user.v1.RegionResponse - 41, // 82: hyapp.user.v1.RegionAdminService.ReplaceRegionCountries:output_type -> hyapp.user.v1.RegionResponse - 44, // 83: hyapp.user.v1.UserIdentityService.GetUserIdentity:output_type -> hyapp.user.v1.GetUserIdentityResponse - 46, // 84: hyapp.user.v1.UserIdentityService.ResolveDisplayUserID:output_type -> hyapp.user.v1.ResolveDisplayUserIDResponse - 48, // 85: hyapp.user.v1.UserIdentityService.ChangeDisplayUserID:output_type -> hyapp.user.v1.ChangeDisplayUserIDResponse - 50, // 86: hyapp.user.v1.UserIdentityService.ApplyPrettyDisplayUserID:output_type -> hyapp.user.v1.ApplyPrettyDisplayUserIDResponse - 52, // 87: hyapp.user.v1.UserIdentityService.ExpirePrettyDisplayUserID:output_type -> hyapp.user.v1.ExpirePrettyDisplayUserIDResponse - 66, // [66:88] is the sub-list for method output_type - 44, // [44:66] is the sub-list for method input_type - 44, // [44:44] is the sub-list for extension type_name - 44, // [44:44] is the sub-list for extension extendee - 0, // [0:44] is the sub-list for field type_name + 1, // 6: hyapp.user.v1.CronBatchRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 1, // 7: hyapp.user.v1.GetUserRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 5, // 8: hyapp.user.v1.GetUserResponse.user:type_name -> hyapp.user.v1.User + 1, // 9: hyapp.user.v1.BatchGetUsersRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 55, // 10: hyapp.user.v1.BatchGetUsersResponse.users:type_name -> hyapp.user.v1.BatchGetUsersResponse.UsersEntry + 1, // 11: hyapp.user.v1.ListUserIDsRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 1, // 12: hyapp.user.v1.UpdateUserProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 5, // 13: hyapp.user.v1.UpdateUserProfileResponse.user:type_name -> hyapp.user.v1.User + 1, // 14: hyapp.user.v1.ChangeUserCountryRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 5, // 15: hyapp.user.v1.ChangeUserCountryResponse.user:type_name -> hyapp.user.v1.User + 1, // 16: hyapp.user.v1.CompleteOnboardingRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 5, // 17: hyapp.user.v1.CompleteOnboardingResponse.user:type_name -> hyapp.user.v1.User + 13, // 18: hyapp.user.v1.CompleteOnboardingResponse.token:type_name -> hyapp.user.v1.AuthToken + 7, // 19: hyapp.user.v1.CompleteOnboardingResponse.invite:type_name -> hyapp.user.v1.InviteBinding + 1, // 20: hyapp.user.v1.BindPushTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 1, // 21: hyapp.user.v1.DeletePushTokenRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 1, // 22: hyapp.user.v1.ListCountriesRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 30, // 23: hyapp.user.v1.ListCountriesResponse.countries:type_name -> hyapp.user.v1.Country + 1, // 24: hyapp.user.v1.UpdateCountryRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 30, // 25: hyapp.user.v1.CountryResponse.country:type_name -> hyapp.user.v1.Country + 1, // 26: hyapp.user.v1.ListRegistrationCountriesRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 30, // 27: hyapp.user.v1.ListRegistrationCountriesResponse.countries:type_name -> hyapp.user.v1.Country + 1, // 28: hyapp.user.v1.ListRegionsRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 31, // 29: hyapp.user.v1.ListRegionsResponse.regions:type_name -> hyapp.user.v1.Region + 1, // 30: hyapp.user.v1.GetRegionRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 1, // 31: hyapp.user.v1.UpdateRegionRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 1, // 32: hyapp.user.v1.ReplaceRegionCountriesRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 31, // 33: hyapp.user.v1.RegionResponse.region:type_name -> hyapp.user.v1.Region + 1, // 34: hyapp.user.v1.GetUserIdentityRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 44, // 35: hyapp.user.v1.GetUserIdentityResponse.identity:type_name -> hyapp.user.v1.UserIdentity + 1, // 36: hyapp.user.v1.ResolveDisplayUserIDRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 44, // 37: hyapp.user.v1.ResolveDisplayUserIDResponse.identity:type_name -> hyapp.user.v1.UserIdentity + 1, // 38: hyapp.user.v1.ChangeDisplayUserIDRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 44, // 39: hyapp.user.v1.ChangeDisplayUserIDResponse.identity:type_name -> hyapp.user.v1.UserIdentity + 1, // 40: hyapp.user.v1.ApplyPrettyDisplayUserIDRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 44, // 41: hyapp.user.v1.ApplyPrettyDisplayUserIDResponse.identity:type_name -> hyapp.user.v1.UserIdentity + 1, // 42: hyapp.user.v1.ExpirePrettyDisplayUserIDRequest.meta:type_name -> hyapp.user.v1.RequestMeta + 44, // 43: hyapp.user.v1.ExpirePrettyDisplayUserIDResponse.identity:type_name -> hyapp.user.v1.UserIdentity + 5, // 44: hyapp.user.v1.BatchGetUsersResponse.UsersEntry.value:type_name -> hyapp.user.v1.User + 14, // 45: hyapp.user.v1.UserService.GetUser:input_type -> hyapp.user.v1.GetUserRequest + 16, // 46: hyapp.user.v1.UserService.BatchGetUsers:input_type -> hyapp.user.v1.BatchGetUsersRequest + 18, // 47: hyapp.user.v1.UserService.ListUserIDs:input_type -> hyapp.user.v1.ListUserIDsRequest + 9, // 48: hyapp.user.v1.UserService.GetUserMicLifetimeStats:input_type -> hyapp.user.v1.GetUserMicLifetimeStatsRequest + 20, // 49: hyapp.user.v1.UserService.UpdateUserProfile:input_type -> hyapp.user.v1.UpdateUserProfileRequest + 22, // 50: hyapp.user.v1.UserService.ChangeUserCountry:input_type -> hyapp.user.v1.ChangeUserCountryRequest + 24, // 51: hyapp.user.v1.UserService.CompleteOnboarding:input_type -> hyapp.user.v1.CompleteOnboardingRequest + 11, // 52: hyapp.user.v1.UserCronService.ProcessLoginIPRiskBatch:input_type -> hyapp.user.v1.CronBatchRequest + 11, // 53: hyapp.user.v1.UserCronService.ProcessRegionRebuildBatch:input_type -> hyapp.user.v1.CronBatchRequest + 11, // 54: hyapp.user.v1.UserCronService.CompensateMicOpenSessions:input_type -> hyapp.user.v1.CronBatchRequest + 26, // 55: hyapp.user.v1.UserDeviceService.BindPushToken:input_type -> hyapp.user.v1.BindPushTokenRequest + 28, // 56: hyapp.user.v1.UserDeviceService.DeletePushToken:input_type -> hyapp.user.v1.DeletePushTokenRequest + 3, // 57: hyapp.user.v1.AppRegistryService.ResolveApp:input_type -> hyapp.user.v1.ResolveAppRequest + 32, // 58: hyapp.user.v1.CountryAdminService.ListCountries:input_type -> hyapp.user.v1.ListCountriesRequest + 34, // 59: hyapp.user.v1.CountryAdminService.UpdateCountry:input_type -> hyapp.user.v1.UpdateCountryRequest + 36, // 60: hyapp.user.v1.CountryQueryService.ListRegistrationCountries:input_type -> hyapp.user.v1.ListRegistrationCountriesRequest + 38, // 61: hyapp.user.v1.RegionAdminService.ListRegions:input_type -> hyapp.user.v1.ListRegionsRequest + 40, // 62: hyapp.user.v1.RegionAdminService.GetRegion:input_type -> hyapp.user.v1.GetRegionRequest + 41, // 63: hyapp.user.v1.RegionAdminService.UpdateRegion:input_type -> hyapp.user.v1.UpdateRegionRequest + 42, // 64: hyapp.user.v1.RegionAdminService.ReplaceRegionCountries:input_type -> hyapp.user.v1.ReplaceRegionCountriesRequest + 45, // 65: hyapp.user.v1.UserIdentityService.GetUserIdentity:input_type -> hyapp.user.v1.GetUserIdentityRequest + 47, // 66: hyapp.user.v1.UserIdentityService.ResolveDisplayUserID:input_type -> hyapp.user.v1.ResolveDisplayUserIDRequest + 49, // 67: hyapp.user.v1.UserIdentityService.ChangeDisplayUserID:input_type -> hyapp.user.v1.ChangeDisplayUserIDRequest + 51, // 68: hyapp.user.v1.UserIdentityService.ApplyPrettyDisplayUserID:input_type -> hyapp.user.v1.ApplyPrettyDisplayUserIDRequest + 53, // 69: hyapp.user.v1.UserIdentityService.ExpirePrettyDisplayUserID:input_type -> hyapp.user.v1.ExpirePrettyDisplayUserIDRequest + 15, // 70: hyapp.user.v1.UserService.GetUser:output_type -> hyapp.user.v1.GetUserResponse + 17, // 71: hyapp.user.v1.UserService.BatchGetUsers:output_type -> hyapp.user.v1.BatchGetUsersResponse + 19, // 72: hyapp.user.v1.UserService.ListUserIDs:output_type -> hyapp.user.v1.ListUserIDsResponse + 10, // 73: hyapp.user.v1.UserService.GetUserMicLifetimeStats:output_type -> hyapp.user.v1.GetUserMicLifetimeStatsResponse + 21, // 74: hyapp.user.v1.UserService.UpdateUserProfile:output_type -> hyapp.user.v1.UpdateUserProfileResponse + 23, // 75: hyapp.user.v1.UserService.ChangeUserCountry:output_type -> hyapp.user.v1.ChangeUserCountryResponse + 25, // 76: hyapp.user.v1.UserService.CompleteOnboarding:output_type -> hyapp.user.v1.CompleteOnboardingResponse + 12, // 77: hyapp.user.v1.UserCronService.ProcessLoginIPRiskBatch:output_type -> hyapp.user.v1.CronBatchResponse + 12, // 78: hyapp.user.v1.UserCronService.ProcessRegionRebuildBatch:output_type -> hyapp.user.v1.CronBatchResponse + 12, // 79: hyapp.user.v1.UserCronService.CompensateMicOpenSessions:output_type -> hyapp.user.v1.CronBatchResponse + 27, // 80: hyapp.user.v1.UserDeviceService.BindPushToken:output_type -> hyapp.user.v1.BindPushTokenResponse + 29, // 81: hyapp.user.v1.UserDeviceService.DeletePushToken:output_type -> hyapp.user.v1.DeletePushTokenResponse + 4, // 82: hyapp.user.v1.AppRegistryService.ResolveApp:output_type -> hyapp.user.v1.ResolveAppResponse + 33, // 83: hyapp.user.v1.CountryAdminService.ListCountries:output_type -> hyapp.user.v1.ListCountriesResponse + 35, // 84: hyapp.user.v1.CountryAdminService.UpdateCountry:output_type -> hyapp.user.v1.CountryResponse + 37, // 85: hyapp.user.v1.CountryQueryService.ListRegistrationCountries:output_type -> hyapp.user.v1.ListRegistrationCountriesResponse + 39, // 86: hyapp.user.v1.RegionAdminService.ListRegions:output_type -> hyapp.user.v1.ListRegionsResponse + 43, // 87: hyapp.user.v1.RegionAdminService.GetRegion:output_type -> hyapp.user.v1.RegionResponse + 43, // 88: hyapp.user.v1.RegionAdminService.UpdateRegion:output_type -> hyapp.user.v1.RegionResponse + 43, // 89: hyapp.user.v1.RegionAdminService.ReplaceRegionCountries:output_type -> hyapp.user.v1.RegionResponse + 46, // 90: hyapp.user.v1.UserIdentityService.GetUserIdentity:output_type -> hyapp.user.v1.GetUserIdentityResponse + 48, // 91: hyapp.user.v1.UserIdentityService.ResolveDisplayUserID:output_type -> hyapp.user.v1.ResolveDisplayUserIDResponse + 50, // 92: hyapp.user.v1.UserIdentityService.ChangeDisplayUserID:output_type -> hyapp.user.v1.ChangeDisplayUserIDResponse + 52, // 93: hyapp.user.v1.UserIdentityService.ApplyPrettyDisplayUserID:output_type -> hyapp.user.v1.ApplyPrettyDisplayUserIDResponse + 54, // 94: hyapp.user.v1.UserIdentityService.ExpirePrettyDisplayUserID:output_type -> hyapp.user.v1.ExpirePrettyDisplayUserIDResponse + 70, // [70:95] is the sub-list for method output_type + 45, // [45:70] is the sub-list for method input_type + 45, // [45:45] is the sub-list for extension type_name + 45, // [45:45] is the sub-list for extension extendee + 0, // [0:45] is the sub-list for field type_name } func init() { file_proto_user_v1_user_proto_init() } @@ -4772,17 +5033,17 @@ func file_proto_user_v1_user_proto_init() { if File_proto_user_v1_user_proto != nil { return } - file_proto_user_v1_user_proto_msgTypes[17].OneofWrappers = []any{} - file_proto_user_v1_user_proto_msgTypes[29].OneofWrappers = []any{} + file_proto_user_v1_user_proto_msgTypes[19].OneofWrappers = []any{} + file_proto_user_v1_user_proto_msgTypes[31].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_user_v1_user_proto_rawDesc, NumEnums: 1, - NumMessages: 53, + NumMessages: 55, NumExtensions: 0, - NumServices: 7, + NumServices: 8, }, GoTypes: file_proto_user_v1_user_proto_goTypes, DependencyIndexes: file_proto_user_v1_user_proto_depIdxs, diff --git a/api/proto/user/v1/user.proto b/api/proto/user/v1/user.proto index b4dbff98..9802ca57 100644 --- a/api/proto/user/v1/user.proto +++ b/api/proto/user/v1/user.proto @@ -16,6 +16,9 @@ message RequestMeta { string country_by_ip = 8; string session_id = 9; string app_code = 10; + string platform = 11; + string language = 12; + string timezone = 13; } // App 是 user-service 维护的 App 注册表投影,用于把客户端包名映射到内部租户键。 @@ -120,6 +123,26 @@ message GetUserMicLifetimeStatsResponse { UserMicLifetimeStats stats = 1; } +// CronBatchRequest 是 cron-service 触发 user-service 后台批处理的统一请求。 +message CronBatchRequest { + RequestMeta meta = 1; + string run_id = 2; + string worker_id = 3; + int32 batch_size = 4; + int64 lock_ttl_ms = 5; + int64 pending_publish_max_age_ms = 6; + int64 publishing_session_max_age_ms = 7; +} + +// CronBatchResponse 返回一个批处理 run 的执行摘要。 +message CronBatchResponse { + int32 claimed_count = 1; + int32 processed_count = 2; + int32 success_count = 3; + int32 failure_count = 4; + bool has_more = 5; +} + // AuthToken 是登录、刷新和注册完成后返回给 gateway 的访问令牌投影。 message AuthToken { int64 user_id = 1; @@ -443,6 +466,13 @@ service UserService { rpc CompleteOnboarding(CompleteOnboardingRequest) returns (CompleteOnboardingResponse); } +// UserCronService 只给 cron-service 调用,业务状态仍由 user-service owner 修改。 +service UserCronService { + rpc ProcessLoginIPRiskBatch(CronBatchRequest) returns (CronBatchResponse); + rpc ProcessRegionRebuildBatch(CronBatchRequest) returns (CronBatchResponse); + rpc CompensateMicOpenSessions(CronBatchRequest) returns (CronBatchResponse); +} + // UserDeviceService 承载 App 设备推送 token 的绑定和失效。 service UserDeviceService { rpc BindPushToken(BindPushTokenRequest) returns (BindPushTokenResponse); diff --git a/api/proto/user/v1/user_grpc.pb.go b/api/proto/user/v1/user_grpc.pb.go index 8d16995b..469f0237 100644 --- a/api/proto/user/v1/user_grpc.pb.go +++ b/api/proto/user/v1/user_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.27.3 +// - protoc v4.25.3 // source: proto/user/v1/user.proto package userv1 @@ -352,6 +352,188 @@ var UserService_ServiceDesc = grpc.ServiceDesc{ Metadata: "proto/user/v1/user.proto", } +const ( + UserCronService_ProcessLoginIPRiskBatch_FullMethodName = "/hyapp.user.v1.UserCronService/ProcessLoginIPRiskBatch" + UserCronService_ProcessRegionRebuildBatch_FullMethodName = "/hyapp.user.v1.UserCronService/ProcessRegionRebuildBatch" + UserCronService_CompensateMicOpenSessions_FullMethodName = "/hyapp.user.v1.UserCronService/CompensateMicOpenSessions" +) + +// UserCronServiceClient is the client API for UserCronService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// UserCronService 只给 cron-service 调用,业务状态仍由 user-service owner 修改。 +type UserCronServiceClient interface { + ProcessLoginIPRiskBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error) + ProcessRegionRebuildBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error) + CompensateMicOpenSessions(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error) +} + +type userCronServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewUserCronServiceClient(cc grpc.ClientConnInterface) UserCronServiceClient { + return &userCronServiceClient{cc} +} + +func (c *userCronServiceClient) ProcessLoginIPRiskBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CronBatchResponse) + err := c.cc.Invoke(ctx, UserCronService_ProcessLoginIPRiskBatch_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userCronServiceClient) ProcessRegionRebuildBatch(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CronBatchResponse) + err := c.cc.Invoke(ctx, UserCronService_ProcessRegionRebuildBatch_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userCronServiceClient) CompensateMicOpenSessions(ctx context.Context, in *CronBatchRequest, opts ...grpc.CallOption) (*CronBatchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CronBatchResponse) + err := c.cc.Invoke(ctx, UserCronService_CompensateMicOpenSessions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// UserCronServiceServer is the server API for UserCronService service. +// All implementations must embed UnimplementedUserCronServiceServer +// for forward compatibility. +// +// UserCronService 只给 cron-service 调用,业务状态仍由 user-service owner 修改。 +type UserCronServiceServer interface { + ProcessLoginIPRiskBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) + ProcessRegionRebuildBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) + CompensateMicOpenSessions(context.Context, *CronBatchRequest) (*CronBatchResponse, error) + mustEmbedUnimplementedUserCronServiceServer() +} + +// UnimplementedUserCronServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedUserCronServiceServer struct{} + +func (UnimplementedUserCronServiceServer) ProcessLoginIPRiskBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ProcessLoginIPRiskBatch not implemented") +} +func (UnimplementedUserCronServiceServer) ProcessRegionRebuildBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ProcessRegionRebuildBatch not implemented") +} +func (UnimplementedUserCronServiceServer) CompensateMicOpenSessions(context.Context, *CronBatchRequest) (*CronBatchResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CompensateMicOpenSessions not implemented") +} +func (UnimplementedUserCronServiceServer) mustEmbedUnimplementedUserCronServiceServer() {} +func (UnimplementedUserCronServiceServer) testEmbeddedByValue() {} + +// UnsafeUserCronServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to UserCronServiceServer will +// result in compilation errors. +type UnsafeUserCronServiceServer interface { + mustEmbedUnimplementedUserCronServiceServer() +} + +func RegisterUserCronServiceServer(s grpc.ServiceRegistrar, srv UserCronServiceServer) { + // If the following call pancis, it indicates UnimplementedUserCronServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&UserCronService_ServiceDesc, srv) +} + +func _UserCronService_ProcessLoginIPRiskBatch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CronBatchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserCronServiceServer).ProcessLoginIPRiskBatch(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserCronService_ProcessLoginIPRiskBatch_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserCronServiceServer).ProcessLoginIPRiskBatch(ctx, req.(*CronBatchRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserCronService_ProcessRegionRebuildBatch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CronBatchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserCronServiceServer).ProcessRegionRebuildBatch(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserCronService_ProcessRegionRebuildBatch_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserCronServiceServer).ProcessRegionRebuildBatch(ctx, req.(*CronBatchRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserCronService_CompensateMicOpenSessions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CronBatchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserCronServiceServer).CompensateMicOpenSessions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserCronService_CompensateMicOpenSessions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserCronServiceServer).CompensateMicOpenSessions(ctx, req.(*CronBatchRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// UserCronService_ServiceDesc is the grpc.ServiceDesc for UserCronService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var UserCronService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "hyapp.user.v1.UserCronService", + HandlerType: (*UserCronServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ProcessLoginIPRiskBatch", + Handler: _UserCronService_ProcessLoginIPRiskBatch_Handler, + }, + { + MethodName: "ProcessRegionRebuildBatch", + Handler: _UserCronService_ProcessRegionRebuildBatch_Handler, + }, + { + MethodName: "CompensateMicOpenSessions", + Handler: _UserCronService_CompensateMicOpenSessions_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/user/v1/user.proto", +} + const ( UserDeviceService_BindPushToken_FullMethodName = "/hyapp.user.v1.UserDeviceService/BindPushToken" UserDeviceService_DeletePushToken_FullMethodName = "/hyapp.user.v1.UserDeviceService/DeletePushToken" diff --git a/api/proto/wallet/v1/wallet.pb.go b/api/proto/wallet/v1/wallet.pb.go index e67719a0..9408550c 100644 --- a/api/proto/wallet/v1/wallet.pb.go +++ b/api/proto/wallet/v1/wallet.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.1 -// protoc v5.27.3 +// protoc v4.25.3 // source: proto/wallet/v1/wallet.proto package walletv1 @@ -5226,6 +5226,178 @@ func (x *ListRechargeBillsResponse) GetTotal() int64 { return 0 } +// CreditTaskRewardRequest 是 activity-service 领取任务奖励时调用的钱包入账命令。 +type CreditTaskRewardRequest 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"` + AppCode string `protobuf:"bytes,2,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` + TargetUserId int64 `protobuf:"varint,3,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"` + Amount int64 `protobuf:"varint,4,opt,name=amount,proto3" json:"amount,omitempty"` + TaskType string `protobuf:"bytes,5,opt,name=task_type,json=taskType,proto3" json:"task_type,omitempty"` + TaskId string `protobuf:"bytes,6,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + CycleKey string `protobuf:"bytes,7,opt,name=cycle_key,json=cycleKey,proto3" json:"cycle_key,omitempty"` + Reason string `protobuf:"bytes,8,opt,name=reason,proto3" json:"reason,omitempty"` +} + +func (x *CreditTaskRewardRequest) Reset() { + *x = CreditTaskRewardRequest{} + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreditTaskRewardRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreditTaskRewardRequest) ProtoMessage() {} + +func (x *CreditTaskRewardRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[53] + 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 CreditTaskRewardRequest.ProtoReflect.Descriptor instead. +func (*CreditTaskRewardRequest) Descriptor() ([]byte, []int) { + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{53} +} + +func (x *CreditTaskRewardRequest) GetCommandId() string { + if x != nil { + return x.CommandId + } + return "" +} + +func (x *CreditTaskRewardRequest) GetAppCode() string { + if x != nil { + return x.AppCode + } + return "" +} + +func (x *CreditTaskRewardRequest) GetTargetUserId() int64 { + if x != nil { + return x.TargetUserId + } + return 0 +} + +func (x *CreditTaskRewardRequest) GetAmount() int64 { + if x != nil { + return x.Amount + } + return 0 +} + +func (x *CreditTaskRewardRequest) GetTaskType() string { + if x != nil { + return x.TaskType + } + return "" +} + +func (x *CreditTaskRewardRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *CreditTaskRewardRequest) GetCycleKey() string { + if x != nil { + return x.CycleKey + } + return "" +} + +func (x *CreditTaskRewardRequest) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +// CreditTaskRewardResponse 返回任务奖励入账流水和用户 COIN 账后余额。 +type CreditTaskRewardResponse 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"` + Balance *AssetBalance `protobuf:"bytes,2,opt,name=balance,proto3" json:"balance,omitempty"` + Amount int64 `protobuf:"varint,3,opt,name=amount,proto3" json:"amount,omitempty"` + GrantedAtMs int64 `protobuf:"varint,4,opt,name=granted_at_ms,json=grantedAtMs,proto3" json:"granted_at_ms,omitempty"` +} + +func (x *CreditTaskRewardResponse) Reset() { + *x = CreditTaskRewardResponse{} + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreditTaskRewardResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreditTaskRewardResponse) ProtoMessage() {} + +func (x *CreditTaskRewardResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[54] + 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 CreditTaskRewardResponse.ProtoReflect.Descriptor instead. +func (*CreditTaskRewardResponse) Descriptor() ([]byte, []int) { + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{54} +} + +func (x *CreditTaskRewardResponse) GetTransactionId() string { + if x != nil { + return x.TransactionId + } + return "" +} + +func (x *CreditTaskRewardResponse) GetBalance() *AssetBalance { + if x != nil { + return x.Balance + } + return nil +} + +func (x *CreditTaskRewardResponse) GetAmount() int64 { + if x != nil { + return x.Amount + } + return 0 +} + +func (x *CreditTaskRewardResponse) GetGrantedAtMs() int64 { + if x != nil { + return x.GrantedAtMs + } + return 0 +} + var File_proto_wallet_v1_wallet_proto protoreflect.FileDescriptor var file_proto_wallet_v1_wallet_proto_rawDesc = []byte{ @@ -6200,173 +6372,207 @@ var file_proto_wallet_v1_wallet_proto_rawDesc = []byte{ 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, 0x22, 0xfc, 0x01, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x54, 0x61, 0x73, 0x6b, + 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 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, 0x16, 0x0a, + 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x61, 0x73, 0x6b, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, + 0x79, 0x63, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x63, 0x79, 0x63, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x22, 0xb6, 0x01, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x52, + 0x65, 0x77, 0x61, 0x72, 0x64, 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, 0x37, 0x0a, 0x07, 0x62, 0x61, 0x6c, 0x61, 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, 0x12, 0x16, 0x0a, + 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x67, 0x72, + 0x61, 0x6e, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x32, 0x9d, 0x15, 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, 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, - 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, - 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, 0x1a, 0x2f, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x5e, 0x0a, - 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x25, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, - 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x23, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 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, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x26, 0x2e, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x21, 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, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x26, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, - 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, - 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x61, 0x0a, 0x11, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, - 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x21, 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, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x2a, 0x2e, 0x68, 0x79, 0x61, - 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x1a, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, + 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, 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, 0x1a, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x5e, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x67, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, - 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 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, 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, 0x6a, 0x0a, 0x13, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x12, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, - 0x1a, 0x26, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, - 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 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, 0x1a, 0x26, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 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, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x5b, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x12, 0x26, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x0e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x26, + 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, 0x11, 0x53, 0x65, 0x74, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, + 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x12, + 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x12, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x1a, 0x2b, + 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 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, 0x67, 0x0a, 0x10, 0x47, + 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, + 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, + 0x75, 0x70, 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, 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, 0x70, 0x0a, 0x16, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2e, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 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, 0x1a, 0x26, - 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, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x69, - 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x27, 0x2e, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, 0x10, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, - 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x61, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x47, 0x69, 0x66, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x67, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x1a, 0x23, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x0d, 0x47, - 0x72, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x25, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x72, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x26, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x12, 0x47, - 0x72, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x12, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 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, 0x1a, 0x26, 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, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, - 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x2b, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x1a, 0x26, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x6a, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x1a, 0x26, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, 0x0a, 0x16, + 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, + 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x1a, 0x26, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64, + 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x73, 0x12, 0x27, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x6a, 0x0a, 0x11, 0x45, 0x71, 0x75, 0x69, 0x70, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, - 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x71, 0x75, 0x69, 0x70, 0x55, 0x73, - 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 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, 0x6d, 0x0a, - 0x12, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, - 0x6e, 0x74, 0x73, 0x12, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x1a, - 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 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, 0x6a, 0x0a, 0x11, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x42, 0x69, 0x6c, 0x6c, - 0x73, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, - 0x42, 0x69, 0x6c, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x42, 0x2a, 0x5a, 0x28, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x69, + 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x23, 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, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, 0x13, 0x53, 0x65, + 0x74, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x2e, 0x76, 0x31, 0x2e, 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, 0x1a, 0x23, + 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, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x0d, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 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, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x12, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x1a, 0x26, 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, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, + 0x11, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, + 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, 0x6a, 0x0a, 0x11, 0x45, 0x71, 0x75, + 0x69, 0x70, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x29, + 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x45, 0x71, 0x75, 0x69, 0x70, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x6d, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x2a, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x1a, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x6a, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x63, 0x68, + 0x61, 0x72, 0x67, 0x65, 0x42, 0x69, 0x6c, 0x6c, 0x73, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x42, 0x69, 0x6c, 0x6c, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 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, 0x67, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, + 0x77, 0x61, 0x72, 0x64, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x54, 0x61, 0x73, + 0x6b, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 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, 0x43, 0x72, 0x65, 0x64, 0x69, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x65, 0x77, 0x61, 0x72, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2a, 0x5a, 0x28, 0x68, 0x79, 0x61, + 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -6381,7 +6587,7 @@ 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, 53) +var file_proto_wallet_v1_wallet_proto_msgTypes = make([]protoimpl.MessageInfo, 55) var file_proto_wallet_v1_wallet_proto_goTypes = []any{ (*DebitGiftRequest)(nil), // 0: hyapp.wallet.v1.DebitGiftRequest (*DebitGiftResponse)(nil), // 1: hyapp.wallet.v1.DebitGiftResponse @@ -6436,6 +6642,8 @@ var file_proto_wallet_v1_wallet_proto_goTypes = []any{ (*RechargeBill)(nil), // 50: hyapp.wallet.v1.RechargeBill (*ListRechargeBillsRequest)(nil), // 51: hyapp.wallet.v1.ListRechargeBillsRequest (*ListRechargeBillsResponse)(nil), // 52: hyapp.wallet.v1.ListRechargeBillsResponse + (*CreditTaskRewardRequest)(nil), // 53: hyapp.wallet.v1.CreditTaskRewardRequest + (*CreditTaskRewardResponse)(nil), // 54: hyapp.wallet.v1.CreditTaskRewardResponse } var file_proto_wallet_v1_wallet_proto_depIdxs = []int32{ 2, // 0: hyapp.wallet.v1.GetBalancesResponse.balances:type_name -> hyapp.wallet.v1.AssetBalance @@ -6460,61 +6668,64 @@ var file_proto_wallet_v1_wallet_proto_depIdxs = []int32{ 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.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 + 2, // 22: hyapp.wallet.v1.CreditTaskRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 0, // 23: hyapp.wallet.v1.WalletService.DebitGift:input_type -> hyapp.wallet.v1.DebitGiftRequest + 3, // 24: hyapp.wallet.v1.WalletService.GetBalances:input_type -> hyapp.wallet.v1.GetBalancesRequest + 5, // 25: hyapp.wallet.v1.WalletService.AdminCreditAsset:input_type -> hyapp.wallet.v1.AdminCreditAssetRequest + 7, // 26: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:input_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockRequest + 9, // 27: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:input_type -> hyapp.wallet.v1.TransferCoinFromSellerRequest + 19, // 28: hyapp.wallet.v1.WalletService.ListResources:input_type -> hyapp.wallet.v1.ListResourcesRequest + 21, // 29: hyapp.wallet.v1.WalletService.GetResource:input_type -> hyapp.wallet.v1.GetResourceRequest + 23, // 30: hyapp.wallet.v1.WalletService.CreateResource:input_type -> hyapp.wallet.v1.CreateResourceRequest + 24, // 31: hyapp.wallet.v1.WalletService.UpdateResource:input_type -> hyapp.wallet.v1.UpdateResourceRequest + 25, // 32: hyapp.wallet.v1.WalletService.SetResourceStatus:input_type -> hyapp.wallet.v1.SetResourceStatusRequest + 27, // 33: hyapp.wallet.v1.WalletService.ListResourceGroups:input_type -> hyapp.wallet.v1.ListResourceGroupsRequest + 29, // 34: hyapp.wallet.v1.WalletService.GetResourceGroup:input_type -> hyapp.wallet.v1.GetResourceGroupRequest + 31, // 35: hyapp.wallet.v1.WalletService.CreateResourceGroup:input_type -> hyapp.wallet.v1.CreateResourceGroupRequest + 32, // 36: hyapp.wallet.v1.WalletService.UpdateResourceGroup:input_type -> hyapp.wallet.v1.UpdateResourceGroupRequest + 33, // 37: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:input_type -> hyapp.wallet.v1.SetResourceGroupStatusRequest + 35, // 38: hyapp.wallet.v1.WalletService.ListGiftConfigs:input_type -> hyapp.wallet.v1.ListGiftConfigsRequest + 37, // 39: hyapp.wallet.v1.WalletService.CreateGiftConfig:input_type -> hyapp.wallet.v1.CreateGiftConfigRequest + 38, // 40: hyapp.wallet.v1.WalletService.UpdateGiftConfig:input_type -> hyapp.wallet.v1.UpdateGiftConfigRequest + 39, // 41: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:input_type -> hyapp.wallet.v1.SetGiftConfigStatusRequest + 41, // 42: hyapp.wallet.v1.WalletService.GrantResource:input_type -> hyapp.wallet.v1.GrantResourceRequest + 42, // 43: hyapp.wallet.v1.WalletService.GrantResourceGroup:input_type -> hyapp.wallet.v1.GrantResourceGroupRequest + 44, // 44: hyapp.wallet.v1.WalletService.ListUserResources:input_type -> hyapp.wallet.v1.ListUserResourcesRequest + 46, // 45: hyapp.wallet.v1.WalletService.EquipUserResource:input_type -> hyapp.wallet.v1.EquipUserResourceRequest + 48, // 46: hyapp.wallet.v1.WalletService.ListResourceGrants:input_type -> hyapp.wallet.v1.ListResourceGrantsRequest + 51, // 47: hyapp.wallet.v1.WalletService.ListRechargeBills:input_type -> hyapp.wallet.v1.ListRechargeBillsRequest + 53, // 48: hyapp.wallet.v1.WalletService.CreditTaskReward:input_type -> hyapp.wallet.v1.CreditTaskRewardRequest + 1, // 49: hyapp.wallet.v1.WalletService.DebitGift:output_type -> hyapp.wallet.v1.DebitGiftResponse + 4, // 50: hyapp.wallet.v1.WalletService.GetBalances:output_type -> hyapp.wallet.v1.GetBalancesResponse + 6, // 51: hyapp.wallet.v1.WalletService.AdminCreditAsset:output_type -> hyapp.wallet.v1.AdminCreditAssetResponse + 8, // 52: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:output_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockResponse + 10, // 53: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:output_type -> hyapp.wallet.v1.TransferCoinFromSellerResponse + 20, // 54: hyapp.wallet.v1.WalletService.ListResources:output_type -> hyapp.wallet.v1.ListResourcesResponse + 22, // 55: hyapp.wallet.v1.WalletService.GetResource:output_type -> hyapp.wallet.v1.GetResourceResponse + 26, // 56: hyapp.wallet.v1.WalletService.CreateResource:output_type -> hyapp.wallet.v1.ResourceResponse + 26, // 57: hyapp.wallet.v1.WalletService.UpdateResource:output_type -> hyapp.wallet.v1.ResourceResponse + 26, // 58: hyapp.wallet.v1.WalletService.SetResourceStatus:output_type -> hyapp.wallet.v1.ResourceResponse + 28, // 59: hyapp.wallet.v1.WalletService.ListResourceGroups:output_type -> hyapp.wallet.v1.ListResourceGroupsResponse + 30, // 60: hyapp.wallet.v1.WalletService.GetResourceGroup:output_type -> hyapp.wallet.v1.GetResourceGroupResponse + 34, // 61: hyapp.wallet.v1.WalletService.CreateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 34, // 62: hyapp.wallet.v1.WalletService.UpdateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 34, // 63: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 36, // 64: hyapp.wallet.v1.WalletService.ListGiftConfigs:output_type -> hyapp.wallet.v1.ListGiftConfigsResponse + 40, // 65: hyapp.wallet.v1.WalletService.CreateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse + 40, // 66: hyapp.wallet.v1.WalletService.UpdateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse + 40, // 67: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:output_type -> hyapp.wallet.v1.GiftConfigResponse + 43, // 68: hyapp.wallet.v1.WalletService.GrantResource:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 43, // 69: hyapp.wallet.v1.WalletService.GrantResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 45, // 70: hyapp.wallet.v1.WalletService.ListUserResources:output_type -> hyapp.wallet.v1.ListUserResourcesResponse + 47, // 71: hyapp.wallet.v1.WalletService.EquipUserResource:output_type -> hyapp.wallet.v1.EquipUserResourceResponse + 49, // 72: hyapp.wallet.v1.WalletService.ListResourceGrants:output_type -> hyapp.wallet.v1.ListResourceGrantsResponse + 52, // 73: hyapp.wallet.v1.WalletService.ListRechargeBills:output_type -> hyapp.wallet.v1.ListRechargeBillsResponse + 54, // 74: hyapp.wallet.v1.WalletService.CreditTaskReward:output_type -> hyapp.wallet.v1.CreditTaskRewardResponse + 49, // [49:75] is the sub-list for method output_type + 23, // [23:49] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name } func init() { file_proto_wallet_v1_wallet_proto_init() } @@ -6528,7 +6739,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: 53, + NumMessages: 55, NumExtensions: 0, NumServices: 1, }, diff --git a/api/proto/wallet/v1/wallet.proto b/api/proto/wallet/v1/wallet.proto index 304c8276..6872ea09 100644 --- a/api/proto/wallet/v1/wallet.proto +++ b/api/proto/wallet/v1/wallet.proto @@ -580,6 +580,26 @@ message ListRechargeBillsResponse { int64 total = 2; } +// CreditTaskRewardRequest 是 activity-service 领取任务奖励时调用的钱包入账命令。 +message CreditTaskRewardRequest { + string command_id = 1; + string app_code = 2; + int64 target_user_id = 3; + int64 amount = 4; + string task_type = 5; + string task_id = 6; + string cycle_key = 7; + string reason = 8; +} + +// CreditTaskRewardResponse 返回任务奖励入账流水和用户 COIN 账后余额。 +message CreditTaskRewardResponse { + string transaction_id = 1; + AssetBalance balance = 2; + int64 amount = 3; + int64 granted_at_ms = 4; +} + // WalletService 是内部账务 gRPC 边界,外部 HTTP 入口仍由 gateway-service 承载。 service WalletService { rpc DebitGift(DebitGiftRequest) returns (DebitGiftResponse); @@ -607,4 +627,5 @@ service WalletService { rpc EquipUserResource(EquipUserResourceRequest) returns (EquipUserResourceResponse); rpc ListResourceGrants(ListResourceGrantsRequest) returns (ListResourceGrantsResponse); rpc ListRechargeBills(ListRechargeBillsRequest) returns (ListRechargeBillsResponse); + rpc CreditTaskReward(CreditTaskRewardRequest) returns (CreditTaskRewardResponse); } diff --git a/api/proto/wallet/v1/wallet_grpc.pb.go b/api/proto/wallet/v1/wallet_grpc.pb.go index b51d983d..3d4ec788 100644 --- a/api/proto/wallet/v1/wallet_grpc.pb.go +++ b/api/proto/wallet/v1/wallet_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.27.3 +// - protoc v4.25.3 // source: proto/wallet/v1/wallet.proto package walletv1 @@ -44,6 +44,7 @@ const ( 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_CreditTaskReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditTaskReward" ) // WalletServiceClient is the client API for WalletService service. @@ -77,6 +78,7 @@ type WalletServiceClient interface { EquipUserResource(ctx context.Context, in *EquipUserResourceRequest, opts ...grpc.CallOption) (*EquipUserResourceResponse, error) ListResourceGrants(ctx context.Context, in *ListResourceGrantsRequest, opts ...grpc.CallOption) (*ListResourceGrantsResponse, error) ListRechargeBills(ctx context.Context, in *ListRechargeBillsRequest, opts ...grpc.CallOption) (*ListRechargeBillsResponse, error) + CreditTaskReward(ctx context.Context, in *CreditTaskRewardRequest, opts ...grpc.CallOption) (*CreditTaskRewardResponse, error) } type walletServiceClient struct { @@ -337,6 +339,16 @@ func (c *walletServiceClient) ListRechargeBills(ctx context.Context, in *ListRec return out, nil } +func (c *walletServiceClient) CreditTaskReward(ctx context.Context, in *CreditTaskRewardRequest, opts ...grpc.CallOption) (*CreditTaskRewardResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreditTaskRewardResponse) + err := c.cc.Invoke(ctx, WalletService_CreditTaskReward_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // WalletServiceServer is the server API for WalletService service. // All implementations must embed UnimplementedWalletServiceServer // for forward compatibility. @@ -368,6 +380,7 @@ type WalletServiceServer interface { EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error) ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, error) ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error) + CreditTaskReward(context.Context, *CreditTaskRewardRequest) (*CreditTaskRewardResponse, error) mustEmbedUnimplementedWalletServiceServer() } @@ -453,6 +466,9 @@ func (UnimplementedWalletServiceServer) ListResourceGrants(context.Context, *Lis func (UnimplementedWalletServiceServer) ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListRechargeBills not implemented") } +func (UnimplementedWalletServiceServer) CreditTaskReward(context.Context, *CreditTaskRewardRequest) (*CreditTaskRewardResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreditTaskReward not implemented") +} func (UnimplementedWalletServiceServer) mustEmbedUnimplementedWalletServiceServer() {} func (UnimplementedWalletServiceServer) testEmbeddedByValue() {} @@ -924,6 +940,24 @@ func _WalletService_ListRechargeBills_Handler(srv interface{}, ctx context.Conte return interceptor(ctx, in, info, handler) } +func _WalletService_CreditTaskReward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreditTaskRewardRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletServiceServer).CreditTaskReward(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WalletService_CreditTaskReward_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletServiceServer).CreditTaskReward(ctx, req.(*CreditTaskRewardRequest)) + } + return interceptor(ctx, in, info, handler) +} + // WalletService_ServiceDesc is the grpc.ServiceDesc for WalletService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1031,6 +1065,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListRechargeBills", Handler: _WalletService_ListRechargeBills_Handler, }, + { + MethodName: "CreditTaskReward", + Handler: _WalletService_CreditTaskReward_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "proto/wallet/v1/wallet.proto", diff --git a/deploy/mysql/initdb/999_local_grants.sql b/deploy/mysql/initdb/999_local_grants.sql index 1b0c79dd..3103b279 100644 --- a/deploy/mysql/initdb/999_local_grants.sql +++ b/deploy/mysql/initdb/999_local_grants.sql @@ -7,5 +7,6 @@ GRANT ALL PRIVILEGES ON hyapp_user.* TO 'hyapp'@'%'; GRANT ALL PRIVILEGES ON hyapp_wallet.* TO 'hyapp'@'%'; GRANT ALL PRIVILEGES ON hyapp_activity.* TO 'hyapp'@'%'; GRANT ALL PRIVILEGES ON hyapp_admin.* TO 'hyapp'@'%'; +GRANT ALL PRIVILEGES ON hyapp_cron.* TO 'hyapp'@'%'; FLUSH PRIVILEGES; diff --git a/docker-compose.yml b/docker-compose.yml index a96c3cfe..b22d85a8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,6 +51,10 @@ services: depends_on: mysql: condition: service_healthy + user-service: + condition: service_healthy + activity-service: + condition: service_healthy healthcheck: test: ["CMD-SHELL", "/app/grpc-health-probe -addr=127.0.0.1:13004 -service=wallet-service"] interval: 5s @@ -94,6 +98,27 @@ services: retries: 20 start_period: 10s + cron-service: + build: + context: . + dockerfile: services/cron-service/Dockerfile + container_name: cron-service + ports: + - "13007:13007" + depends_on: + mysql: + condition: service_healthy + user-service: + condition: service_healthy + activity-service: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "/app/grpc-health-probe -addr=127.0.0.1:13007 -service=cron-service"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 10s + mysql: image: mysql:8.4 container_name: hyapp-mysql @@ -116,6 +141,7 @@ services: - ./deploy/mysql/initdb/005_utf8mb4_chinese_support.sql:/docker-entrypoint-initdb.d/005_utf8mb4_chinese_support.sql:ro - ./deploy/mysql/initdb/006_admin_database.sql:/docker-entrypoint-initdb.d/006_admin_database.sql:ro - ./deploy/mysql/initdb/007_resource_group_wallet_asset_items.sql:/docker-entrypoint-initdb.d/007_resource_group_wallet_asset_items.sql:ro + - ./services/cron-service/deploy/mysql/initdb/001_cron_service.sql:/docker-entrypoint-initdb.d/008_cron_service.sql:ro - ./deploy/mysql/initdb/999_local_grants.sql:/docker-entrypoint-initdb.d/999_local_grants.sql:ro healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -proot --silent"] diff --git a/docs/app-message-tab-architecture.md b/docs/app-message-tab-architecture.md index c3c63036..698b94cc 100644 --- a/docs/app-message-tab-architecture.md +++ b/docs/app-message-tab-architecture.md @@ -32,7 +32,7 @@ | 系统消息生产者 | `TODO` | wallet、room、user host domain 已有各自业务/outbox 方向 | 尚无统一 `CreateInboxMessage` 或 MQ consumer | | 活动消息生产者 | `TODO` | activity-service 目前只有活动状态查询 | 需要活动上线、奖励、任务、运营触达入箱 | | 未读数缓存 | `TODO` | 无 Redis key 设计落地 | 需要缓存失效和 fallback 统计 | -| 广播/fanout | `DONE` | `message_fanout_jobs` + `CreateFanoutJob` + activity fanout worker;region/country/all_active_users 通过 user-service `ListUserIDs` 游标取目标 | 还需要后台运营生产者接入 | +| 广播/fanout | `DONE` | `message_fanout_jobs` + `CreateFanoutJob` + `ActivityCronService.ProcessMessageFanoutBatch`;region/country/all_active_users 通过 user-service `ListUserIDs` 游标取目标 | 还需要后台运营生产者接入 | 这张表只描述当前仓库事实和缺口。后续实现必须先补 Redis 未读缓存和第一个领域生产者,不能让生产服务直接写 inbox 表。 @@ -199,7 +199,7 @@ user_inbox_messages( ### Fanout Jobs -大范围活动消息不要在 HTTP 请求里同步 fanout。后台只创建任务,worker 分批写入,并记录进度和失败原因。 +大范围活动消息不要在 HTTP 请求里同步 fanout。后台只创建任务,cron-service 触发 activity-service 分批写入,并记录进度和失败原因。 ```sql message_fanout_jobs( @@ -457,14 +457,14 @@ sequenceDiagram | `user_group` | 区域活动、主播群体通知、指定用户列表 | 创建 fanout job,分批写 inbox | | `global` | 全 App 公告 | 首版仍建议 fanout 物化;超大规模后再做 broadcast lazy state | -Fanout worker 要求: +Fanout batch 要求: - 每轮锁定一批待执行 job,使用 `locked_by/locked_until_ms` 防止多实例重复处理。 - 按 `cursor_user_id` 分页查询目标用户,不使用 offset。 - 每个目标用户的 `producer_event_id` 必须稳定,例如 `fanout:{job_id}:{user_id}`。 - 单批失败不能让整个 job 丢失,记录 `failure_count/error_message` 并继续或进入 retry。 - 目标范围按区域/国家选择时,使用 user-service 当前主数据;是否需要冻结目标用户快照由具体活动决定。 -- 大型 fanout 配置必须支持 `poll_interval`、`batch_size`、`max_retry`、`lock_ttl`。 +- 大型 fanout 配置必须支持 cron `interval`、`batch_size`、`lock_ttl`;失败重试次数由 activity-service owner 侧状态机控制。 ## Read Consistency @@ -501,17 +501,21 @@ Fanout worker 要求: ## Configuration -首版实现需要在 `activity-service` 增加 message inbox 配置,并同步本地 `config.yaml`、Docker `config.docker.yaml`、线上 `config.tencent.example.yaml`: +首版实现需要在 `activity-service` 增加 message inbox 读取配置;`message_fanout_jobs` 由 `cron-service` 调度,不再由 activity-service 内置 worker 轮询: ```yaml message_inbox: unread_cache_ttl: 60s - fanout_worker: + +# services/cron-service/configs/config*.yaml +tasks: + message_fanout: enabled: true - poll_interval: 1s - batch_size: 500 + interval: 1s + timeout: 10s lock_ttl: 30s - max_retry: 8 + batch_size: 500 + app_codes: ["lalu"] ``` 如果消息 owner 后续拆成独立 `message-service`,这些配置迁移到新服务;gateway 只需要更新 message gRPC 地址。 @@ -523,7 +527,7 @@ message_inbox: 3. 在 `activity-service` 增加 inbox domain/repository/service,先实现单用户入箱、列表、未读、已读、删除。 4. 在 `gateway-service` 增加 activity/message client 和 App HTTP:`/api/v1/messages/tabs`、`/api/v1/messages`、`/api/v1/messages/{id}/read`、`/api/v1/messages/read-all`、`DELETE /api/v1/messages/{id}`。 5. 接入第一个系统生产者,建议从提现审核或 host 申请结果开始,因为它们天然是单用户消息。 -6. 增加 fanout job 和 worker,先支持 `region`、`country`、`user_ids`、`all_active_users`。 +6. 增加 fanout job 和 `ActivityCronService.ProcessMessageFanoutBatch`,由 cron-service 调度,先支持 `region`、`country`、`user_ids`、`all_active_users`。 7. 增加 `/api/v1/users/profiles:batch`,给腾讯云 IM 用户会话列表补头像、昵称、短号。 8. 增加 Redis 未读数缓存和缓存失效逻辑。 9. 补 OpenAPI 文档、Docker 配置、线上配置示例和端到端冒烟脚本。 @@ -576,7 +580,7 @@ docker compose down - activity-service inbox repository 单元/集成测试:幂等、分页、未读、已读、删除、过期。 - gateway transport 测试:JWT 用户 ID 生效、app_code 透传、envelope、错误映射。 -- fanout worker 测试:锁 job、分页、重复 target 幂等、失败重试。 +- fanout batch 测试:锁 job、分页、重复 target 幂等、失败重试。 - producer consumer 测试:同 event 重复消费只入箱一次。 - profiles batch 测试:只返回安全展示字段。 diff --git a/docs/cron-service-architecture.md b/docs/cron-service-architecture.md new file mode 100644 index 00000000..4a7e8eb8 --- /dev/null +++ b/docs/cron-service-architecture.md @@ -0,0 +1,301 @@ +# Cron Service Architecture And Task Inventory + +本文定义 `cron-service` 的服务边界、运行模型、当前代码中可迁移的后台任务,以及哪些任务不能放进 cron-service。 + +核心结论: + +- `cron-service` 是调度和批处理触发进程,不是新的业务状态 owner。 +- 它不能直接 import 其他服务的 `internal` 包,也不能绕过 owner service 直接改业务表。 +- 房间状态、钱包账务、用户身份、活动消息等业务状态仍由各自服务处理;cron-service 只调用 owner service 暴露的内部 gRPC 批处理接口。 +- 不要把所有 `time.NewTicker` 都迁到 cron-service。事件投递、Room Cell 清理和高频 outbox 消费不是通用 cron 问题。 + +## Current Worker Inventory + +当前仓库已经有多类后台 worker: + +| Service | Current Worker | Current Location | Decision | +| --- | --- | --- | --- | +| `room-service` | stale presence cleanup | `services/room-service/internal/room/service/presence.go` | 留在 `room-service` | +| `room-service` | mic publish timeout cleanup | `services/room-service/internal/room/service/mic_publish_timeout.go` | 留在 `room-service` | +| `room-service` | room outbox delivery to Tencent IM | `services/room-service/internal/room/service/outbox_worker.go` | 留在 `room-service` 或后续拆 `room-worker`,不进通用 cron | +| `user-service` | region rebuild task | `UserCronService.ProcessRegionRebuildBatch` | 已迁移到 cron-service 调度,旧内置 ticker 已删除 | +| `user-service` | login IP risk job | `UserCronService.ProcessLoginIPRiskBatch` | 已迁移到 cron-service 调度,旧内置 ticker 已删除 | +| `user-service` | invite recharge wallet outbox consumer | `services/user-service/internal/service/invite/service.go` | 先不迁,需补 durable cursor 或事件总线 | +| `user-service` | room mic outbox consumer | `services/user-service/internal/service/mictime/service.go` | 先不迁,属于事件消费,不是普通 cron | +| `user-service` | mic open session compensation | `UserCronService.CompensateMicOpenSessions` | 已迁移到 cron-service 调度,旧内置 ticker 已删除 | +| `activity-service` | message fanout job | `ActivityCronService.ProcessMessageFanoutBatch` | 已迁移到 cron-service 调度,旧内置 ticker 已删除 | +| `server/admin` | admin export jobs | `server/admin/internal/job/runner.go` | 留在 admin module | +| `wallet-service` | wallet outbox table exists, no worker | `services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql` | 后续做 wallet outbox worker,不进通用 cron | + +## Service Boundary + +`cron-service` 的职责是“按计划触发 owner service 处理一批任务”,不是“拿到所有数据库连接后自己处理业务”。 + +```mermaid +graph LR + Cron["cron-service\nscheduler + run history"] --> CronDB[("hyapp_cron")] + + Cron -->|"ProcessLoginIPRiskBatch"| User["user-service"] + Cron -->|"ProcessRegionRebuildBatch"| User + Cron -->|"CompensateMicOpenSessions"| User + Cron -->|"ProcessMessageFanoutBatch"| Activity["activity-service"] + Cron -->|"ExpireVipBatch / SettlementBatch"| Wallet["wallet-service"] + + User --> UserDB[("hyapp_user")] + Activity --> ActivityDB[("hyapp_activity")] + Wallet --> WalletDB[("hyapp_wallet")] + Room["room-service"] --> RoomDB[("hyapp_room")] +``` + +边界规则: + +| Rule | Explanation | +| --- | --- | +| 只调 gRPC | cron-service 通过 `hyapp.local/api` 生成 client 调用内部 RPC,不 import 其他服务实现 | +| 不直接写业务库 | cron-service 只写自己的 run history 和 schedule lock;业务表由 owner service 写 | +| owner service 自己 claim | 每个批处理 RPC 在 owner service 内完成任务 claim、幂等、事务和状态推进 | +| request meta 仍要透传 | cron 调内部 RPC 也必须带 `RequestMeta.request_id/app_code/caller` | +| 多 App 按 app_code 执行 | cron 配置任务的 `app_codes`,每个 app_code 独立触发,避免跨租户混跑 | +| 失败不影响主链路 | cron 挂了只造成后台积压,不能阻塞登录、送礼、进房等在线路径 | + +## What Goes Into Cron-Service + +适合放进 cron-service 的任务必须同时满足: + +- 不是用户请求主链路。 +- 有持久任务表、状态机或可重试条件。 +- 单批处理可幂等,重复调用不会重复入账、重复发奖励或重复清状态。 +- owner service 已经有或可以新增 `Process...Batch` 这类内部 RPC。 +- 任务失败后可以下一轮重试,且需要运行记录、告警和统一调度。 + +当前已放入 cron-service 调度的任务: + +| Priority | Task | Owner | Why | +| --- | --- | --- | --- | +| P0 | login IP risk job | `user-service` | 登录后异步 IP 查询,已有 `login_ip_risk_jobs`、claim TTL、session revoke 逻辑;cron 只触发 `ProcessLoginIPRiskBatch` | +| P1 | user region rebuild | `user-service` | 已有 `user_region_rebuild_tasks` 和 `ProcessNextRegionRebuildTask`;cron 按 app_code 触发单批 | +| P1 | message fanout job | `activity-service` | 已有 `message_fanout_jobs`、cursor、lock TTL、retry;cron 触发后 owner service 自己 claim | +| P2 | mic open session compensation | `user-service` | 低频补偿任务,修正 user mic facts,不反写 Room Cell;窗口参数放在 cron task 配置 | + +后续推荐接入 cron-service 的任务: + +| Priority | Task | Owner | Why | +| --- | --- | --- | --- | +| P2 | VIP expiration scan | `wallet-service` | VIP 文档已定义过期扫描,未来由 wallet-service 批量过期并写 outbox | +| P2 | daily task claim retry / expiry | `activity-service` | 活动任务中的 pending claim、过期状态适合后台补偿 | +| P3 | host salary settlement trigger | `user-service` + `wallet-service` | 日结、周结、半月结、月结是明确的定时结算入口,但实际工资单和入账仍由 owner service 完成 | + +## What Must Not Go Into Cron-Service + +以下任务不能放到通用 cron-service: + +| Task | Reason | +| --- | --- | +| Room Cell stale presence cleanup | 依赖本节点已装载 Room Cell 和 Redis lease,必须由 `room-service` owner 执行命令链路 | +| Mic publish timeout cleanup | 必须对当前 Room Cell 状态做二次校验并执行 `MicDown`,不能让 cron 直接清麦 | +| room outbox delivery | 这是 room-service 的事件投递补偿,不是通用定时任务;需要和 Tencent IM bridge、outbox 状态语义绑定 | +| wallet ledger transaction | 金币、钻石、积分、余额的加减和冻结必须在 `wallet-service` 事务内完成 | +| wallet outbox publish | 属于 wallet-service outbox relay 或 MQ 投递,不应该由通用 cron 直接扫表改状态 | +| invite recharge outbox consumer | 当前使用 in-memory cursor 扫 wallet outbox,迁移前必须先做 durable offset 或正式事件总线 | +| room mic outbox consumer | 属于高频事件消费,当前靠消费幂等表兜底;迁移前必须先做 durable offset,不能用 cron 从头扫 | +| admin export jobs | 属于 `server/admin` 独立 module,涉及后台权限、导出文件和 artifact 下载,继续留在 admin runner | + +## Runtime Model + +cron-service 是长运行进程,不是一次性脚本。它本身只需要自己的库和内部服务地址。 + +建议本地端口: + +| Port | Usage | +| --- | --- | +| `13007` | `cron-service` gRPC health 和内部管理接口 | + +建议目录: + +```text +services/cron-service/ + cmd/server/main.go + configs/ + config.yaml + config.docker.yaml + config.tencent.example.yaml + deploy/mysql/initdb/ + 001_cron_service.sql + internal/ + app/ + config/ + scheduler/ + storage/mysql/ + integration/ + transport/grpc/ +``` + +建议配置: + +```yaml +service_name: cron-service +node_id: cron-local +environment: local +grpc_addr: ":13007" +mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_cron?parseTime=true&charset=utf8mb4&loc=Local" + +user_service_addr: "127.0.0.1:13005" +activity_service_addr: "127.0.0.1:13006" +wallet_service_addr: "127.0.0.1:13004" + +tasks: + login_ip_risk: + enabled: true + interval: "5s" + timeout: "3s" + lock_ttl: "30s" + batch_size: 100 + app_codes: ["lalu"] + user_region_rebuild: + enabled: true + interval: "5s" + timeout: "5s" + lock_ttl: "30s" + batch_size: 500 + app_codes: ["lalu"] + message_fanout: + enabled: true + interval: "1s" + timeout: "10s" + lock_ttl: "30s" + batch_size: 500 + app_codes: ["lalu"] + mic_open_session_compensation: + enabled: true + interval: "60s" + timeout: "10s" + lock_ttl: "60s" + batch_size: 100 + pending_publish_max_age: "2m" + publishing_session_max_age: "12h" + app_codes: ["lalu"] +``` + +## Cron Database + +cron-service 需要自己的运行记录和调度锁,避免多个 cron 实例同时触发同一个计划。 + +```sql +CREATE TABLE cron_task_leases ( + task_name VARCHAR(96) NOT NULL, + app_code VARCHAR(32) NOT NULL, + owner_node_id VARCHAR(128) NOT NULL, + lease_until_ms BIGINT NOT NULL, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (task_name, app_code), + KEY idx_cron_task_leases_expire (lease_until_ms) +); + +CREATE TABLE cron_task_runs ( + run_id VARCHAR(96) NOT NULL, + task_name VARCHAR(96) NOT NULL, + app_code VARCHAR(32) NOT NULL, + owner_node_id VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL, + claimed_count INT NOT NULL DEFAULT 0, + processed_count INT NOT NULL DEFAULT 0, + success_count INT NOT NULL DEFAULT 0, + failure_count INT NOT NULL DEFAULT 0, + started_at_ms BIGINT NOT NULL, + finished_at_ms BIGINT NULL, + duration_ms BIGINT NOT NULL DEFAULT 0, + error_message VARCHAR(512) NOT NULL DEFAULT '', + PRIMARY KEY (run_id), + KEY idx_cron_task_runs_task_time (task_name, app_code, started_at_ms), + KEY idx_cron_task_runs_status (status, started_at_ms) +); +``` + +`cron_task_leases` 只防止调度层重复触发;真正的业务并发安全仍必须由 owner service 的任务表锁和幂等保证。 + +## Internal RPC Contract + +不要设计一个可以执行任意任务名的通用 `Execute(task_name, payload)`。这种接口最后会变成绕过领域边界的后门。 + +每个 owner service 暴露明确的内部批处理 RPC,例如: + +```protobuf +message CronBatchRequest { + RequestMeta meta = 1; + string run_id = 2; + string worker_id = 3; + int32 batch_size = 4; + int64 lock_ttl_ms = 5; + int64 pending_publish_max_age_ms = 6; + int64 publishing_session_max_age_ms = 7; +} + +message CronBatchResponse { + int32 claimed_count = 1; + int32 processed_count = 2; + int32 success_count = 3; + int32 failure_count = 4; + bool has_more = 5; +} +``` + +owner service 侧实现要求: + +- RPC 方法只处理一个明确任务,例如 `ProcessLoginIPRiskBatch`。 +- 任务 claim、事务、状态推进、幂等和业务错误都留在 owner service。 +- 返回 `has_more=true` 时,cron-service 可以在同一轮继续拉下一批,直到空批次或达到单轮最大耗时。 +- RPC 必须能安全重复调用。cron 超时后无法确认结果时,下一轮重试不能造成重复副作用。 + +## Current Implementation + +当前实现已经完成以下迁移: + +1. `login_ip_risk_jobs`:cron-service 调 `user-service` 的 `ProcessLoginIPRiskBatch`,旧 `RunLoginIPRiskWorker` 已删除。 +2. `user_region_rebuild_tasks`:cron-service 调 `user-service` 的 `ProcessRegionRebuildBatch`,旧 `RunRegionRebuildWorker` 已删除。 +3. `message_fanout_jobs`:cron-service 调 `activity-service` 的 `ProcessMessageFanoutBatch`,旧 `RunFanoutWorker` 已删除。 +4. `user_mic_sessions` open session compensation:cron-service 调 `user-service` 的 `CompensateMicOpenSessions`,旧 `RunOpenSessionCompensationWorker` 已删除。 + +cron-service 可以直接开启多个实例;调度层通过 `cron_task_leases` 防重复触发,业务层仍通过 owner service 内部的任务 claim、状态机和幂等保证最终正确。 + +### Event Consumers Stay Separate + +以下不是 cron-service 的首要工作: + +- invite recharge wallet outbox consumer。 +- room mic outbox consumer。 +- wallet outbox relay。 +- activity room outbox consumer。 + +这些需要的是事件消费基础设施:MQ、Redis Stream、或 MySQL durable offset。cron-service 不能用“每隔几秒从头扫 outbox”的方式替代事件消费者。 + +### Future Scheduled Business Jobs + +在对应业务落地后再接入: + +- VIP 过期扫描。 +- 主播工资周期结算触发。 +- 钻石兑换过期订单补偿。 +- 提现人工打款超时提醒。 +- daily task 过期和 pending reward claim 重试。 + +## Operational Notes + +| Area | Requirement | +| --- | --- | +| Shutdown | 停止调度新 run,等待当前 RPC 在 timeout 内结束,再关闭连接 | +| Logs | 固定输出 `task_name/run_id/app_code/worker_id/duration_ms/claimed_count/processed_count/status` | +| Metrics | 每个任务记录积压、成功数、失败数、最老 pending age、运行耗时 | +| Alerts | 连续失败、积压持续增长、最老 pending 超阈值、owner service gRPC unavailable | +| Backoff | 单任务连续失败时指数退避,但不能影响其他任务 | +| Idempotency | cron-service 只负责少触发,业务不重复必须由 owner service 保证 | + +## Final Placement Decision + +当前 `cron-service` 已经承接四个成熟批处理任务;后续只继续接明确的定时批处理,不接在线主链路和事件消费者。 + +最终放置原则: + +1. `login_ip_risk_jobs`、`user_region_rebuild_tasks`、`message_fanout_jobs`、mic open session compensation 由 cron-service 调度。 +2. owner service 暴露明确 RPC,cron-service 不直接访问业务库。 +3. outbox/event consumer 单独做 durable offset 或 MQ,不混进 cron-service。 +4. 未来 VIP、主播工资、daily task 这类周期任务统一接入 cron-service。 diff --git a/docs/daily-task-system-architecture.md b/docs/daily-task-system-architecture.md new file mode 100644 index 00000000..1aa344eb --- /dev/null +++ b/docs/daily-task-system-architecture.md @@ -0,0 +1,579 @@ +# 每日任务系统架构方案 + +本文定义 App 每日任务和专属任务的产品边界、服务归属、事件来源、数据模型、后台管理和关键异常处理。 + +## 目标 + +- 后台增加 `活动管理` 一级菜单,`每日任务` 作为活动管理下的二级菜单。 +- `每日任务` 页面包含两个 Tab:`每日任务`、`专属任务`。 +- 每日任务按服务器统一时区每天刷新,当天只能领取一次,超额完成不结转到明天。 +- 专属任务是用户维度终身只能完成并领取一次的任务。 +- 任务奖励统一发金币,金币发放必须走 `wallet-service` 账本。 +- 任务进度由服务端事实事件驱动,客户端不能上报任务进度。 + +## 非目标 + +- 首版不做客户端自定义任务排序。 +- 首版不做多奖励类型;奖励只支持金币。 +- 首版不做任务进度回滚;退款、撤销、CP 解除等后续事件进入风控和补偿,不直接减已完成任务。 +- 首版不让 room-service、wallet-service、user-service 同步调用任务系统更新进度。 + +## 服务边界 + +| 模块 | 责任 | 不负责 | +| --- | --- | --- | +| `server/admin` | 菜单、后台 HTTP API、任务配置管理、启停、排序、查询统计 | 不直接写 app 业务库;通过 protobuf client 或明确集成接口访问后端 | +| `activity-service` | 任务定义、任务版本、任务进度、完成状态、领奖状态、任务事件消费幂等 | 不直接改钱包余额,不拥有房间/钱包/用户基础事实 | +| `wallet-service` | 金币奖励入账、账本幂等、奖励流水 | 不判断任务是否完成 | +| `user-service` | CP 关系事实、用户麦上时长事实 | 不判断任务奖励 | +| `room-service` | 房间和麦位状态 owner,产生房间 outbox | 不同步更新任务 | + +建议首版把任务系统放在 `activity-service`,因为它已经承接活动、消息、fanout 等低频活动能力。后续如果任务规模独立膨胀,再拆 `task-service`。 + +## 总体架构 + +```mermaid +flowchart LR + Admin["admin 活动管理 > 每日任务"] --> AdminAPI["admin-server task APIs"] + AdminAPI --> ActivityAdmin["activity-service AdminTaskService"] + + Room["room-service outbox"] --> UserMic["user-service mic time consumer"] + UserMic --> UserOutbox["user-service user_outbox"] + Wallet["wallet-service outbox"] --> TaskConsumer["activity-service task consumers"] + UserOutbox --> TaskConsumer + CP["user-service CP outbox"] --> TaskConsumer + + TaskConsumer --> TaskDB["activity MySQL task tables"] + App["App task page"] --> Gateway["gateway-service"] + Gateway --> ActivityQuery["activity-service TaskQueryService"] + Gateway --> ActivityClaim["activity-service TaskClaimService"] + ActivityClaim --> WalletGrant["wallet-service CreditTaskReward"] +``` + +核心原则: + +- 任务系统只消费事实事件,不参与房间、钱包、CP 主链路。 +- 金币奖励只通过钱包账本发放。 +- 每个事件必须有稳定 `event_id`,任务消费必须幂等。 +- 任务完成和领奖是两个状态,完成不等于已经发奖。 + +## 任务类型 + +| 分类 | 进度指标 | 事件来源 | 口径 | +| --- | --- | --- | --- | +| CP 任务 | 组成 CP 次数 | user-service CP 关系创建事件 | 以 `CpRelationshipCreated` 为准,解除 CP 不回滚 | +| 上麦任务 | 麦上有效分钟数 | user-service 麦位时长聚合事件 | 使用 `mic_online_ms`,不是单纯占麦时间 | +| 游戏任务 | 游戏消耗金币数 | wallet-service 游戏扣费事件 | 只统计成功扣费的 COIN | +| 送礼任务 | 送礼消耗金币数 | wallet-service 送礼扣费事件 | 使用钱包扣费金额,不能用客户端礼物价格 | +| 幸运礼物任务 | 幸运礼物消耗金币数或中奖金币数 | wallet-service 幸运礼物结算事件 | 首版一个任务只配置一种指标:`spend_coin` 或 `win_coin` | +| 充值任务 | 充值金币数 | wallet-service 充值成功事件 | Google、Apple、三方支付、币商转普通金币都可以按配置纳入口径 | + +### 上麦任务口径 + +上麦任务默认读取 `user_mic_daily_stats.mic_online_ms` 或消费 `UserMicSessionClosed` 派生事件。`mic_online_ms` 只从确认发流开始累计,未发声、只占麦、pending_publish 不计入有效麦上时间。 + +跨天上麦必须按服务器任务时区拆分到不同 `task_day`,不能把整段时长归到下麦当天。 + +### 幸运礼物任务口径 + +幸运礼物任务需要避免把“消耗”和“中奖”混在同一个字段: + +| 指标 | 含义 | +| --- | --- | +| `lucky_gift_spend_coin` | 用户购买/发送幸运礼物实际消耗金币 | +| `lucky_gift_win_coin` | 用户因幸运礼物中奖获得的金币价值 | + +如果运营需要“消耗 n 金币且中奖 n 金币”,应在第二阶段支持多条件 AND。首版建议一个任务一个指标,减少配置歧义。 + +## 任务周期 + +### 每日任务 + +- 周期 key 使用服务器统一时区计算,例如 `Asia/Shanghai` 下的 `2026-05-09`。 +- 服务器时区是系统配置,不使用用户手机时区。 +- 事件归属日按 `occurred_at_ms` 转服务器时区计算,不按消费时间。 +- 用户当天完成后只能领取一次。 +- 当天超额完成只保留当天进度,不结转到明天。 +- 每日刷新不需要批量清空用户数据;查询时按当前 `task_day` 读取或懒创建进度。 + +### 专属任务 + +- 周期 key 固定为 `lifetime`。 +- 用户终身只能完成一次、领取一次。 +- 首版可作为全体用户可见的 lifetime 任务。 +- 如果后续要“指定用户专属”,再增加任务分配表,不要把用户列表塞进任务定义 JSON。 + +## 后台管理 + +后台菜单: + +```text +活动管理 + - 每日任务 + Tab 1: 每日任务 + Tab 2: 专属任务 +``` + +### 列表字段 + +| 字段 | 说明 | +| --- | --- | +| 任务 ID | 系统生成 | +| 任务名称 | App 展示名 | +| 任务分类 | CP / 上麦 / 游戏 / 送礼 / 幸运礼物 / 充值 | +| 任务类型 | 每日任务 / 专属任务 | +| 目标值 | n 次、n 分钟、n 金币 | +| 奖励金币 | 完成后领取的金币数 | +| 状态 | draft / active / paused / archived | +| 生效时间 | 可选 | +| 结束时间 | 可选 | +| 排序值 | App 列表排序 | +| 当前版本 | 配置版本 | +| 创建人/更新时间 | 审计 | + +### 新增/编辑任务 + +后台表单: + +| 字段 | 必填 | 说明 | +| --- | --- | --- | +| 任务类型 | 是 | daily / exclusive | +| 任务分类 | 是 | 固定枚举 | +| 任务名称 | 是 | App 展示 | +| 任务描述 | 否 | App 展示 | +| 指标类型 | 是 | 按分类收敛,例如 `mic_online_minutes`、`gift_spend_coin` | +| 目标值 | 是 | 必须大于 0 | +| 奖励金币 | 是 | 必须大于 0 | +| 排序值 | 是 | 小值靠前 | +| 生效/结束时间 | 否 | 控制任务上下架窗口 | +| 状态 | 是 | draft 保存草稿,active 对 App 生效 | + +编辑 active 任务时建议生成新版本。每日任务的新版本默认从下一个 `task_day` 生效,避免当天用户看到目标值突然变化。 + +## App 接口 + +### 查询任务列表 + +```text +GET /api/v1/tasks/tabs +``` + +返回两个分区: + +- `daily` +- `exclusive` + +每个任务返回: + +| 字段 | 说明 | +| --- | --- | +| `task_id` | 任务 ID | +| `task_type` | daily / exclusive | +| `category` | 任务分类 | +| `title` | 展示名称 | +| `description` | 展示描述 | +| `target_value` | 目标值 | +| `progress_value` | 当前进度 | +| `reward_coin_amount` | 奖励金币 | +| `status` | in_progress / completed / claimed / expired | +| `claimable` | 是否可领取 | +| `task_day` | daily 任务返回当前日期 | +| `server_time_ms` | 服务端时间 | +| `next_refresh_at_ms` | 下次刷新时间 | + +### 领取奖励 + +```text +POST /api/v1/tasks/claim +``` + +请求: + +```json +{ + "task_id": "123", + "task_type": "daily", + "task_day": "2026-05-09", + "command_id": "client-or-gateway-generated-id" +} +``` + +规则: + +- 必须已完成。 +- 必须未领取。 +- daily 只能领取当前 `task_day` 的任务。 +- exclusive 只能领取 `lifetime` 任务。 +- `command_id` 幂等,重复请求返回同一结果。 + +可选第二阶段增加: + +```text +POST /api/v1/tasks/claim-all +``` + +## 数据模型 + +### `task_definitions` + +任务定义当前版本的读模型。 + +| 字段 | 说明 | +| --- | --- | +| `task_id` | 主键 | +| `app_code` | App 租户 | +| `task_type` | daily / exclusive | +| `category` | cp / mic / game / gift / lucky_gift / recharge | +| `metric_type` | 具体指标 | +| `title` | 展示名称 | +| `description` | 展示描述 | +| `target_value` | 目标值 | +| `target_unit` | count / minute / coin | +| `reward_coin_amount` | 奖励金币 | +| `status` | draft / active / paused / archived | +| `sort_order` | 排序 | +| `version` | 当前版本 | +| `effective_from_ms` | 生效时间 | +| `effective_to_ms` | 结束时间 | +| `created_by_admin_id` | 创建人 | +| `updated_by_admin_id` | 更新人 | +| `created_at_ms` | 创建时间 | +| `updated_at_ms` | 更新时间 | + +### `task_definition_versions` + +保存历史版本,保证事件和进度可以追溯。 + +| 字段 | 说明 | +| --- | --- | +| `task_version_id` | 主键 | +| `task_id` | 任务 ID | +| `version` | 版本号 | +| `snapshot_json` | 任务配置快照 | +| `effective_from_cycle` | 从哪个 daily cycle 或 lifetime 生效 | +| `created_at_ms` | 创建时间 | + +### `user_task_progress` + +用户任务进度。 + +| 字段 | 说明 | +| --- | --- | +| `app_code` | App 租户 | +| `user_id` | 用户 ID | +| `task_id` | 任务 ID | +| `task_version_id` | 任务版本 | +| `cycle_key` | daily 使用 `yyyy-mm-dd`,exclusive 使用 `lifetime` | +| `progress_value` | 当前进度,可超过目标值 | +| `target_value` | 进度创建时的目标快照 | +| `status` | in_progress / completed / claimed / expired | +| `completed_at_ms` | 完成时间 | +| `claimed_at_ms` | 领取时间 | +| `updated_at_ms` | 更新时间 | + +唯一键: + +```text +UNIQUE(app_code, user_id, task_id, cycle_key) +``` + +### `task_event_consumption` + +事件幂等表。 + +| 字段 | 说明 | +| --- | --- | +| `app_code` | App 租户 | +| `event_id` | 来源事件 ID | +| `event_type` | 来源事件类型 | +| `source_service` | room / wallet / user | +| `user_id` | 事件归属用户 | +| `task_day` | 服务器时区日期 | +| `status` | consumed / skipped / failed | +| `skip_reason` | 跳过原因 | +| `created_at_ms` | 创建时间 | +| `consumed_at_ms` | 消费时间 | + +主键: + +```text +PRIMARY KEY(app_code, event_id) +``` + +### `task_reward_claims` + +领奖幂等和钱包发奖状态。 + +| 字段 | 说明 | +| --- | --- | +| `claim_id` | 主键 | +| `app_code` | App 租户 | +| `command_id` | 领奖幂等键 | +| `user_id` | 用户 ID | +| `task_id` | 任务 ID | +| `cycle_key` | daily 日期或 lifetime | +| `reward_coin_amount` | 奖励金币快照 | +| `wallet_command_id` | 钱包发奖幂等键 | +| `status` | pending / granted / failed | +| `failure_reason` | 失败原因 | +| `created_at_ms` | 创建时间 | +| `updated_at_ms` | 更新时间 | + +唯一键: + +```text +UNIQUE(app_code, command_id) +UNIQUE(app_code, user_id, task_id, cycle_key) +``` + +## 事件消费流程 + +```mermaid +sequenceDiagram + participant Source as Source outbox + participant Task as activity task consumer + participant DB as activity MySQL + + Source->>Task: event(event_id, user_id, metric, value, occurred_at_ms) + Task->>DB: INSERT task_event_consumption(event_id) + alt event_id 已存在 + DB-->>Task: duplicate + Task-->>Source: ack + else 新事件 + Task->>DB: 查 active task definitions + Task->>DB: upsert user_task_progress + Task->>DB: progress >= target 时标记 completed + Task->>DB: task_event_consumption = consumed + Task-->>Source: ack + end +``` + +进度更新必须在一个数据库事务内完成: + +1. 写入 `task_event_consumption`。 +2. 找到事件命中的 active 任务。 +3. 按 `user_id + task_id + cycle_key` 锁定或创建进度行。 +4. 增加进度。 +5. 如果达到目标,标记 `completed`。 +6. 标记事件已消费。 + +如果事件没有匹配任何任务,写 `skipped`,避免重复消费同一个无效事件。 + +## 领奖流程 + +```mermaid +sequenceDiagram + participant App + participant Gateway + participant Activity as activity-service + participant DB as activity MySQL + participant Wallet as wallet-service + + App->>Gateway: POST /tasks/claim + Gateway->>Activity: ClaimTaskReward + Activity->>DB: 锁 user_task_progress + DB-->>Activity: completed and not claimed + Activity->>DB: 创建 task_reward_claims(pending) + Activity->>Wallet: CreditTaskReward(wallet_command_id) + Wallet-->>Activity: granted + Activity->>DB: progress=claimed, claim=granted + Activity-->>Gateway: reward granted + Gateway-->>App: OK +``` + +钱包调用失败时: + +- `task_reward_claims` 保持 `pending` 或 `failed`。 +- `user_task_progress` 不应标记 `claimed`。 +- 用户重试同一 `command_id` 返回当前状态。 +- 后台可以扫描 `pending/failed` claim 重试。 + +## 边界和补充规则 + +### 1. 服务器时区 + +必须定义全局任务时区,例如: + +```yaml +task_timezone: "Asia/Shanghai" +``` + +所有 daily 周期都按这个时区计算: + +- `task_day` +- `cycle_start_ms` +- `cycle_end_ms` +- `next_refresh_at_ms` + +不要使用用户手机时区,不要使用请求 IP 所在时区。 + +### 2. 任务定义修改 + +active 任务修改目标值或奖励时,必须生成新版本。 + +建议规则: + +- 文案、排序可以立即生效。 +- 目标值、奖励金币、指标类型从下一个 daily cycle 生效。 +- 专属任务修改后只影响未开始用户,已完成或已领取用户不重算。 + +### 3. 延迟事件 + +事件按 `occurred_at_ms` 归属日期。 + +建议首版: + +- 当前 task_day 之前的迟到事件可以记录到历史进度。 +- App 只展示当前 daily 任务。 +- daily 奖励只允许在当前 task_day 领取。 +- 是否允许“昨日补领”不要默认支持,后续作为后台补偿能力单独设计。 + +### 4. 超额完成 + +`progress_value` 可以超过 `target_value`,便于展示真实进度,例如 `120/100`。 + +但: + +- daily 超额不结转。 +- reward 仍只发一次。 +- exclusive 完成后不再重复累计领取。 + +### 5. 任务可见性 + +首版所有 active 任务对当前 App 全体用户可见。 + +后续可扩展: + +- 地区可见。 +- 新用户可见。 +- VIP 可见。 +- 指定用户可见。 +- 主播/Agency/BD 身份可见。 + +这些应通过 `task_audience_rules` 扩展,不要把复杂人群条件塞进基础任务表。 + +### 6. CP 任务解除关系 + +CP 任务以成功组成 CP 的事件为准。后续解除 CP 不回滚已完成进度,避免用户完成后被动丢奖励。 + +如果运营需要“当前仍保持 CP”,应该设计状态型任务,不要复用事件计数任务。 + +### 7. 上麦任务跨天 + +跨天麦位 session 必须拆分到不同 `task_day`。 + +例如服务器时区 23:50 上麦,00:10 下麦: + +```text +前一天 +10 分钟 +后一天 +10 分钟 +``` + +### 8. 充值退款和拒付 + +充值任务按成功充值事件完成。Google/Apple 拒付、退款、风控追回不直接回滚已领任务奖励。 + +如果后续需要严格财务反作弊,应新增: + +- 充值奖励冻结期。 +- 领奖后拒付追扣。 +- 风控黑名单。 + +首版不建议把这些复杂规则放进每日任务主链路。 + +### 9. 金币奖励发放 + +activity-service 不能直接写钱包余额。 + +钱包发奖命令必须包含: + +| 字段 | 说明 | +| --- | --- | +| `wallet_command_id` | 幂等键 | +| `user_id` | 领取用户 | +| `asset_type` | COIN | +| `amount` | 奖励金币 | +| `reason` | `daily_task_reward` / `exclusive_task_reward` | +| `biz_id` | `task_id + cycle_key` | + +### 10. 幂等 + +必须有三层幂等: + +- 来源事件:`task_event_consumption(event_id)`。 +- 用户进度:`user_task_progress(user_id, task_id, cycle_key)`。 +- 领奖:`task_reward_claims(command_id)` 和 `task_reward_claims(user_id, task_id, cycle_key)`。 + +### 11. 删除任务 + +后台不做物理删除。 + +状态流转: + +```text +draft -> active -> paused -> archived +``` + +`archived` 后 App 不展示,但历史进度、领奖记录仍可审计。 + +### 12. 任务排序 + +建议排序: + +1. 可领取任务。 +2. 已完成未领取任务。 +3. 进行中任务。 +4. 已领取任务。 +5. 同状态按 `sort_order`。 + +### 13. 任务进度查询性能 + +App 查询任务列表时不要实时扫钱包流水或房间事件。 + +查询路径只读: + +```text +task_definitions + user_task_progress +``` + +没有进度行时返回 0,不必提前为所有用户初始化每日任务。 + +### 14. 多 App 隔离 + +所有任务定义、进度、事件消费、领奖记录都必须带 `app_code`。 + +任务 ID 不能跨 App 复用语义。 + +### 15. 管理端审计 + +后台每次创建、启停、修改任务都要写审计: + +- operator_admin_id +- request_id +- before_json +- after_json +- created_at_ms + +任务配置影响发奖,必须可追溯。 + +## 推荐实现顺序 + +1. Admin 菜单和权限:`活动管理` 一级菜单下增加 `每日任务` 二级菜单。 +2. activity-service 增加任务定义表、版本表、进度表、事件消费表、领奖表。 +3. Admin API:每日任务/专属任务列表、新增、编辑、启停、排序。 +4. App 查询接口:返回 daily/exclusive 两个分区和用户进度。 +5. 钱包金币奖励接口:`CreditTaskReward` 幂等入账。 +6. 领奖接口:完成校验、claim 幂等、调用钱包发奖。 +7. 接入充值、送礼、游戏、幸运礼物事件。 +8. 接入上麦时长事件或 user-service 麦时长 read model。 +9. 接入 CP 创建事件。 +10. 增加补偿任务:扫描 pending claim、failed event、消费延迟。 + +## 需要提前确认的问题 + +1. 服务器任务时区使用哪个 IANA 时区,是否所有 App 共用。 +2. 幸运礼物任务首版是“消耗金币”或“中奖金币”二选一,还是需要 AND 条件。 +3. 充值任务是否包含币商转金币、后台补偿、活动赠币;建议首版只包含真实充值和币商转普通金币。 +4. daily 任务是否允许次日补领;建议首版不允许。 +5. 专属任务是否全体用户可见,还是需要后台指定用户或人群。 +6. 游戏消耗金币的事件来源和业务类型枚举需要先统一,否则任务无法区分普通扣费和游戏扣费。 diff --git a/docs/login-region-risk-architecture.md b/docs/login-region-risk-architecture.md new file mode 100644 index 00000000..ebbc8b97 --- /dev/null +++ b/docs/login-region-risk-architecture.md @@ -0,0 +1,583 @@ +# Login Region Risk Architecture + +本文档定义登录地区、语言和时区限制的当前推荐方案。方案按当前产品取舍设计:不引入 `pending_risk` 会话,不让各玩法服务分别判断风险状态;用户在未知 IP 首次登录时可以进入 App,后台异步 IP 风控命中不支持国家后撤销 session 并踢下线,后续同一 IP 再登录会在入口直接拒绝。 + +## Scope + +| Capability | Owner | Rule | +| --- | --- | --- | +| 真实入口 IP 提取 | `gateway-service` | 从可信上游 header 或 `RemoteAddr` 提取,不信任前端 JSON 里的 IP | +| 语言和时区快速判断 | `gateway-service` | 只做入口级弱信号拦截,命中明显不支持配置时直接拒绝登录 | +| IP 风控缓存读取 | `gateway-service` | 登录前读取 Redis IP 决策缓存,`blocked` 直接拒绝,`allowed/unknown/miss` 放行并触发复核 | +| 登录、session 和 token | `user-service` | 仍是 session owner;登录成功后创建 `auth_sessions` 并签发 token | +| 异步 IP 风控任务 | `cron-service` 调度,`user-service` 执行 | 按优先级顺序查询 IP Geo 源;当前一个不可用时才降级到下一个,写持久审计和 Redis 缓存 | +| session 撤销 | `user-service` | 命中不支持国家后撤销 session,写撤销原因和审计 | +| 登录态接口统一拦截 | `gateway-service` | JWT 校验后统一检查 session 是否已撤销,避免玩法服务散落判断 | +| 房间/IM 踢下线 | `gateway-service` 编排,`room-service`/腾讯云 IM 执行 | 只能走 Room Cell 命令或腾讯云 IM 服务端接口,不能直接改 room Redis | +| 后台登录日志 | `hyapp-admin-server` | 只在用户管理一级菜单下新增二级菜单“登录日志”,不单独做风控策略后台 | + +## Non-Goals + +| Non-goal | Reason | +| --- | --- | +| 不做 `pending_risk` session | 当前阶段要降低接入成本,避免设计白名单接口和受限权限集 | +| 不在玩法服务判断地区风险 | 房间、礼物、钱包、VIP、任务都不应该各自写 `risk_status` 判断,容易漏 | +| 不把 IP Geo 查询接口放进同步登录路径 | 外部接口慢、超时或不稳定会拖慢登录 | +| 不信任前端传过来的 IP | 客户端字段可伪造,只能作为调试或兼容字段记录 | +| 不靠“清客户端 token”完成踢下线 | JWT 已签发后客户端仍可能继续使用,必须服务端撤销 session 并由 gateway 拦截 | +| 不把 Redis 当唯一事实来源 | Redis 只做快查缓存;审计、任务、明文 IP 和撤销原因必须能在 MySQL 追溯 | +| 不做独立风控后台 | 当前后台只提供登录日志查看,不提供策略编辑、provider 管理或人工改判 | + +## Core Decision + +采用“入口快拦 + 登录后异步复核 + session 统一撤销”的模型: + +```mermaid +flowchart LR + C["Client"] --> G["gateway-service"] + G --> LangTZ["language/timezone fast guard"] + G --> Cache["Redis IP decision cache"] + Cache -->|blocked| Deny["reject login"] + Cache -->|allowed/miss/unknown| U["user-service Auth"] + U --> Session[("auth_sessions")] + U --> Resp["token response"] + U --> Job[("login_ip_risk_jobs / outbox")] + Job --> RiskBatch["cron triggered IP risk batch"] + RiskBatch --> IP1["IP Geo A"] + IP1 -. "unavailable/timeout" .-> IP2["IP Geo B"] + IP2 -. "unavailable/timeout" .-> IP3["IP Geo C"] + IP3 -. "unavailable/timeout" .-> IP4["IP Geo D"] + RiskBatch --> Decision[("risk decision + Redis cache")] + RiskBatch -->|blocked| Revoke["revoke session"] + Revoke --> Kick["IM/Room kick"] +``` + +这个方案接受一个明确边界:未知 IP 第一次可能短暂进入 App。通过 Redis 缓存和 session 撤销,同一个 IP 后续不能反复完整登录。 + +## Login Flow + +### Password Login + +```mermaid +sequenceDiagram + participant App + participant Gateway + participant User as user-service + participant Redis + participant Risk as cron-service/user-service batch + + App->>Gateway: POST /api/v1/auth/account/login + Gateway->>Gateway: extract trusted client_ip + Gateway->>Gateway: check language/timezone config + Gateway->>Redis: GET ip decision + alt ip blocked + Gateway-->>App: AUTH_LOGIN_BLOCKED + else allowed/miss/unknown + Gateway->>User: LoginPassword(meta.client_ip, language, timezone) + User->>User: verify password and create session + User->>User: enqueue login_ip_risk_job + User-->>Gateway: token(session_id) + Gateway-->>App: token + Risk->>Risk: async resolve IP by provider fallback chain + alt unsupported country + Risk->>User: RevokeSession(session_id, reason) + Risk->>Redis: SET ip decision = blocked + Risk->>Gateway: trigger kick orchestration + else allowed country + Risk->>Redis: SET ip decision = allowed + end + end +``` + +### Third Party Login + +三方登录和密码登录使用同一套入口风控,只是 `user-service` 在创建或复用用户 session 后写风险任务。三方首次注册时,客户端提交的 `country` 仍然只是用户选择国家;IP 国家由服务端 IP Geo 结果决定,不能用 `country` 替代风控国家。 + +## Gateway Fast Guard + +gateway 只做三件事: + +1. 提取可信入口 IP。 +2. 对 `language` 和 `timezone` 做简单配置判断。 +3. 查询 Redis IP 决策缓存,命中 `blocked` 时拒绝登录。 + +### Trusted IP + +可信 IP 来源顺序: + +```text +trusted edge header -> X-Forwarded-For first public IP -> RemoteAddr +``` + +前端提交的 `ip` 字段不参与判断。旧客户端如果还传 IP,只能写入审计扩展字段,不能作为风控依据。 + +### Language And Timezone + +语言和时区是弱信号,因为客户端可以修改。它们只能做入口快速拦截: + +```yaml +login_risk: + blocked_languages: + - zh-CN + blocked_language_primary_tags: + - zh + blocked_timezones: + - Asia/Shanghai + blocked_timezone_prefixes: + - Asia/ +``` + +规则: + +- `language` 按 BCP 47 轻量格式校验,例如 `en-US`、`zh-CN`。 +- `timezone` 必须是 IANA 名称,例如 `America/Los_Angeles`。 +- 命中阻断配置时,gateway 返回 `AUTH_LOGIN_BLOCKED`。 +- 未传、格式异常或不可信时,不直接推断国家;只记录审计。 + +### IP Decision Cache + +Redis key 建议不要保存明文 IP: + +```text +login_risk:ip:{app_code}:{sha256(ip)} +``` + +Value: + +```json +{ + "decision": "blocked", + "country": "CN", + "source": "geo_provider_chain", + "checked_at_ms": 1710000000000, + "expires_at_ms": 1710604800000 +} +``` + +决策值: + +| Decision | Gateway Action | +| --- | --- | +| `blocked` | 登录前直接拒绝 | +| `allowed` | 放行登录,可低频复核 | +| `unknown` | 放行登录,必须异步复核 | +| miss | 放行登录,必须异步复核 | + +TTL 建议: + +| Decision | TTL | +| --- | --- | +| `blocked` | 7 到 30 天 | +| `allowed` | 1 到 7 天 | +| `unknown` | 10 到 60 分钟 | + +可用性策略:Redis 不可用时放行登录,只记录错误和告警;不能因为 Redis 故障阻断登录。 + +## Async IP Risk Check + +异步 IP 风控 batch 按优先级顺序调用 IP Geo 源,不阻塞登录响应。一次任务只使用一个可用 provider 的结果;只有当前 provider 超时、不可用或返回不可解析结果时,才请求下一个 provider。 + +### Job + +风险任务需要包含: + +| Field | Rule | +| --- | --- | +| `job_id` | 幂等任务 ID | +| `app_code` | 多 App 隔离 | +| `session_id` | 命中后撤销的 session | +| `user_id` | 审计和踢下线目标 | +| `client_ip` | 明文入口 IP,后台登录日志直接展示 | +| `client_ip_hash` | 缓存 key、索引和日志关联字段,避免普通日志输出明文 IP | +| `channel` | 登录渠道:`account/google/facebook/twitter/tiktok` | +| `platform` | 客户端平台:`android/ios` | +| `language` | 登录时客户端语言 | +| `timezone` | 登录时客户端时区 | +| `request_id` | 串联登录日志 | +| `status` | `pending/running/completed/failed/skipped` | +| `created_at_ms` | 任务创建时间 | +| `updated_at_ms` | 任务更新时间 | + +任务可以落 MySQL 表 `login_ip_risk_jobs`,也可以由 user outbox 派生。首版如果没有统一 MQ,优先用 MySQL 任务表 + cron-service 调度的 batch claim,避免 Redis list 丢失导致永远不复核。MySQL 写任务失败时不影响登录结果,但必须记录错误和告警。 + +### Provider Fallback Chain + +batch 行为: + +1. 先查 Redis 是否已有新鲜决策;已有 `blocked/allowed` 可以跳过外部查询。 +2. miss 时按配置顺序调用第一个 provider。 +3. 如果 provider 在 timeout 内返回有效国家码,立即停止查询后续 provider。 +4. 如果 provider 超时、不可用、限流、返回空国家或返回不可解析结果,记录失败原因并降级到下一个 provider。 +5. 所有 provider 都失败时,输出 `unknown`,写短 TTL 缓存并等待后续任务重试;如果缓存也不可用,只记录日志和告警。 +6. 有效国家码命中不支持国家时输出 `blocked`;有效国家码未命中不支持国家时输出 `allowed`。 +7. 写 MySQL 决策记录和 Redis 缓存。 +8. 如果命中不支持国家,撤销当前 session 并触发踢下线。 + +该模型没有“结果冲突”问题,因为同一个任务不会同时采信多个 provider。provider 顺序就是信任优先级: + +| Case | Decision | +| --- | --- | +| 第一个可用 provider 返回不支持国家 | `blocked` | +| 第一个可用 provider 返回支持国家 | `allowed` | +| provider 超时、不可用或返回无效结果 | 请求下一个 provider | +| 所有 provider 都失败 | `unknown`,短 TTL,后续重试 | + +不支持国家列表必须来自配置,不应硬编码在风控逻辑里;当前后台不提供策略编辑能力。 + +## Session Revocation + +命中不支持国家时,撤销 session 是核心动作。 + +```text +auth_sessions.revoked_at_ms = now +auth_sessions.revoked_reason = GEO_POLICY_BLOCKED +``` + +当前表只有 `revoked_at_ms`,实现时建议补充: + +| Field | Reason | +| --- | --- | +| `revoked_reason` | 区分用户 logout、refresh 轮换、后台封禁、IP 风控撤销 | +| `revoked_request_id` | 串联风险任务和审计 | +| `revoked_by` | `risk_worker`、`user_logout`、`admin` | + +撤销成功后写 Redis denylist: + +```text +auth:revoked_session:{app_code}:{session_id} = GEO_POLICY_BLOCKED +TTL = access token remaining ttl + safety window +``` + +## Gateway Session Guard + +如果 access token 是 JWT,只校验签名不够。用户已经拿到 access token 后,即使 `auth_sessions` 被撤销,JWT 在过期前仍然能访问接口。 + +因此 gateway 必须增加统一 session guard: + +```text +Authorization JWT valid ++ sid exists ++ auth:revoked_session:{app_code}:{sid} not exists +``` + +首版可以先只查 Redis denylist,不对每个请求同步查 MySQL: + +- 登录成功时不需要写 active cache。 +- 风控撤销 session 时写 denylist。 +- logout、refresh 轮换、后台封禁同样写 denylist。 +- access token TTL 到期后 denylist 自然失效。 + +后续如果需要更强一致性,再增加 `ValidateSession` RPC 或 `auth:active_session` cache。 + +所有 App 登录态接口都走 gateway middleware,因此玩法服务不需要感知风险状态: + +```mermaid +flowchart TD + Req["Business Request"] --> JWT["Verify JWT"] + JWT --> SID["Read sid claim"] + SID --> Deny["Check Redis revoked_session"] + Deny -->|revoked| Fail["SESSION_REVOKED"] + Deny -->|active| Handler["Business handler"] +``` + +## Kick Offline + +踢下线分三层,按能做到的程度逐步接入: + +| Layer | Action | Required | +| --- | --- | --- | +| API | gateway session guard 返回 `SESSION_REVOKED` | 必须 | +| IM | 调腾讯云 IM 服务端接口踢下线或发系统消息通知客户端清 token | 建议 | +| Room | 如果用户在房间,通过 room-service Room Cell 命令踢出或触发离房 | 建议 | + +不能直接删除 room-service Redis presence 或麦位缓存。房间状态 owner 是 Room Cell,风险踢人必须走 room-service 的命令入口;如果首版没有踢人命令,至少依靠 session guard 阻止后续 HTTP 操作,并让房间 heartbeat/断线补偿释放状态。 + +## Error Codes + +建议新增稳定业务码: + +| Code | HTTP | gRPC | Meaning | +| --- | --- | --- | --- | +| `AUTH_LOGIN_BLOCKED` | 403 | `PermissionDenied` | 登录入口命中地区、语言、时区或 IP 风控策略 | +| `SESSION_REVOKED` | 401 | `Unauthenticated` | session 已撤销,客户端必须清登录态 | + +客户端行为: + +- `AUTH_LOGIN_BLOCKED`:停留登录页,展示地区不支持类提示。 +- `SESSION_REVOKED`:清本地 token,退出房间/IM,回登录页。 +- 普通 5xx 或网络失败不能清 token。 + +## Audit And Observability + +必须记录: + +| Event | Fields | +| --- | --- | +| `login_fast_guard_blocked` | `request_id/app_code/client_ip_hash/language/timezone/block_reason` | +| `login_ip_risk_job_created` | `request_id/app_code/session_id/user_id/client_ip/client_ip_hash/channel/platform` | +| `login_ip_geo_provider_attempt` | `job_id/provider/order/country/status/duration_ms/error_code` | +| `login_ip_risk_decision` | `job_id/session_id/user_id/decision/country/confidence` | +| `login_session_revoked_by_risk` | `job_id/session_id/user_id/reason/country` | +| `kick_offline_result` | `session_id/user_id/im_result/room_result` | + +敏感规则: + +- 不记录明文 access token、refresh token、三方 credential。 +- 明文 IP 可以落 MySQL 业务表,供后台登录日志查看;普通服务日志仍只记录 `client_ip_hash`,避免日志系统大范围暴露明文 IP。 +- provider 返回原始报文不进日志,只记录本次尝试的顺序、标准化国家码、耗时和错误码。 + +## Data Model + +### `login_ip_risk_jobs` + +```sql +CREATE TABLE login_ip_risk_jobs ( + app_code VARCHAR(32) NOT NULL, + job_id VARCHAR(96) NOT NULL, + session_id VARCHAR(96) NOT NULL, + user_id BIGINT NOT NULL, + client_ip VARCHAR(64) NOT NULL, + client_ip_hash VARCHAR(128) NOT NULL, + channel VARCHAR(32) NOT NULL, + platform VARCHAR(16) NOT NULL, + language VARCHAR(32) NOT NULL DEFAULT '', + timezone VARCHAR(64) NOT NULL DEFAULT '', + request_id VARCHAR(96) NOT NULL DEFAULT '', + status VARCHAR(32) NOT NULL, + decision VARCHAR(32) NOT NULL DEFAULT '', + country_code VARCHAR(3) NOT NULL DEFAULT '', + failure_reason VARCHAR(128) NOT NULL DEFAULT '', + locked_by VARCHAR(128) NULL, + locked_until_ms BIGINT NULL, + attempt_count INT NOT NULL DEFAULT 0, + created_at_ms BIGINT NOT NULL, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, job_id), + KEY idx_login_ip_risk_jobs_status (app_code, status, updated_at_ms), + KEY idx_login_ip_risk_jobs_session (app_code, session_id), + KEY idx_login_ip_risk_jobs_user (app_code, user_id, created_at_ms), + KEY idx_login_ip_risk_jobs_ip (app_code, client_ip_hash, updated_at_ms) +); +``` + +### `login_ip_risk_decisions` + +```sql +CREATE TABLE login_ip_risk_decisions ( + app_code VARCHAR(32) NOT NULL, + decision_id VARCHAR(96) NOT NULL, + client_ip VARCHAR(64) NOT NULL, + client_ip_hash VARCHAR(128) NOT NULL, + decision VARCHAR(32) NOT NULL, + country_code VARCHAR(3) NOT NULL DEFAULT '', + confidence INT NOT NULL DEFAULT 0, + provider_attempts_json JSON NOT NULL, + decided_at_ms BIGINT NOT NULL, + expires_at_ms BIGINT NOT NULL, + created_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, decision_id), + KEY idx_login_ip_risk_decisions_ip (app_code, client_ip_hash, decided_at_ms), + KEY idx_login_ip_risk_decisions_country (app_code, country_code, decided_at_ms), + KEY idx_login_ip_risk_decisions_expires (app_code, expires_at_ms) +); +``` + +### `login_audit` Additions + +现有 `login_audit` 已记录登录类型、provider、结果和失败原因。为了支撑后台登录日志,需要把登录入口展示字段补齐: + +```sql +ALTER TABLE login_audit + ADD COLUMN channel VARCHAR(32) NOT NULL DEFAULT '', + ADD COLUMN platform VARCHAR(16) NOT NULL DEFAULT '', + ADD COLUMN client_ip VARCHAR(64) NOT NULL DEFAULT '', + ADD COLUMN ip_country_code VARCHAR(3) NOT NULL DEFAULT '', + ADD COLUMN blocked TINYINT(1) NOT NULL DEFAULT 0, + ADD COLUMN block_reason VARCHAR(64) NOT NULL DEFAULT '', + ADD KEY idx_login_audit_user_created (app_code, user_id, created_at_ms), + ADD KEY idx_login_audit_blocked (app_code, blocked, created_at_ms), + ADD KEY idx_login_audit_ip_country (app_code, ip_country_code, created_at_ms); +``` + +字段口径: + +| Field | Rule | +| --- | --- | +| `channel` | `account/google/facebook/twitter/tiktok`,三方 provider 必须归一成这几个展示值 | +| `platform` | `android/ios`,来自客户端请求体或可信 header | +| `client_ip` | 明文入口 IP,来自 gateway 提取结果 | +| `ip_country_code` | IP Geo 解析出的国家码,未解析时为空 | +| `blocked` | 当前登录是否被语言、时区、IP cache 或异步 IP 风控阻断 | +| `block_reason` | `language/timezone/ip_cache/ip_async` 等稳定原因 | + +### `auth_sessions` Additions + +```sql +ALTER TABLE auth_sessions + ADD COLUMN revoked_reason VARCHAR(64) NOT NULL DEFAULT '', + ADD COLUMN revoked_request_id VARCHAR(96) NOT NULL DEFAULT '', + ADD COLUMN revoked_by VARCHAR(64) NOT NULL DEFAULT ''; +``` + +## Admin Login Logs + +后台管理当前只做查询和展示,不做风控配置编辑。入口位置: + +```text +用户管理 + └── 登录日志 +``` + +### List Page + +列表按登录事件展示,默认按 `created_at_ms DESC` 分页。 + +展示字段: + +| Column | Source | +| --- | --- | +| 用户 ID | `login_audit.user_id` | +| 短号 | join `users.current_display_user_id` | +| 昵称 | join `users.username` | +| 国家/区域 | join `users.country/region_id`,只作用户资料快照展示 | +| 登录 IP | `login_audit.client_ip` | +| IP 所在国家 | `login_audit.ip_country_code` | +| 渠道 | `login_audit.channel`,值为 `account/google/facebook/twitter/tiktok` | +| 平台 | `login_audit.platform`,值为 `android/ios` | +| 结果 | `success/failed/blocked/revoked` | +| 屏蔽原因 | `login_audit.block_reason` | +| 登录时间 | `login_audit.created_at_ms` | + +交互规则: + +- `blocked=true` 或 `failure_code=AUTH_LOGIN_BLOCKED` 的表格行背景显示红色。 +- 如果异步 IP 风控在登录成功后撤销 session,该次登录日志也要更新为屏蔽态,行背景显示红色。 +- 点击任意用户行,进入该用户历史登录日志侧栏或详情页。 +- 列表只读,不提供解除屏蔽、修改策略、重试 provider 或手动踢下线按钮。 + +筛选条件: + +| Filter | Rule | +| --- | --- | +| 用户 ID / 短号 | 精确搜索,短号需要先解析成 `user_id` | +| IP | 精确搜索明文 IP | +| IP 国家 | 按 `ip_country_code` | +| 渠道 | `account/google/facebook/twitter/tiktok` | +| 平台 | `android/ios` | +| 是否屏蔽 | `all/blocked/not_blocked` | +| 时间范围 | 按 `created_at_ms` | + +### User History + +点击用户行后展示该用户历史登录日志: + +| Field | Rule | +| --- | --- | +| 用户基础信息 | user_id、短号、昵称、头像、当前国家、当前区域 | +| 最近登录列表 | 按时间倒序展示该用户所有登录事件 | +| 风控结果 | 展示 IP 国家、是否屏蔽、屏蔽原因、撤销 session 时间 | +| 渠道和平台 | 展示每次登录的 channel/platform | + +用户历史页只读。若后续需要手动封禁、解封或策略配置,必须走独立后台能力和审计,不混在登录日志里。 + +## Configuration + +首版配置放 gateway、user-service 和 cron-service 的 YAML。当前后台只做登录日志查看,不管理不支持国家、provider 顺序或语言/时区策略: + +```yaml +login_risk: + enabled: true + fast_guard: + blocked_languages: [] + blocked_language_primary_tags: [] + blocked_timezones: [] + blocked_timezone_prefixes: [] + ip: + unsupported_countries: + - CN + blocked_ttl_sec: 2592000 + allowed_ttl_sec: 604800 + unknown_ttl_sec: 1800 + provider_timeout_ms: 800 + providers: + - primary_geo + - fallback_geo_a + - fallback_geo_b + - fallback_geo_c + +# services/cron-service/configs/config*.yaml +tasks: + login_ip_risk: + enabled: true + interval: 5s + timeout: 3s + lock_ttl: 30s + batch_size: 100 +``` + +配置原则: + +- gateway 快速判断和 user-service IP 风控策略的不支持国家列表必须同源,避免 gateway 放行但后台风控用另一套规则。 +- provider 顺序必须显式配置,顺序代表信任优先级;不要在代码里随机切换。 +- 线上配置不要写 provider secret;provider key 走密钥系统或环境变量。 +- 多 App 时按 `app_code` 覆盖策略。 + +## Failure Policy + +| Failure | Policy | +| --- | --- | +| Redis 不可用 | 放行登录;记录错误和告警,不读写 IP decision cache;session denylist 失效属于踢下线实时性下降,不能阻断登录 | +| MySQL 写登录日志或风险任务失败 | 放行登录;记录 error 和告警,后续依赖下次登录或缓存命中补偿 | +| 所有 IP provider 都失败 | 放行登录;能写缓存则写 `unknown` 短 TTL,不能写缓存也不阻塞 | +| 前置 provider 不可用 | 自动降级到下一个 provider;如果全部不可用则放行 | +| session 已经 logout | 后台风控撤销幂等成功,不重复踢 | +| 用户已换新 session | 只撤销命中的 `session_id`;如果 IP 已 blocked,后续登录会被 gateway 拒绝 | +| 踢 IM 失败 | session 已撤销仍然生效;记录补偿任务或等待客户端下一次 API 返回 `SESSION_REVOKED` | +| 房间踢出失败 | 不直接改 Redis;记录补偿,依赖 Room Cell/heartbeat 兜底 | + +## Implementation Sequence + +1. 增加 `AUTH_LOGIN_BLOCKED` 错误码,并确认 gateway 对 `SESSION_REVOKED` 的客户端语义。 +2. gateway 登录入口增加 language/timezone fast guard 和 IP decision cache 读取。 +3. 登录请求把 `client_ip/language/timezone` 透传到 user-service;密码登录也需要补这两个字段。 +4. user-service 登录成功后创建 `login_ip_risk_jobs`,任务至少包含 `session_id/user_id/client_ip/client_ip_hash/channel/platform/request_id`。 +5. 扩展 `login_audit`,保存明文 IP、IP 国家、渠道、平台、是否屏蔽和屏蔽原因。 +6. 实现 `UserCronService.ProcessLoginIPRiskBatch`:MySQL claim 任务,按优先级顺序调用 provider fallback chain,写决策和 Redis cache。 +7. user-service 增加按 `session_id` 撤销 session 的内部能力,并写撤销原因。 +8. gateway 增加统一 session denylist middleware,JWT 校验后查 `auth:revoked_session:{app_code}:{sid}`。 +9. 风控撤销 session 时写 Redis denylist,并把对应登录审计更新为屏蔽态。 +10. 接入腾讯云 IM 踢下线或系统消息通知。 +11. 增加 room-service 踢人命令或复用现有 Room Cell 管理命令,不直接操作 room Redis。 +12. admin 在用户管理下新增二级菜单“登录日志”,支持列表、筛选、红色屏蔽行和用户历史登录日志。 +13. 增加日志、指标和告警:provider 超时率、unknown 比例、blocked 数、撤销数、踢下线失败数。 + +## Test Cases + +| Case | Expected | +| --- | --- | +| language 命中阻断 | gateway 直接返回 `AUTH_LOGIN_BLOCKED`,不调用 user-service 登录 | +| timezone 命中阻断 | gateway 直接返回 `AUTH_LOGIN_BLOCKED` | +| IP cache blocked | gateway 直接返回 `AUTH_LOGIN_BLOCKED` | +| IP cache miss | 登录成功,创建风险任务 | +| 后台风控判定 allowed | 写 allowed cache,不撤销 session | +| 后台风控判定 blocked | 撤销 session,写 blocked cache,写 denylist,触发踢下线 | +| blocked 后同 IP 再登录 | gateway 登录前拒绝 | +| 已撤销 session 调业务接口 | gateway middleware 返回 `SESSION_REVOKED` | +| provider 全失败 | 放行,能写缓存则写 unknown cache,不踢下线 | +| 第一个 provider 不可用、第二个 provider 返回 blocked country | 写 blocked cache,撤销 session | +| 登录日志列表含 blocked 行 | 该行背景为红色 | +| 点击用户登录日志行 | 展示该用户历史登录日志 | +| refresh 使用已撤销 session | user-service 返回 `SESSION_REVOKED` | + +## Open Decisions + +| Decision | Recommendation | +| --- | --- | +| Redis/MySQL/IP 查询不可用时是否阻断登录 | 当前方案统一 fail-open:全部放行,只记录错误和告警 | +| 第一个可用 provider 命中不支持国家是否踢 | 当前方案直接踢;provider 顺序代表信任优先级 | +| 明文 IP 是否落库 | 明文 IP 落 MySQL,普通服务日志仍只打 hash | +| 是否做后台管理页面 | 只做用户管理下的“登录日志”二级菜单,不做策略编辑后台 | + +## Final Rule + +当前阶段最重要的工程约束是:地区风险只在 gateway 入口和 user-service session 层处理,玩法服务不感知风险状态。只要 gateway 统一检查 session denylist,后台异步命中后撤销 session 就能对房间、钱包、礼物、VIP、消息等所有 HTTP 业务统一生效。 diff --git a/docs/user-mic-online-time-architecture.md b/docs/user-mic-online-time-architecture.md index 4ce5979f..e89a75b1 100644 --- a/docs/user-mic-online-time-architecture.md +++ b/docs/user-mic-online-time-architecture.md @@ -270,25 +270,25 @@ user_mic_event_consumption( ## Open Session Compensation -user-service 已内置 open session compensation worker,用于兜底关闭 room-service down 事件缺失导致的异常 open session。补偿只修正用户麦时长事实,不反写 Room Cell,也不向客户端发送房间控制消息。 +open session compensation 由 `cron-service` 调度,`user-service` 只暴露单批 `CompensateMicOpenSessions` 处理能力,用于兜底关闭 room-service down 事件缺失导致的异常 open session。补偿只修正用户麦时长事实,不反写 Room Cell,也不向客户端发送房间控制消息。 | Case | Action | | --- | --- | -| `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 兜底 | +| `pending_publish` 超过 `pending_publish_max_age` | 关闭 session,`end_reason=compensated_publish_timeout`,`mic_online_ms=0` | +| `publishing` 超过 `publishing_session_max_age` | 关闭 session,`end_reason=compensated_publishing_max_window`,按确认发流后到截断点累计 `mic_online_ms` | +| 消费者停机后恢复 | room outbox worker 继续从位点扫描;cron-service 触发补偿批处理扫描 MySQL open session 兜底 | 补偿扫描使用 MySQL `FOR UPDATE SKIP LOCKED` 锁定候选 session。单批 claim、关闭、daily/lifetime 聚合和 outbox 写入在同一个事务内完成,避免关闭成功但派生事件丢失。 -补偿关闭必须写 `end_reason=compensated_*`,不能伪装成用户主动下麦。补偿必须设置最大可累计窗口,避免房间事件缺失时把异常 open session 算成超长在线。当前默认配置: +补偿关闭必须写 `end_reason=compensated_*`,不能伪装成用户主动下麦。补偿必须设置最大可累计窗口,避免房间事件缺失时把异常 open session 算成超长在线。当前默认配置在 cron-service: | 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 小时 | +| `tasks.mic_open_session_compensation.enabled` | `true` | 是否启动异常 session 补偿调度 | +| `tasks.mic_open_session_compensation.interval` | `60s` | 没有积压时两轮扫描间隔 | +| `tasks.mic_open_session_compensation.batch_size` | `100` | 单批最多关闭 session 数 | +| `tasks.mic_open_session_compensation.pending_publish_max_age` | `2m` | pending 未确认发流的最大保留窗口 | +| `tasks.mic_open_session_compensation.publishing_session_max_age` | `12h` | publishing session 最大可累计窗口 | ## Derived Events And Consumers @@ -408,7 +408,7 @@ 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。 +7. 已增加 `CompensateMicOpenSessions`,由 cron-service 调度关闭异常 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/pkg/xerr/catalog.go b/pkg/xerr/catalog.go index c2fcfdac..56a3ead9 100644 --- a/pkg/xerr/catalog.go +++ b/pkg/xerr/catalog.go @@ -41,6 +41,7 @@ var catalog = map[Code]Spec{ Internal: spec(codes.Internal, httpStatusInternalServerError, Internal, "internal error"), AuthFailed: spec(codes.Unauthenticated, httpStatusUnauthorized, AuthFailed, "authentication failed"), + AuthLoginBlocked: spec(codes.PermissionDenied, httpStatusForbidden, AuthLoginBlocked, "login blocked"), PasswordAlreadySet: spec(codes.AlreadyExists, httpStatusConflict, PasswordAlreadySet, "conflict"), UserDisabled: spec(codes.PermissionDenied, httpStatusForbidden, UserDisabled, "permission denied"), SessionExpired: spec(codes.Unauthenticated, httpStatusUnauthorized, SessionExpired, "unauthorized"), diff --git a/pkg/xerr/errors.go b/pkg/xerr/errors.go index dfe8d4e0..0a9cf9aa 100644 --- a/pkg/xerr/errors.go +++ b/pkg/xerr/errors.go @@ -31,6 +31,8 @@ const ( // AuthFailed 统一短号不存在、未设置密码、密码错误和三方凭证拒绝,避免身份枚举。 AuthFailed Code = "AUTH_FAILED" + // AuthLoginBlocked 表示登录入口命中地区、语言、时区或 IP 风控策略。 + AuthLoginBlocked Code = "AUTH_LOGIN_BLOCKED" // PasswordAlreadySet 表示用户已经设置过密码,不能重复初始化密码身份。 PasswordAlreadySet Code = "PASSWORD_ALREADY_SET" // UserDisabled 表示用户被禁用或封禁。 diff --git a/scripts/apply-local-mysql-initdb.sh b/scripts/apply-local-mysql-initdb.sh index 05df6a82..86be7e21 100755 --- a/scripts/apply-local-mysql-initdb.sh +++ b/scripts/apply-local-mysql-initdb.sh @@ -18,6 +18,7 @@ SQL_FILES=( "deploy/mysql/initdb/005_utf8mb4_chinese_support.sql" "deploy/mysql/initdb/006_admin_database.sql" "deploy/mysql/initdb/007_resource_group_wallet_asset_items.sql" + "services/cron-service/deploy/mysql/initdb/001_cron_service.sql" "deploy/mysql/initdb/999_local_grants.sql" ) diff --git a/scripts/print-compose-addresses.sh b/scripts/print-compose-addresses.sh index cc425335..3952f655 100755 --- a/scripts/print-compose-addresses.sh +++ b/scripts/print-compose-addresses.sh @@ -97,6 +97,7 @@ print_row "room-service" "gRPC" "grpc" "room-service" "13001" "13001" print_row "wallet-service" "gRPC" "grpc" "wallet-service" "13004" "13004" print_row "user-service" "gRPC" "grpc" "user-service" "13005" "13005" print_row "activity-service" "gRPC" "grpc" "activity-service" "13006" "13006" +print_row "cron-service" "gRPC" "grpc" "cron-service" "13007" "13007" print_row "mysql" "MySQL" "tcp" "mysql" "3306" "23306" print_row "redis" "Redis" "tcp" "redis" "6379" "13379" diff --git a/server/admin/cmd/server/main.go b/server/admin/cmd/server/main.go index 8b18d764..4919d14c 100644 --- a/server/admin/cmd/server/main.go +++ b/server/admin/cmd/server/main.go @@ -16,6 +16,7 @@ import ( "hyapp-admin-server/internal/cache" "hyapp-admin-server/internal/config" + "hyapp-admin-server/internal/integration/activityclient" "hyapp-admin-server/internal/integration/userclient" "hyapp-admin-server/internal/integration/walletclient" jobrunner "hyapp-admin-server/internal/job" @@ -27,6 +28,7 @@ import ( auditmodule "hyapp-admin-server/internal/modules/audit" authmodule "hyapp-admin-server/internal/modules/auth" countryregionmodule "hyapp-admin-server/internal/modules/countryregion" + dailytaskmodule "hyapp-admin-server/internal/modules/dailytask" dashboardmodule "hyapp-admin-server/internal/modules/dashboard" healthmodule "hyapp-admin-server/internal/modules/health" hostorgmodule "hyapp-admin-server/internal/modules/hostorg" @@ -152,6 +154,12 @@ func main() { } defer walletConn.Close() + activityConn, err := grpc.Dial(cfg.ActivityService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + fatalRuntime("connect_activity_service_failed", err) + } + defer activityConn.Close() + auth := service.NewAuthService(cfg.JWTSecret, cfg.AccessTokenTTL) var runner *jobrunner.Runner var jobStatus healthmodule.JobStatusProvider @@ -185,6 +193,7 @@ func main() { AppRegistry: appregistrymodule.New(userDB), AppUser: appusermodule.New(userclient.NewGRPC(userConn), userDB, walletDB, cfg, auditHandler), CountryRegion: countryregionmodule.New(userclient.NewGRPC(userConn), userDB, cfg, auditHandler), + DailyTask: dailytaskmodule.New(activityclient.NewGRPC(activityConn), auditHandler), Dashboard: dashboardmodule.New(store), Health: healthmodule.New(store, redisClient, jobStatus), HostOrg: hostorgmodule.New(userclient.NewGRPC(userConn), walletclient.NewGRPC(walletConn), userDB, walletDB, sqlDB, auditHandler), diff --git a/server/admin/configs/config.yaml b/server/admin/configs/config.yaml index 9e5364b2..706be4d2 100644 --- a/server/admin/configs/config.yaml +++ b/server/admin/configs/config.yaml @@ -38,6 +38,9 @@ user_service: wallet_service: addr: "127.0.0.1:13004" request_timeout: "3s" +activity_service: + addr: "127.0.0.1:13006" + request_timeout: "3s" tencent-cos: enabled: true secret-id: "IKIDMchhZEfrsiNo472DAtTpzzmLjttkOnyu" diff --git a/server/admin/internal/config/config.go b/server/admin/internal/config/config.go index e68da971..6560c7e7 100644 --- a/server/admin/internal/config/config.go +++ b/server/admin/internal/config/config.go @@ -13,29 +13,30 @@ import ( ) 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"` - WalletMySQLDSN string `yaml:"wallet_mysql_dsn"` - RoomMySQLDSN string `yaml:"room_mysql_dsn"` - MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"` - Migrations MigrationConfig `yaml:"migrations"` - JWTSecret string `yaml:"jwt_secret"` - AccessTokenTTL time.Duration `yaml:"access_token_ttl"` - RefreshTokenTTL time.Duration `yaml:"refresh_token_ttl"` - CORSAllowedOrigins []string `yaml:"cors_allowed_origins"` - RefreshCookieSecure bool `yaml:"refresh_cookie_secure"` - Bootstrap BootstrapConfig `yaml:"bootstrap"` - BootstrapPassword string `yaml:"bootstrap_password"` - UserService UserServiceConfig `yaml:"user_service"` - TencentCOS TencentCOSConfig `yaml:"tencent-cos"` - Redis RedisConfig `yaml:"redis"` - Jobs JobsConfig `yaml:"jobs"` - WalletService WalletServiceConfig `yaml:"wallet_service"` + 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"` + WalletMySQLDSN string `yaml:"wallet_mysql_dsn"` + RoomMySQLDSN string `yaml:"room_mysql_dsn"` + MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"` + Migrations MigrationConfig `yaml:"migrations"` + JWTSecret string `yaml:"jwt_secret"` + AccessTokenTTL time.Duration `yaml:"access_token_ttl"` + RefreshTokenTTL time.Duration `yaml:"refresh_token_ttl"` + CORSAllowedOrigins []string `yaml:"cors_allowed_origins"` + RefreshCookieSecure bool `yaml:"refresh_cookie_secure"` + Bootstrap BootstrapConfig `yaml:"bootstrap"` + BootstrapPassword string `yaml:"bootstrap_password"` + UserService UserServiceConfig `yaml:"user_service"` + TencentCOS TencentCOSConfig `yaml:"tencent-cos"` + Redis RedisConfig `yaml:"redis"` + Jobs JobsConfig `yaml:"jobs"` + WalletService WalletServiceConfig `yaml:"wallet_service"` + ActivityService ActivityServiceConfig `yaml:"activity_service"` } type MigrationConfig struct { @@ -53,6 +54,11 @@ type WalletServiceConfig struct { RequestTimeout time.Duration `yaml:"request_timeout"` } +type ActivityServiceConfig struct { + Addr string `yaml:"addr"` + RequestTimeout time.Duration `yaml:"request_timeout"` +} + type TencentCOSConfig struct { Enabled bool `yaml:"enabled"` SecretID string `yaml:"secret-id"` @@ -129,6 +135,10 @@ func Default() Config { Addr: "127.0.0.1:13004", RequestTimeout: 3 * time.Second, }, + ActivityService: ActivityServiceConfig{ + Addr: "127.0.0.1:13006", + RequestTimeout: 3 * time.Second, + }, TencentCOS: TencentCOSConfig{ Enabled: false, ObjectPrefix: "admin", @@ -288,6 +298,12 @@ func (cfg Config) Validate() error { if cfg.WalletService.RequestTimeout <= 0 { return errors.New("wallet_service.request_timeout must be greater than 0") } + if strings.TrimSpace(cfg.ActivityService.Addr) == "" { + return errors.New("activity_service.addr is required") + } + if cfg.ActivityService.RequestTimeout <= 0 { + return errors.New("activity_service.request_timeout must be greater than 0") + } if cfg.TencentCOS.Enabled { if cfg.TencentCOS.SecretID == "" || cfg.TencentCOS.SecretKey == "" || cfg.TencentCOS.BucketName == "" || cfg.TencentCOS.Region == "" || cfg.TencentCOS.AccessURL == "" { return errors.New("tencent-cos config is incomplete") diff --git a/server/admin/internal/integration/activityclient/client.go b/server/admin/internal/integration/activityclient/client.go new file mode 100644 index 00000000..1fcf4b6c --- /dev/null +++ b/server/admin/internal/integration/activityclient/client.go @@ -0,0 +1,36 @@ +package activityclient + +import ( + "context" + + activityv1 "hyapp.local/api/proto/activity/v1" + + "google.golang.org/grpc" +) + +// Client 是 admin-server 访问 activity-service 任务配置能力的最小 gRPC 依赖。 +type Client interface { + ListTaskDefinitions(ctx context.Context, req *activityv1.ListTaskDefinitionsRequest) (*activityv1.ListTaskDefinitionsResponse, error) + UpsertTaskDefinition(ctx context.Context, req *activityv1.UpsertTaskDefinitionRequest) (*activityv1.UpsertTaskDefinitionResponse, error) + SetTaskDefinitionStatus(ctx context.Context, req *activityv1.SetTaskDefinitionStatusRequest) (*activityv1.SetTaskDefinitionStatusResponse, error) +} + +type GRPCClient struct { + client activityv1.AdminTaskServiceClient +} + +func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient { + return &GRPCClient{client: activityv1.NewAdminTaskServiceClient(conn)} +} + +func (c *GRPCClient) ListTaskDefinitions(ctx context.Context, req *activityv1.ListTaskDefinitionsRequest) (*activityv1.ListTaskDefinitionsResponse, error) { + return c.client.ListTaskDefinitions(ctx, req) +} + +func (c *GRPCClient) UpsertTaskDefinition(ctx context.Context, req *activityv1.UpsertTaskDefinitionRequest) (*activityv1.UpsertTaskDefinitionResponse, error) { + return c.client.UpsertTaskDefinition(ctx, req) +} + +func (c *GRPCClient) SetTaskDefinitionStatus(ctx context.Context, req *activityv1.SetTaskDefinitionStatusRequest) (*activityv1.SetTaskDefinitionStatusResponse, error) { + return c.client.SetTaskDefinitionStatus(ctx, req) +} diff --git a/server/admin/internal/modules/appuser/handler.go b/server/admin/internal/modules/appuser/handler.go index 3a48a9fe..0b23db0b 100644 --- a/server/admin/internal/modules/appuser/handler.go +++ b/server/admin/internal/modules/appuser/handler.go @@ -39,6 +39,31 @@ func (h *Handler) ListUsers(c *gin.Context) { response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total}) } +func (h *Handler) ListLoginLogs(c *gin.Context) { + query := parseLoginLogQuery(c) + items, total, err := h.service.ListLoginLogs(c.Request.Context(), query) + if err != nil { + response.ServerError(c, "获取登录日志失败") + return + } + response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total}) +} + +func (h *Handler) ListUserLoginLogs(c *gin.Context) { + userID, ok := parseInt64ID(c, "id") + if !ok { + return + } + query := parseLoginLogQuery(c) + query.UserID = userID + items, total, err := h.service.ListLoginLogs(c.Request.Context(), query) + if err != nil { + response.ServerError(c, "获取用户登录日志失败") + return + } + response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total}) +} + func (h *Handler) GetUser(c *gin.Context) { userID, ok := parseInt64ID(c, "id") if !ok { @@ -125,6 +150,61 @@ func parseListQuery(c *gin.Context) listQuery { }) } +func parseLoginLogQuery(c *gin.Context) loginLogQuery { + options := shared.ListOptions(c) + regionID, regionIDSet := queryOptionalInt64(c, "region_id", "regionId") + return normalizeLoginLogQuery(loginLogQuery{ + Page: options.Page, + PageSize: options.PageSize, + Keyword: options.Keyword, + UserID: queryInt64(c, "user_id", "userId"), + DisplayUserID: firstQuery(c, "display_user_id", "displayUserId"), + IP: firstQuery(c, "ip"), + IPCountryCode: firstQuery(c, "ip_country_code", "ipCountryCode"), + Channel: firstQuery(c, "channel"), + Platform: firstQuery(c, "platform"), + Result: firstQuery(c, "result"), + Blocked: firstQuery(c, "blocked"), + RegionID: regionID, + RegionIDSet: regionIDSet, + StartMs: queryInt64(c, "start_ms", "startMs"), + EndMs: queryInt64(c, "end_ms", "endMs"), + }) +} + +func firstQuery(c *gin.Context, names ...string) string { + for _, name := range names { + if value := strings.TrimSpace(c.Query(name)); value != "" { + return value + } + } + return "" +} + +func queryInt64(c *gin.Context, names ...string) int64 { + value := firstQuery(c, names...) + if value == "" { + return 0 + } + parsed, err := strconv.ParseInt(value, 10, 64) + if err != nil || parsed < 0 { + return 0 + } + return parsed +} + +func queryOptionalInt64(c *gin.Context, names ...string) (int64, bool) { + value := firstQuery(c, names...) + if value == "" { + return 0, false + } + parsed, err := strconv.ParseInt(value, 10, 64) + if err != nil || parsed < 0 { + return 0, false + } + return parsed, true +} + func parseInt64ID(c *gin.Context, name string) (int64, bool) { raw := strings.TrimSpace(c.Param(name)) id, err := strconv.ParseInt(raw, 10, 64) diff --git a/server/admin/internal/modules/appuser/login_logs.go b/server/admin/internal/modules/appuser/login_logs.go new file mode 100644 index 00000000..dc2b5a61 --- /dev/null +++ b/server/admin/internal/modules/appuser/login_logs.go @@ -0,0 +1,231 @@ +package appuser + +import ( + "context" + "database/sql" + "fmt" + "strconv" + "strings" + + "hyapp-admin-server/internal/appctx" +) + +type AppLoginLog struct { + ID int64 `json:"id"` + RequestID string `json:"requestId"` + UserID string `json:"userId"` + DisplayUserID string `json:"displayUserId"` + Username string `json:"username"` + Avatar string `json:"avatar"` + Country string `json:"country"` + RegionID int64 `json:"regionId"` + RegionName string `json:"regionName"` + LoginIP string `json:"loginIp"` + IPCountryCode string `json:"ipCountryCode"` + Channel string `json:"channel"` + Platform string `json:"platform"` + Result string `json:"result"` + FailureCode string `json:"failureCode"` + Blocked bool `json:"blocked"` + BlockReason string `json:"blockReason"` + RiskHighlighted bool `json:"riskHighlighted"` + LoginType string `json:"loginType"` + Provider string `json:"provider"` + CreatedAtMs int64 `json:"createdAtMs"` +} + +func (s *Service) ListLoginLogs(ctx context.Context, query loginLogQuery) ([]AppLoginLog, int64, error) { + if s.userDB == nil { + return nil, 0, fmt.Errorf("user mysql is not configured") + } + query = normalizeLoginLogQuery(query) + whereSQL, args := loginLogWhereSQL(ctx, query) + total, err := countRows(ctx, s.userDB, whereSQL, args...) + if err != nil { + return nil, 0, err + } + rows, err := s.userDB.QueryContext(ctx, fmt.Sprintf(` + SELECT la.id, + la.request_id, + COALESCE(la.user_id, 0), + COALESCE(u.current_display_user_id, ''), + COALESCE(u.username, ''), + COALESCE(u.avatar, ''), + COALESCE(u.country, ''), + COALESCE(u.region_id, 0), + CASE + WHEN COALESCE(u.region_id, 0) = 0 THEN 'GLOBAL' + ELSE COALESCE((SELECT rg.name FROM regions rg WHERE rg.app_code = u.app_code AND rg.region_id = u.region_id LIMIT 1), '') + END, + COALESCE(la.client_ip, ''), + COALESCE(la.ip_country_code, ''), + COALESCE(NULLIF(la.channel, ''), CASE WHEN la.login_type = 'password' THEN 'account' ELSE COALESCE(la.provider, '') END), + COALESCE(la.platform, ''), + la.result, + COALESCE(la.failure_code, ''), + la.blocked, + la.block_reason, + la.login_type, + COALESCE(la.provider, ''), + la.created_at_ms + %s + ORDER BY la.created_at_ms DESC, la.id DESC + LIMIT ? OFFSET ? + `, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...) + if err != nil { + return nil, 0, err + } + defer rows.Close() + + items := make([]AppLoginLog, 0, query.PageSize) + for rows.Next() { + item, err := scanAppLoginLog(rows) + if err != nil { + return nil, 0, err + } + items = append(items, item) + } + if err := rows.Err(); err != nil { + return nil, 0, err + } + return items, total, nil +} + +func loginLogWhereSQL(ctx context.Context, query loginLogQuery) (string, []any) { + whereSQL := ` + FROM login_audit la + LEFT JOIN users u ON u.app_code = la.app_code AND u.user_id = la.user_id + WHERE la.app_code = ?` + args := []any{appctx.FromContext(ctx)} + if query.UserID > 0 { + whereSQL += " AND la.user_id = ?" + args = append(args, query.UserID) + } + if query.DisplayUserID != "" { + whereSQL += " AND u.current_display_user_id = ?" + args = append(args, query.DisplayUserID) + } + if query.IP != "" { + whereSQL += " AND la.client_ip = ?" + args = append(args, query.IP) + } + if query.IPCountryCode != "" { + whereSQL += " AND la.ip_country_code = ?" + args = append(args, query.IPCountryCode) + } + if query.Channel != "" { + whereSQL += " AND la.channel = ?" + args = append(args, query.Channel) + } + if query.Platform != "" { + whereSQL += " AND la.platform = ?" + args = append(args, query.Platform) + } + if query.RegionIDSet { + whereSQL += " AND COALESCE(u.region_id, 0) = ?" + args = append(args, query.RegionID) + } + switch query.Blocked { + case "blocked": + whereSQL += " AND (la.blocked = 1 OR la.failure_code = 'AUTH_LOGIN_BLOCKED')" + case "not_blocked": + whereSQL += " AND la.blocked = 0 AND COALESCE(la.failure_code, '') <> 'AUTH_LOGIN_BLOCKED'" + } + if query.Result != "" { + whereSQL += " AND la.result = ?" + args = append(args, query.Result) + } + if query.StartMs > 0 { + whereSQL += " AND la.created_at_ms >= ?" + args = append(args, query.StartMs) + } + if query.EndMs > 0 { + whereSQL += " AND la.created_at_ms <= ?" + args = append(args, query.EndMs) + } + if query.Keyword != "" { + like := "%" + query.Keyword + "%" + whereSQL += " AND (CAST(la.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ? OR la.client_ip LIKE ?)" + args = append(args, like, like, like, like) + } + return whereSQL, args +} + +func scanAppLoginLog(rows *sql.Rows) (AppLoginLog, error) { + var item AppLoginLog + var userID int64 + if err := rows.Scan( + &item.ID, + &item.RequestID, + &userID, + &item.DisplayUserID, + &item.Username, + &item.Avatar, + &item.Country, + &item.RegionID, + &item.RegionName, + &item.LoginIP, + &item.IPCountryCode, + &item.Channel, + &item.Platform, + &item.Result, + &item.FailureCode, + &item.Blocked, + &item.BlockReason, + &item.LoginType, + &item.Provider, + &item.CreatedAtMs, + ); err != nil { + return AppLoginLog{}, err + } + if userID > 0 { + item.UserID = strconv.FormatInt(userID, 10) + } + item.Result = normalizeLoginLogResult(item.Result, item.FailureCode, item.Blocked) + item.RiskHighlighted = item.Blocked || item.FailureCode == "AUTH_LOGIN_BLOCKED" + return item, nil +} + +func normalizeLoginLogResult(result string, failureCode string, blocked bool) string { + result = strings.TrimSpace(result) + if result != "" { + return result + } + if blocked || failureCode == "AUTH_LOGIN_BLOCKED" { + return "blocked" + } + return "failed" +} + +func normalizeLoginLogQuery(query loginLogQuery) loginLogQuery { + if query.Page < 1 { + query.Page = 1 + } + if query.PageSize < 1 { + query.PageSize = 20 + } + if query.PageSize > 100 { + query.PageSize = 100 + } + query.Keyword = strings.TrimSpace(query.Keyword) + query.DisplayUserID = strings.TrimSpace(query.DisplayUserID) + query.IP = strings.TrimSpace(query.IP) + query.IPCountryCode = normalizeCountryCode(query.IPCountryCode) + query.Channel = strings.ToLower(strings.TrimSpace(query.Channel)) + query.Platform = strings.ToLower(strings.TrimSpace(query.Platform)) + query.Blocked = strings.ToLower(strings.TrimSpace(query.Blocked)) + query.Result = strings.ToLower(strings.TrimSpace(query.Result)) + if query.Result == "blocked" { + query.Blocked = "blocked" + query.Result = "" + } + if query.Blocked != "blocked" && query.Blocked != "not_blocked" { + query.Blocked = "all" + } + switch query.Result { + case "", "failed", "revoked", "success": + default: + query.Result = "" + } + return query +} diff --git a/server/admin/internal/modules/appuser/request.go b/server/admin/internal/modules/appuser/request.go index e8988c40..7669f430 100644 --- a/server/admin/internal/modules/appuser/request.go +++ b/server/admin/internal/modules/appuser/request.go @@ -7,6 +7,24 @@ type listQuery struct { Status string } +type loginLogQuery struct { + Page int + PageSize int + Keyword string + UserID int64 + DisplayUserID string + IP string + IPCountryCode string + Channel string + Platform string + Result string + Blocked string + RegionID int64 + RegionIDSet bool + StartMs int64 + EndMs int64 +} + type updateUserRequest struct { Avatar *string `json:"avatar"` Country *string `json:"country"` diff --git a/server/admin/internal/modules/appuser/routes.go b/server/admin/internal/modules/appuser/routes.go index c5d1f903..fc954be7 100644 --- a/server/admin/internal/modules/appuser/routes.go +++ b/server/admin/internal/modules/appuser/routes.go @@ -12,6 +12,8 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) { } protected.GET("/app/users", middleware.RequirePermission("app-user:view"), h.ListUsers) + protected.GET("/app/users/login-logs", middleware.RequirePermission("app-user:view"), h.ListLoginLogs) + protected.GET("/app/users/:id/login-logs", middleware.RequirePermission("app-user:view"), h.ListUserLoginLogs) protected.GET("/app/users/:id", middleware.RequirePermission("app-user:view"), h.GetUser) protected.PATCH("/app/users/:id", middleware.RequirePermission("app-user:update"), h.UpdateUser) protected.POST("/app/users/:id/ban", middleware.RequirePermission("app-user:status"), h.BanUser) diff --git a/server/admin/internal/modules/appuser/service.go b/server/admin/internal/modules/appuser/service.go index c73d34e2..e5bb0da9 100644 --- a/server/admin/internal/modules/appuser/service.go +++ b/server/admin/internal/modules/appuser/service.go @@ -322,9 +322,9 @@ func (s *Service) SetUserStatus(ctx context.Context, userID int64, status string if status == "banned" || status == "disabled" { if _, err := tx.ExecContext(ctx, ` UPDATE auth_sessions - SET revoked_at_ms = ?, updated_at_ms = ? + SET revoked_at_ms = ?, revoked_reason = ?, revoked_request_id = '', revoked_by = ?, updated_at_ms = ? WHERE app_code = ? AND user_id = ? AND revoked_at_ms IS NULL - `, now, now, appCode, userID); err != nil { + `, now, "ADMIN_USER_STATUS", "admin", now, appCode, userID); err != nil { return AppUser{}, err } } diff --git a/server/admin/internal/modules/dailytask/handler.go b/server/admin/internal/modules/dailytask/handler.go new file mode 100644 index 00000000..bcbc2aa8 --- /dev/null +++ b/server/admin/internal/modules/dailytask/handler.go @@ -0,0 +1,224 @@ +package dailytask + +import ( + "strings" + "time" + + "hyapp-admin-server/internal/appctx" + "hyapp-admin-server/internal/integration/activityclient" + "hyapp-admin-server/internal/middleware" + "hyapp-admin-server/internal/modules/shared" + "hyapp-admin-server/internal/response" + activityv1 "hyapp.local/api/proto/activity/v1" + + "github.com/gin-gonic/gin" +) + +type Handler struct { + activity activityclient.Client + audit shared.OperationLogger +} + +func New(activity activityclient.Client, audit shared.OperationLogger) *Handler { + return &Handler{activity: activity, audit: audit} +} + +type taskRequest struct { + TaskType string `json:"task_type"` + Category string `json:"category"` + MetricType string `json:"metric_type"` + Title string `json:"title"` + Description string `json:"description"` + TargetValue int64 `json:"target_value"` + TargetUnit string `json:"target_unit"` + RewardCoinAmount int64 `json:"reward_coin_amount"` + Status string `json:"status"` + SortOrder int32 `json:"sort_order"` + EffectiveFromMS int64 `json:"effective_from_ms"` + EffectiveToMS int64 `json:"effective_to_ms"` +} + +type statusRequest struct { + Status string `json:"status"` +} + +type taskDTO struct { + TaskID string `json:"task_id"` + TaskType string `json:"task_type"` + Category string `json:"category"` + MetricType string `json:"metric_type"` + Title string `json:"title"` + Description string `json:"description"` + TargetValue int64 `json:"target_value"` + TargetUnit string `json:"target_unit"` + RewardCoinAmount int64 `json:"reward_coin_amount"` + Status string `json:"status"` + SortOrder int32 `json:"sort_order"` + Version int64 `json:"version"` + EffectiveFromMS int64 `json:"effective_from_ms"` + EffectiveToMS int64 `json:"effective_to_ms"` + CreatedByAdminID int64 `json:"created_by_admin_id"` + UpdatedByAdminID int64 `json:"updated_by_admin_id"` + CreatedAtMS int64 `json:"created_at_ms"` + UpdatedAtMS int64 `json:"updated_at_ms"` +} + +// ListTaskDefinitions 返回活动管理 > 每日任务页的 daily/exclusive 配置列表。 +func (h *Handler) ListTaskDefinitions(c *gin.Context) { + options := shared.ListOptions(c) + resp, err := h.activity.ListTaskDefinitions(c.Request.Context(), &activityv1.ListTaskDefinitionsRequest{ + Meta: h.meta(c), + TaskType: strings.TrimSpace(firstQuery(c, "task_type", "taskType")), + Category: strings.TrimSpace(c.Query("category")), + Status: options.Status, + Keyword: options.Keyword, + Page: int32(options.Page), + PageSize: int32(options.PageSize), + }) + if err != nil { + response.ServerError(c, "获取任务列表失败") + return + } + items := make([]taskDTO, 0, len(resp.GetTasks())) + for _, item := range resp.GetTasks() { + items = append(items, taskFromProto(item)) + } + response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()}) +} + +// CreateTaskDefinition 创建任务配置;active 任务会立即被 App 查询看到。 +func (h *Handler) CreateTaskDefinition(c *gin.Context) { + var req taskRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "任务参数不正确") + return + } + resp, err := h.activity.UpsertTaskDefinition(c.Request.Context(), req.toProto(c, "", h.meta(c))) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + item := taskFromProto(resp.GetTask()) + h.auditLog(c, "create-daily-task", item.TaskID, item.Title) + response.Created(c, item) +} + +// UpdateTaskDefinition 编辑任务配置;关键字段变更由 activity-service 生成新版本。 +func (h *Handler) UpdateTaskDefinition(c *gin.Context) { + taskID := strings.TrimSpace(c.Param("task_id")) + if taskID == "" { + response.BadRequest(c, "ID 参数不正确") + return + } + var req taskRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "任务参数不正确") + return + } + resp, err := h.activity.UpsertTaskDefinition(c.Request.Context(), req.toProto(c, taskID, h.meta(c))) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + item := taskFromProto(resp.GetTask()) + h.auditLog(c, "update-daily-task", item.TaskID, item.Title) + response.OK(c, item) +} + +// SetTaskDefinitionStatus 只做 draft/active/paused/archived 状态流转,不物理删除任务。 +func (h *Handler) SetTaskDefinitionStatus(c *gin.Context) { + taskID := strings.TrimSpace(c.Param("task_id")) + if taskID == "" { + response.BadRequest(c, "ID 参数不正确") + return + } + var req statusRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "状态参数不正确") + return + } + resp, err := h.activity.SetTaskDefinitionStatus(c.Request.Context(), &activityv1.SetTaskDefinitionStatusRequest{ + Meta: h.meta(c), + TaskId: taskID, + Status: strings.TrimSpace(req.Status), + OperatorAdminId: int64(middleware.CurrentUserID(c)), + }) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + item := taskFromProto(resp.GetTask()) + h.auditLog(c, "set-daily-task-status", item.TaskID, item.Status) + response.OK(c, item) +} + +func (r taskRequest) toProto(c *gin.Context, taskID string, meta *activityv1.RequestMeta) *activityv1.UpsertTaskDefinitionRequest { + return &activityv1.UpsertTaskDefinitionRequest{ + Meta: meta, + TaskId: taskID, + TaskType: strings.TrimSpace(r.TaskType), + Category: strings.TrimSpace(r.Category), + MetricType: strings.TrimSpace(r.MetricType), + Title: strings.TrimSpace(r.Title), + Description: strings.TrimSpace(r.Description), + TargetValue: r.TargetValue, + TargetUnit: strings.TrimSpace(r.TargetUnit), + RewardCoinAmount: r.RewardCoinAmount, + Status: strings.TrimSpace(r.Status), + SortOrder: r.SortOrder, + EffectiveFromMs: r.EffectiveFromMS, + EffectiveToMs: r.EffectiveToMS, + OperatorAdminId: int64(middleware.CurrentUserID(c)), + } +} + +func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta { + return &activityv1.RequestMeta{ + RequestId: middleware.CurrentRequestID(c), + Caller: "admin-server", + AppCode: appctx.FromContext(c.Request.Context()), + SentAtMs: time.Now().UnixMilli(), + } +} + +func (h *Handler) auditLog(c *gin.Context, action string, taskID string, detail string) { + if h.audit == nil { + return + } + shared.OperationLogWithResourceID(c, h.audit, action, "task_definitions", taskID, "success", detail) +} + +func taskFromProto(item *activityv1.TaskDefinition) taskDTO { + if item == nil { + return taskDTO{} + } + return taskDTO{ + TaskID: item.GetTaskId(), + TaskType: item.GetTaskType(), + Category: item.GetCategory(), + MetricType: item.GetMetricType(), + Title: item.GetTitle(), + Description: item.GetDescription(), + TargetValue: item.GetTargetValue(), + TargetUnit: item.GetTargetUnit(), + RewardCoinAmount: item.GetRewardCoinAmount(), + Status: item.GetStatus(), + SortOrder: item.GetSortOrder(), + Version: item.GetVersion(), + EffectiveFromMS: item.GetEffectiveFromMs(), + EffectiveToMS: item.GetEffectiveToMs(), + CreatedByAdminID: item.GetCreatedByAdminId(), + UpdatedByAdminID: item.GetUpdatedByAdminId(), + CreatedAtMS: item.GetCreatedAtMs(), + UpdatedAtMS: item.GetUpdatedAtMs(), + } +} + +func firstQuery(c *gin.Context, keys ...string) string { + for _, key := range keys { + if value := c.Query(key); value != "" { + return value + } + } + return "" +} diff --git a/server/admin/internal/modules/dailytask/routes.go b/server/admin/internal/modules/dailytask/routes.go new file mode 100644 index 00000000..0581d54a --- /dev/null +++ b/server/admin/internal/modules/dailytask/routes.go @@ -0,0 +1,18 @@ +package dailytask + +import ( + "hyapp-admin-server/internal/middleware" + + "github.com/gin-gonic/gin" +) + +func RegisterRoutes(protected *gin.RouterGroup, h *Handler) { + if h == nil { + return + } + + protected.GET("/admin/activity/daily-tasks", middleware.RequirePermission("daily-task:view"), h.ListTaskDefinitions) + protected.POST("/admin/activity/daily-tasks", middleware.RequirePermission("daily-task:create"), h.CreateTaskDefinition) + protected.PUT("/admin/activity/daily-tasks/:task_id", middleware.RequirePermission("daily-task:update"), h.UpdateTaskDefinition) + protected.POST("/admin/activity/daily-tasks/:task_id/status", middleware.RequirePermission("daily-task:status"), h.SetTaskDefinitionStatus) +} diff --git a/server/admin/internal/repository/seed.go b/server/admin/internal/repository/seed.go index fac141bf..0ec3a77f 100644 --- a/server/admin/internal/repository/seed.go +++ b/server/admin/internal/repository/seed.go @@ -57,6 +57,10 @@ var defaultPermissions = []model.Permission{ {Name: "币商更新", Code: "coin-seller:update", Kind: "button"}, {Name: "币商进货", Code: "coin-seller:stock-credit", Kind: "button"}, {Name: "支付账单查看", Code: "payment-bill:view", Kind: "menu"}, + {Name: "每日任务查看", Code: "daily-task:view", Kind: "menu"}, + {Name: "每日任务创建", Code: "daily-task:create", Kind: "button"}, + {Name: "每日任务更新", Code: "daily-task:update", Kind: "button"}, + {Name: "每日任务状态", Code: "daily-task:status", Kind: "button"}, {Name: "角色查看", Code: "role:view", Kind: "menu"}, {Name: "角色创建", Code: "role:create", Kind: "button"}, {Name: "角色更新", Code: "role:update", Kind: "button"}, @@ -96,7 +100,6 @@ func (s *Store) seedPermissions() error { func (s *Store) seedMenus() error { systemID := uint(0) - logID := uint(0) geoID := uint(0) hostOrgID := uint(0) appUsersID := uint(0) @@ -104,16 +107,18 @@ func (s *Store) seedMenus() error { resourceID := uint(0) roomsID := uint(0) paymentID := uint(0) + activityID := 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: "logs", Path: "", Icon: "history", PermissionCode: "", Sort: 40, Visible: true}, + {Title: "日志审计", Code: "logs", Path: "", Icon: "history", PermissionCode: "", Sort: 40, Visible: false}, {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}, {Title: "房间管理", Code: "rooms", Path: "", Icon: "room", PermissionCode: "", Sort: 65, Visible: true}, {Title: "APP配置", Code: "app-config", Path: "", Icon: "settings", PermissionCode: "", Sort: 66, Visible: true}, {Title: "资源管理", Code: "resources", Path: "", Icon: "inventory", PermissionCode: "", Sort: 67, Visible: true}, {Title: "支付管理", Code: "payment", Path: "", Icon: "wallet", PermissionCode: "", Sort: 68, Visible: true}, + {Title: "活动管理", Code: "activities", Path: "", Icon: "campaign", PermissionCode: "", Sort: 69, Visible: true}, {Title: "地区管理", Code: "geo", Path: "", Icon: "map", PermissionCode: "", Sort: 70, Visible: true}, {Title: "团队管理", Code: "host-org", Path: "", Icon: "network", PermissionCode: "", Sort: 80, Visible: true}, {Title: "任务中心", Code: "jobs", Path: "/jobs", Icon: "task", PermissionCode: "job:view", Sort: 90, Visible: true}, @@ -126,9 +131,6 @@ func (s *Store) seedMenus() error { if syncedMenu.Code == "system" { systemID = syncedMenu.ID } - if syncedMenu.Code == "logs" { - logID = syncedMenu.ID - } if syncedMenu.Code == "geo" { geoID = syncedMenu.ID } @@ -150,15 +152,19 @@ func (s *Store) seedMenus() error { if syncedMenu.Code == "payment" { paymentID = syncedMenu.ID } + if syncedMenu.Code == "activities" { + activityID = syncedMenu.ID + } } 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-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}, - {ParentID: &logID, Title: "操作日志", Code: "logs-operations", Path: "/logs/operations", Icon: "receipt", PermissionCode: "log:view", Sort: 42, Visible: true}, + {ParentID: &systemID, Title: "登录日志", Code: "logs-login", Path: "/logs/login", Icon: "login", PermissionCode: "log:view", Sort: 34, Visible: true}, + {ParentID: &systemID, Title: "操作日志", Code: "logs-operations", Path: "/logs/operations", Icon: "receipt", PermissionCode: "log:view", Sort: 35, Visible: true}, {ParentID: &appUsersID, Title: "用户列表", Code: "app-user-list", Path: "/app/users", Icon: "users", PermissionCode: "app-user:view", Sort: 60, Visible: true}, + {ParentID: &appUsersID, Title: "登录日志", Code: "app-user-login-logs", Path: "/app/users/login-logs", Icon: "login", PermissionCode: "app-user:view", Sort: 61, Visible: true}, {ParentID: &roomsID, Title: "房间列表", Code: "room-list", Path: "/rooms", Icon: "room", PermissionCode: "room:view", Sort: 65, Visible: true}, {ParentID: &appConfigID, Title: "H5配置", Code: "app-config-h5", Path: "/app-config/h5", Icon: "settings", PermissionCode: "app-config:view", Sort: 66, Visible: true}, {ParentID: &appConfigID, Title: "BANNER配置", Code: "app-config-banners", Path: "/app-config/banners", Icon: "image", PermissionCode: "app-config:view", Sort: 67, Visible: true}, @@ -167,6 +173,7 @@ func (s *Store) seedMenus() error { {ParentID: &resourceID, Title: "礼物列表", Code: "gift-list", Path: "/gifts", Icon: "gift", PermissionCode: "gift:view", Sort: 69, Visible: true}, {ParentID: &resourceID, Title: "资源赠送", Code: "resource-grant-list", Path: "/resource-grants", Icon: "send", PermissionCode: "resource-grant:view", Sort: 70, Visible: true}, {ParentID: &paymentID, Title: "账单列表", Code: "payment-bill-list", Path: "/payment/bills", Icon: "receipt", PermissionCode: "payment-bill:view", Sort: 68, Visible: true}, + {ParentID: &activityID, Title: "每日任务", Code: "daily-task-list", Path: "/activities/daily-tasks", Icon: "task", PermissionCode: "daily-task:view", Sort: 69, Visible: true}, {ParentID: &geoID, Title: "国家管理", Code: "host-org-countries", Path: "/host/countries", Icon: "public", PermissionCode: "country:view", Sort: 70, Visible: true}, {ParentID: &geoID, Title: "区域管理", Code: "host-org-regions", Path: "/host/regions", Icon: "map", PermissionCode: "region:view", Sort: 71, Visible: true}, {ParentID: &hostOrgID, Title: "Agency 管理", Code: "host-org-agencies", Path: "/host/agencies", Icon: "apartment", PermissionCode: "agency:view", Sort: 80, Visible: true}, @@ -412,13 +419,14 @@ func defaultRolePermissionCodes(code string) []string { "bd:view", "bd:create", "bd:update", "coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "payment-bill:view", + "daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status", "log:view", "notification:view", "notification:read", "job:view", "job:cancel", "export:create", "upload:create", } case "auditor": - return []string{"overview:view", "log:view", "user:view", "app-user:view", "room:view", "app-config:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "payment-bill:view", "role:view", "permission:view", "job:view"} + return []string{"overview:view", "log:view", "user:view", "app-user:view", "room:view", "app-config:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "payment-bill:view", "daily-task:view", "role:view", "permission:view", "job:view"} case "readonly": return []string{ "overview:view", @@ -437,6 +445,7 @@ func defaultRolePermissionCodes(code string) []string { "bd:view", "coin-seller:view", "payment-bill:view", + "daily-task:view", "role:view", "permission:view", "menu:view", @@ -465,9 +474,10 @@ func defaultRolePermissionMigrationCodes(code string) []string { "region:view", "region:create", "region:update", "region:status", "coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "payment-bill:view", + "daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status", } case "auditor", "readonly": - return []string{"room:view", "app-config:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "coin-seller:view", "payment-bill:view"} + return []string{"room:view", "app-config:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "coin-seller:view", "payment-bill:view", "daily-task:view"} default: return nil } diff --git a/server/admin/internal/router/router.go b/server/admin/internal/router/router.go index a0a90962..11363872 100644 --- a/server/admin/internal/router/router.go +++ b/server/admin/internal/router/router.go @@ -10,6 +10,7 @@ import ( "hyapp-admin-server/internal/modules/audit" authroutes "hyapp-admin-server/internal/modules/auth" "hyapp-admin-server/internal/modules/countryregion" + "hyapp-admin-server/internal/modules/dailytask" "hyapp-admin-server/internal/modules/dashboard" "hyapp-admin-server/internal/modules/health" "hyapp-admin-server/internal/modules/hostorg" @@ -36,6 +37,7 @@ type Handlers struct { Audit *audit.Handler Auth *authroutes.Handler CountryRegion *countryregion.Handler + DailyTask *dailytask.Handler Dashboard *dashboard.Handler Health *health.Handler HostOrg *hostorg.Handler @@ -70,6 +72,7 @@ func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine { appregistry.RegisterRoutes(protected, h.AppRegistry) appconfig.RegisterRoutes(protected, h.AppConfig) countryregion.RegisterRoutes(protected, h.CountryRegion) + dailytask.RegisterRoutes(protected, h.DailyTask) roomadmin.RegisterRoutes(protected, h.RoomAdmin) dashboard.RegisterRoutes(protected, h.Dashboard) hostorg.RegisterRoutes(protected, h.HostOrg) diff --git a/services/activity-service/configs/config.docker.yaml b/services/activity-service/configs/config.docker.yaml index 1f82dad4..03b18726 100644 --- a/services/activity-service/configs/config.docker.yaml +++ b/services/activity-service/configs/config.docker.yaml @@ -10,15 +10,11 @@ log: grpc_addr: ":13006" mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=Local" user_service_addr: "user-service:13005" +wallet_service_addr: "wallet-service:13004" +task_timezone: "Asia/Shanghai" mysql_auto_migrate: false consumer: room_outbox_poll_interval_ms: 1000 batch_size: 100 message_inbox: unread_cache_ttl: "60s" - fanout_worker: - enabled: true - poll_interval: "1s" - batch_size: 500 - lock_ttl: "30s" - max_retry: 8 diff --git a/services/activity-service/configs/config.tencent.example.yaml b/services/activity-service/configs/config.tencent.example.yaml index 4eac9b65..53373c62 100644 --- a/services/activity-service/configs/config.tencent.example.yaml +++ b/services/activity-service/configs/config.tencent.example.yaml @@ -10,15 +10,11 @@ log: grpc_addr: ":13006" mysql_dsn: "${TENCENT_MYSQL_ACTIVITY_DSN}" user_service_addr: "user-service.internal:13005" +wallet_service_addr: "wallet-service.internal:13004" +task_timezone: "Asia/Shanghai" mysql_auto_migrate: false consumer: room_outbox_poll_interval_ms: 1000 batch_size: 100 message_inbox: unread_cache_ttl: "60s" - fanout_worker: - enabled: true - poll_interval: "1s" - batch_size: 500 - lock_ttl: "30s" - max_retry: 8 diff --git a/services/activity-service/configs/config.yaml b/services/activity-service/configs/config.yaml index 8cae64e2..20a9ff20 100644 --- a/services/activity-service/configs/config.yaml +++ b/services/activity-service/configs/config.yaml @@ -10,15 +10,11 @@ log: 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" +wallet_service_addr: "127.0.0.1:13004" +task_timezone: "Asia/Shanghai" mysql_auto_migrate: false consumer: room_outbox_poll_interval_ms: 1000 batch_size: 100 message_inbox: unread_cache_ttl: "60s" - fanout_worker: - enabled: true - poll_interval: "1s" - batch_size: 500 - lock_ttl: "30s" - max_retry: 8 diff --git a/services/activity-service/deploy/mysql/initdb/001_activity_service.sql b/services/activity-service/deploy/mysql/initdb/001_activity_service.sql index e994f2f8..22ff8e00 100644 --- a/services/activity-service/deploy/mysql/initdb/001_activity_service.sql +++ b/services/activity-service/deploy/mysql/initdb/001_activity_service.sql @@ -42,6 +42,99 @@ CREATE TABLE IF NOT EXISTS activity_outbox ( KEY idx_activity_outbox_status_retry (app_code, status, next_retry_at_ms) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS task_definitions ( + app_code VARCHAR(32) NOT NULL DEFAULT 'lalu', + task_id VARCHAR(96) NOT NULL, + task_type VARCHAR(32) NOT NULL, + category VARCHAR(32) NOT NULL, + metric_type VARCHAR(64) NOT NULL, + title VARCHAR(160) NOT NULL, + description VARCHAR(512) NOT NULL DEFAULT '', + target_value BIGINT NOT NULL, + target_unit VARCHAR(32) NOT NULL, + reward_coin_amount BIGINT NOT NULL, + status VARCHAR(32) NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + version BIGINT NOT NULL, + current_task_version_id BIGINT NOT NULL DEFAULT 0, + effective_from_ms BIGINT NOT NULL DEFAULT 0, + effective_to_ms BIGINT NOT NULL DEFAULT 0, + created_by_admin_id BIGINT NOT NULL DEFAULT 0, + updated_by_admin_id BIGINT NOT NULL DEFAULT 0, + created_at_ms BIGINT NOT NULL, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, task_id), + KEY idx_task_definitions_visible (app_code, task_type, status, sort_order), + KEY idx_task_definitions_metric (app_code, metric_type, status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS task_definition_versions ( + app_code VARCHAR(32) NOT NULL DEFAULT 'lalu', + task_version_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + task_id VARCHAR(96) NOT NULL, + version BIGINT NOT NULL, + snapshot_json JSON NOT NULL, + effective_from_cycle VARCHAR(32) NOT NULL, + created_at_ms BIGINT NOT NULL, + UNIQUE KEY uk_task_definition_versions_version (app_code, task_id, version), + KEY idx_task_definition_versions_task (app_code, task_id, task_version_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS user_task_progress ( + app_code VARCHAR(32) NOT NULL DEFAULT 'lalu', + user_id BIGINT NOT NULL, + task_id VARCHAR(96) NOT NULL, + task_version_id BIGINT NOT NULL, + cycle_key VARCHAR(32) NOT NULL, + progress_value BIGINT NOT NULL, + target_value BIGINT NOT NULL, + reward_coin_amount BIGINT NOT NULL, + status VARCHAR(32) NOT NULL, + completed_at_ms BIGINT NOT NULL DEFAULT 0, + claimed_at_ms BIGINT NOT NULL DEFAULT 0, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, user_id, task_id, cycle_key), + KEY idx_user_task_progress_user_cycle (app_code, user_id, cycle_key, status), + KEY idx_user_task_progress_task_cycle (app_code, task_id, cycle_key, status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS task_event_consumption ( + app_code VARCHAR(32) NOT NULL DEFAULT 'lalu', + event_id VARCHAR(128) NOT NULL, + event_type VARCHAR(96) NOT NULL, + source_service VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + task_day VARCHAR(32) NOT NULL, + status VARCHAR(32) NOT NULL, + skip_reason VARCHAR(255) NOT NULL DEFAULT '', + created_at_ms BIGINT NOT NULL, + consumed_at_ms BIGINT NOT NULL DEFAULT 0, + PRIMARY KEY (app_code, event_id), + KEY idx_task_event_consumption_status (app_code, status, consumed_at_ms), + KEY idx_task_event_consumption_user_day (app_code, user_id, task_day) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS task_reward_claims ( + app_code VARCHAR(32) NOT NULL DEFAULT 'lalu', + claim_id VARCHAR(96) NOT NULL, + command_id VARCHAR(128) NOT NULL, + user_id BIGINT NOT NULL, + task_id VARCHAR(96) NOT NULL, + task_type VARCHAR(32) NOT NULL, + cycle_key VARCHAR(32) NOT NULL, + reward_coin_amount BIGINT NOT NULL, + wallet_command_id VARCHAR(128) NOT NULL, + wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '', + status VARCHAR(32) NOT NULL, + failure_reason VARCHAR(255) NOT NULL DEFAULT '', + created_at_ms BIGINT NOT NULL, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, claim_id), + UNIQUE KEY uk_task_reward_claim_command (app_code, command_id), + UNIQUE KEY uk_task_reward_claim_once (app_code, user_id, task_id, cycle_key), + KEY idx_task_reward_claim_status (app_code, status, updated_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE IF NOT EXISTS message_templates ( app_code VARCHAR(32) NOT NULL, template_id VARCHAR(96) NOT NULL, diff --git a/services/activity-service/internal/app/app.go b/services/activity-service/internal/app/app.go index 52a1c070..82459750 100644 --- a/services/activity-service/internal/app/app.go +++ b/services/activity-service/internal/app/app.go @@ -11,29 +11,27 @@ import ( "google.golang.org/grpc/credentials/insecure" healthgrpc "google.golang.org/grpc/health/grpc_health_v1" activityv1 "hyapp.local/api/proto/activity/v1" + walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/grpchealth" "hyapp/pkg/logx" "hyapp/services/activity-service/internal/client" "hyapp/services/activity-service/internal/config" activityservice "hyapp/services/activity-service/internal/service/activity" messageservice "hyapp/services/activity-service/internal/service/message" + taskservice "hyapp/services/activity-service/internal/service/task" mysqlstorage "hyapp/services/activity-service/internal/storage/mysql" grpcserver "hyapp/services/activity-service/internal/transport/grpc" ) // App 装配 activity-service gRPC 入口和底座依赖。 type App struct { - server *grpc.Server - listener net.Listener - health *grpchealth.ServingChecker - mysqlRepo *mysqlstorage.Repository - userConn *grpc.ClientConn - message *messageservice.Service - cfg config.Config - workerCtx context.Context - workerCancel context.CancelFunc - workerWG sync.WaitGroup - closeOnce sync.Once + server *grpc.Server + listener net.Listener + health *grpchealth.ServingChecker + mysqlRepo *mysqlstorage.Repository + userConn *grpc.ClientConn + walletConn *grpc.ClientConn + closeOnce sync.Once } // New 初始化 activity-service 应用。 @@ -58,51 +56,39 @@ func New(cfg config.Config) (*App, error) { _ = repository.Close() return nil, err } + walletConn, err := grpc.Dial(cfg.WalletServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + _ = userConn.Close() + _ = listener.Close() + _ = repository.Close() + return nil, err + } server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("activity-service"))) svc := activityservice.New(activityservice.Config{NodeID: cfg.NodeID}, repository) messageSvc := messageservice.New(messageservice.Config{NodeID: cfg.NodeID}, repository, messageservice.WithTargetSource(client.NewGRPCUserTargetSource(userConn))) + taskSvc := taskservice.New(taskservice.Config{TaskTimezone: cfg.TaskTimezone}, repository, walletv1.NewWalletServiceClient(walletConn)) activityv1.RegisterActivityServiceServer(server, grpcserver.NewServer(svc)) activityv1.RegisterMessageInboxServiceServer(server, grpcserver.NewMessageServer(messageSvc)) + activityv1.RegisterActivityCronServiceServer(server, grpcserver.NewCronServer(messageSvc)) + activityv1.RegisterTaskServiceServer(server, grpcserver.NewTaskServer(taskSvc)) + activityv1.RegisterAdminTaskServiceServer(server, grpcserver.NewAdminTaskServer(taskSvc)) health := grpchealth.NewServingChecker("activity-service") healthgrpc.RegisterHealthServer(server, grpchealth.NewServer(health)) - workerCtx, workerCancel := context.WithCancel(context.Background()) return &App{ - server: server, - listener: listener, - health: health, - mysqlRepo: repository, - userConn: userConn, - message: messageSvc, - cfg: cfg, - workerCtx: workerCtx, - workerCancel: workerCancel, + server: server, + listener: listener, + health: health, + mysqlRepo: repository, + userConn: userConn, + walletConn: walletConn, }, nil } // Run 启动 gRPC 服务。 func (a *App) Run() error { a.health.MarkServing() - defer func() { - if a.workerCancel != nil { - a.workerCancel() - } - a.workerWG.Wait() - }() - if a.cfg.MessageInbox.FanoutWorker.Enabled { - a.workerWG.Add(1) - go func() { - defer a.workerWG.Done() - a.message.RunFanoutWorker(a.workerCtx, messageservice.FanoutWorkerOptions{ - WorkerID: a.cfg.NodeID, - PollInterval: parseDurationOrZero(a.cfg.MessageInbox.FanoutWorker.PollInterval), - LockTTL: parseDurationOrZero(a.cfg.MessageInbox.FanoutWorker.LockTTL), - BatchSize: a.cfg.MessageInbox.FanoutWorker.BatchSize, - MaxRetry: a.cfg.MessageInbox.FanoutWorker.MaxRetry, - }) - }() - } err := a.server.Serve(a.listener) a.health.MarkStopped() if errors.Is(err, grpc.ErrServerStopped) { @@ -116,25 +102,16 @@ func (a *App) Run() error { func (a *App) Close() { a.closeOnce.Do(func() { a.health.MarkDraining() - if a.workerCancel != nil { - a.workerCancel() - } - a.workerWG.Wait() a.server.GracefulStop() if a.userConn != nil { _ = a.userConn.Close() } + if a.walletConn != nil { + _ = a.walletConn.Close() + } if a.mysqlRepo != nil { // MySQL 连接池最后关闭,保证 drain 中的查询或消费提交能完成。 _ = a.mysqlRepo.Close() } }) } - -func parseDurationOrZero(raw string) time.Duration { - duration, err := time.ParseDuration(raw) - if err != nil { - return 0 - } - return duration -} diff --git a/services/activity-service/internal/config/config.go b/services/activity-service/internal/config/config.go index c234265a..205a6ff2 100644 --- a/services/activity-service/internal/config/config.go +++ b/services/activity-service/internal/config/config.go @@ -17,6 +17,10 @@ type Config struct { MySQLDSN string `yaml:"mysql_dsn"` // UserServiceAddr 是 fanout worker 读取目标用户游标的内部 gRPC 地址。 UserServiceAddr string `yaml:"user_service_addr"` + // WalletServiceAddr 是任务奖励入账依赖;activity 只发命令,不直接写余额。 + WalletServiceAddr string `yaml:"wallet_service_addr"` + // TaskTimezone 是 daily 任务统一周期时区,不能使用客户端手机时区。 + TaskTimezone string `yaml:"task_timezone"` // MySQLAutoMigrate 保留给显式本地迁移;线上 schema 由发布流程或 initdb 管理。 MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"` Consumer ConsumerConfig `yaml:"consumer"` @@ -30,44 +34,29 @@ type ConsumerConfig struct { BatchSize int `yaml:"batch_size"` } -// MessageInboxConfig 保存 App system/activity inbox 的缓存和 fanout worker 参数。 +// MessageInboxConfig 保存 App system/activity inbox 的读取缓存参数;fanout 扫描由 cron-service 调度。 type MessageInboxConfig struct { - UnreadCacheTTL string `yaml:"unread_cache_ttl"` - FanoutWorker MessageFanoutWorkerConfig `yaml:"fanout_worker"` -} - -// MessageFanoutWorkerConfig 描述后台消息 fanout worker 的 claim、批量和重试策略。 -type MessageFanoutWorkerConfig struct { - Enabled bool `yaml:"enabled"` - PollInterval string `yaml:"poll_interval"` - BatchSize int `yaml:"batch_size"` - LockTTL string `yaml:"lock_ttl"` - MaxRetry int `yaml:"max_retry"` + UnreadCacheTTL string `yaml:"unread_cache_ttl"` } // Default 返回本地默认配置。 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", - MySQLAutoMigrate: false, + 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", + WalletServiceAddr: "127.0.0.1:13004", + TaskTimezone: "Asia/Shanghai", + MySQLAutoMigrate: false, Consumer: ConsumerConfig{ RoomOutboxPollIntervalMs: 1000, BatchSize: 100, }, MessageInbox: MessageInboxConfig{ UnreadCacheTTL: "60s", - FanoutWorker: MessageFanoutWorkerConfig{ - Enabled: true, - PollInterval: "1s", - BatchSize: 500, - LockTTL: "30s", - MaxRetry: 8, - }, }, Log: logx.Config{ Level: "info", @@ -90,6 +79,12 @@ func Load(path string) (Config, error) { if strings.TrimSpace(cfg.UserServiceAddr) == "" { cfg.UserServiceAddr = Default().UserServiceAddr } + if strings.TrimSpace(cfg.WalletServiceAddr) == "" { + cfg.WalletServiceAddr = Default().WalletServiceAddr + } + if strings.TrimSpace(cfg.TaskTimezone) == "" { + cfg.TaskTimezone = Default().TaskTimezone + } cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) if cfg.ServiceName == "" { cfg.ServiceName = "activity-service" diff --git a/services/activity-service/internal/domain/task/task.go b/services/activity-service/internal/domain/task/task.go new file mode 100644 index 00000000..7aa06156 --- /dev/null +++ b/services/activity-service/internal/domain/task/task.go @@ -0,0 +1,166 @@ +package task + +const ( + TypeDaily = "daily" + TypeExclusive = "exclusive" + + SectionDaily = "daily" + SectionExclusive = "exclusive" + + CycleLifetime = "lifetime" + + StatusDraft = "draft" + StatusActive = "active" + StatusPaused = "paused" + StatusArchived = "archived" + StatusInProgress = "in_progress" + StatusCompleted = "completed" + StatusClaimed = "claimed" + StatusExpired = "expired" + + ClaimStatusPending = "pending" + ClaimStatusGranted = "granted" + ClaimStatusFailed = "failed" + + EventStatusConsumed = "consumed" + EventStatusSkipped = "skipped" + EventStatusFailed = "failed" +) + +// Definition 是 task_definitions 的当前版本读模型;App 查询只读这张表和用户进度表。 +type Definition struct { + AppCode string + TaskID string + TaskType string + Category string + MetricType string + Title string + Description string + TargetValue int64 + TargetUnit string + RewardCoinAmount int64 + Status string + SortOrder int32 + Version int64 + CurrentVersionID int64 + EffectiveFromMS int64 + EffectiveToMS int64 + CreatedByAdminID int64 + UpdatedByAdminID int64 + CreatedAtMS int64 + UpdatedAtMS int64 +} + +// Progress 是 user_task_progress 的用户维度状态;target/reward 使用创建进度时的任务快照。 +type Progress struct { + AppCode string + UserID int64 + TaskID string + TaskVersionID int64 + CycleKey string + ProgressValue int64 + TargetValue int64 + RewardCoinAmount int64 + Status string + CompletedAtMS int64 + ClaimedAtMS int64 + UpdatedAtMS int64 +} + +// Item 是 App 任务列表返回前的领域快照。 +type Item struct { + Definition + ProgressValue int64 + UserStatus string + Claimable bool + TaskDay string + ServerTimeMS int64 + NextRefreshAtMS int64 +} + +// Section 是 daily/exclusive 的固定分区。 +type Section struct { + Section string + Items []Item + ServerTimeMS int64 + NextRefreshAtMS int64 +} + +// ListResult 是 App 任务页的完整只读结果。 +type ListResult struct { + Sections []Section + ServerTimeMS int64 + NextRefreshAtMS int64 +} + +// DefinitionQuery 是后台任务配置列表筛选条件。 +type DefinitionQuery struct { + TaskType string + Category string + Status string + Keyword string + Page int32 + PageSize int32 +} + +// DefinitionCommand 是后台创建/编辑任务定义的命令快照。 +type DefinitionCommand struct { + TaskID string + TaskType string + Category string + MetricType string + Title string + Description string + TargetValue int64 + TargetUnit string + RewardCoinAmount int64 + Status string + SortOrder int32 + EffectiveFromMS int64 + EffectiveToMS int64 + OperatorAdminID int64 +} + +// Event 是由 wallet/user/room outbox 派生的任务进度事实。 +type Event struct { + EventID string + EventType string + SourceService string + UserID int64 + MetricType string + Value int64 + OccurredAtMS int64 +} + +// EventResult 表达事件消费幂等结果。 +type EventResult struct { + EventID string + Status string + MatchedTaskCount int32 +} + +// ClaimCommand 是用户领取任务奖励的幂等命令。 +type ClaimCommand struct { + UserID int64 + TaskID string + TaskType string + CycleKey string + CommandID string +} + +// Claim 是 task_reward_claims 的领奖事实。 +type Claim struct { + ClaimID string + CommandID string + UserID int64 + TaskID string + TaskType string + CycleKey string + RewardCoinAmount int64 + WalletCommandID string + WalletTransactionID string + Status string + FailureReason string + CreatedAtMS int64 + UpdatedAtMS int64 +} diff --git a/services/activity-service/internal/service/message/fanout_worker.go b/services/activity-service/internal/service/message/fanout_worker.go index 6ba8415e..36397364 100644 --- a/services/activity-service/internal/service/message/fanout_worker.go +++ b/services/activity-service/internal/service/message/fanout_worker.go @@ -57,26 +57,6 @@ type fanoutTemplateSnapshot struct { MetadataJSON json.RawMessage `json:"metadata_json"` } -// RunFanoutWorker continuously claims and processes fanout jobs until ctx is cancelled. -func (s *Service) RunFanoutWorker(ctx context.Context, options FanoutWorkerOptions) { - options = normalizeFanoutWorkerOptions(options, s.cfg.NodeID) - timer := time.NewTimer(0) - defer timer.Stop() - for { - select { - case <-ctx.Done(): - return - case <-timer.C: - processed, _ := s.ProcessNextFanoutJob(ctx, options) - delay := time.Duration(0) - if !processed { - delay = options.PollInterval - } - timer.Reset(delay) - } - } -} - // ProcessNextFanoutJob claims one job, processes one target page, then commits progress. func (s *Service) ProcessNextFanoutJob(ctx context.Context, options FanoutWorkerOptions) (bool, error) { if err := s.requireRepository(); err != nil { diff --git a/services/activity-service/internal/service/task/service.go b/services/activity-service/internal/service/task/service.go new file mode 100644 index 00000000..d91f1c2e --- /dev/null +++ b/services/activity-service/internal/service/task/service.go @@ -0,0 +1,403 @@ +package task + +import ( + "context" + "sort" + "strings" + "time" + + "google.golang.org/grpc" + walletv1 "hyapp.local/api/proto/wallet/v1" + "hyapp/pkg/appcode" + "hyapp/pkg/xerr" + taskdomain "hyapp/services/activity-service/internal/domain/task" +) + +const ( + defaultTaskTimezone = "Asia/Shanghai" + defaultTaskOffset = 8 * 60 * 60 + taskRewardAsset = "COIN" +) + +// Repository 是任务系统的唯一持久化边界;进度、领奖和事件幂等都必须落 activity MySQL。 +type Repository interface { + ListVisibleDefinitions(ctx context.Context, nowMS int64) ([]taskdomain.Definition, error) + ListUserProgress(ctx context.Context, userID int64, cycleKeys []string) ([]taskdomain.Progress, error) + ConsumeTaskEvent(ctx context.Context, event taskdomain.Event, cycleKey string, nowMS int64) (taskdomain.EventResult, error) + ListTaskDefinitions(ctx context.Context, query taskdomain.DefinitionQuery) ([]taskdomain.Definition, int64, error) + UpsertTaskDefinition(ctx context.Context, command taskdomain.DefinitionCommand, nowMS int64) (taskdomain.Definition, bool, error) + SetTaskDefinitionStatus(ctx context.Context, taskID string, status string, operatorAdminID int64, nowMS int64) (taskdomain.Definition, error) + PrepareTaskClaim(ctx context.Context, command taskdomain.ClaimCommand, nowMS int64) (taskdomain.Claim, error) + MarkTaskClaimGranted(ctx context.Context, claimID string, walletTransactionID string, grantedAtMS int64) (taskdomain.Claim, error) + MarkTaskClaimFailed(ctx context.Context, claimID string, failureReason string, nowMS int64) error +} + +// WalletClient 是 activity-service 发放金币奖励时依赖的钱包入账接口。 +type WalletClient interface { + CreditTaskReward(ctx context.Context, req *walletv1.CreditTaskRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditTaskRewardResponse, error) +} + +// Config 保存任务系统的全局口径;daily 周期只能使用服务端时区。 +type Config struct { + TaskTimezone string +} + +// Service 承载任务查询、事件消费、领奖和后台配置用例。 +type Service struct { + cfg Config + repository Repository + wallet WalletClient + now func() time.Time + location *time.Location +} + +// New 创建任务服务;时区加载失败会退回 Asia/Shanghai,避免启动后使用用户或机器本地时区。 +func New(cfg Config, repository Repository, wallet WalletClient) *Service { + tz := strings.TrimSpace(cfg.TaskTimezone) + if tz == "" { + tz = defaultTaskTimezone + } + location, err := time.LoadLocation(tz) + if err != nil { + location, _ = time.LoadLocation(defaultTaskTimezone) + } + if location == nil { + // Alpine 等精简镜像可能没有 IANA tzdata;任务日边界必须继续按产品口径 UTC+8 计算。 + location = time.FixedZone(defaultTaskTimezone, defaultTaskOffset) + } + return &Service{cfg: Config{TaskTimezone: tz}, repository: repository, wallet: wallet, now: time.Now, location: location} +} + +// SetClock 给测试注入稳定时间;生产路径始终使用 time.Now。 +func (s *Service) SetClock(now func() time.Time) { + if now != nil { + s.now = now + } +} + +// ListUserTasks 只读 task_definitions + user_task_progress,不实时扫描钱包或房间事件。 +func (s *Service) ListUserTasks(ctx context.Context, userID int64) (taskdomain.ListResult, error) { + if err := s.requireRepository(); err != nil { + return taskdomain.ListResult{}, err + } + if userID <= 0 { + return taskdomain.ListResult{}, xerr.New(xerr.InvalidArgument, "user_id is required") + } + now := s.now() + nowMS := now.UnixMilli() + cycle := s.dailyCycle(now) + definitions, err := s.repository.ListVisibleDefinitions(ctx, nowMS) + if err != nil { + return taskdomain.ListResult{}, err + } + progresses, err := s.repository.ListUserProgress(ctx, userID, []string{cycle.Key, taskdomain.CycleLifetime}) + if err != nil { + return taskdomain.ListResult{}, err + } + progressByKey := make(map[string]taskdomain.Progress, len(progresses)) + for _, progress := range progresses { + progressByKey[progressKey(progress.TaskID, progress.CycleKey)] = progress + } + + daily := taskdomain.Section{Section: taskdomain.SectionDaily, ServerTimeMS: nowMS, NextRefreshAtMS: cycle.NextRefreshMS} + exclusive := taskdomain.Section{Section: taskdomain.SectionExclusive, ServerTimeMS: nowMS, NextRefreshAtMS: cycle.NextRefreshMS} + for _, definition := range definitions { + switch definition.TaskType { + case taskdomain.TypeDaily: + daily.Items = append(daily.Items, s.itemFromDefinition(definition, progressByKey[progressKey(definition.TaskID, cycle.Key)], cycle.Key, nowMS, cycle.NextRefreshMS)) + case taskdomain.TypeExclusive: + exclusive.Items = append(exclusive.Items, s.itemFromDefinition(definition, progressByKey[progressKey(definition.TaskID, taskdomain.CycleLifetime)], taskdomain.CycleLifetime, nowMS, cycle.NextRefreshMS)) + } + } + sortTaskItems(daily.Items) + sortTaskItems(exclusive.Items) + return taskdomain.ListResult{ + Sections: []taskdomain.Section{ + daily, + exclusive, + }, + ServerTimeMS: nowMS, + NextRefreshAtMS: cycle.NextRefreshMS, + }, nil +} + +// ClaimTaskReward 校验 activity 侧完成状态后,使用 wallet-service 幂等命令发放 COIN。 +func (s *Service) ClaimTaskReward(ctx context.Context, command taskdomain.ClaimCommand) (taskdomain.Claim, error) { + if err := s.requireRepository(); err != nil { + return taskdomain.Claim{}, err + } + command.TaskID = strings.TrimSpace(command.TaskID) + command.TaskType = normalizeTaskType(command.TaskType) + command.CommandID = strings.TrimSpace(command.CommandID) + if command.UserID <= 0 || command.TaskID == "" || command.CommandID == "" { + return taskdomain.Claim{}, xerr.New(xerr.InvalidArgument, "claim command is incomplete") + } + cycle := s.dailyCycle(s.now()) + switch command.TaskType { + case taskdomain.TypeDaily: + if command.CycleKey != cycle.Key { + return taskdomain.Claim{}, xerr.New(xerr.InvalidArgument, "daily task can only claim current task_day") + } + case taskdomain.TypeExclusive: + if command.CycleKey == "" { + command.CycleKey = taskdomain.CycleLifetime + } + if command.CycleKey != taskdomain.CycleLifetime { + return taskdomain.Claim{}, xerr.New(xerr.InvalidArgument, "exclusive task cycle_key must be lifetime") + } + default: + return taskdomain.Claim{}, xerr.New(xerr.InvalidArgument, "task_type is invalid") + } + + nowMS := s.now().UnixMilli() + claim, err := s.repository.PrepareTaskClaim(ctx, command, nowMS) + if err != nil { + return taskdomain.Claim{}, err + } + if claim.Status == taskdomain.ClaimStatusGranted { + return claim, nil + } + if s.wallet == nil { + return taskdomain.Claim{}, xerr.New(xerr.Unavailable, "wallet client is not configured") + } + + reason := "daily_task_reward" + if command.TaskType == taskdomain.TypeExclusive { + reason = "exclusive_task_reward" + } + resp, err := s.wallet.CreditTaskReward(ctx, &walletv1.CreditTaskRewardRequest{ + CommandId: claim.WalletCommandID, + AppCode: appcode.FromContext(ctx), + TargetUserId: claim.UserID, + Amount: claim.RewardCoinAmount, + TaskType: claim.TaskType, + TaskId: claim.TaskID, + CycleKey: claim.CycleKey, + Reason: reason, + }) + if err != nil { + // 钱包失败时不能把任务进度改成 claimed;同一 command_id 重试会复用 claim 和 wallet_command_id。 + _ = s.repository.MarkTaskClaimFailed(ctx, claim.ClaimID, xerr.MessageOf(err), s.now().UnixMilli()) + return taskdomain.Claim{}, err + } + grantedAtMS := resp.GetGrantedAtMs() + if grantedAtMS <= 0 { + grantedAtMS = s.now().UnixMilli() + } + return s.repository.MarkTaskClaimGranted(ctx, claim.ClaimID, resp.GetTransactionId(), grantedAtMS) +} + +// ConsumeTaskEvent 把服务端事实事件归属到 occurred_at_ms 所在的任务周期。 +func (s *Service) ConsumeTaskEvent(ctx context.Context, event taskdomain.Event) (taskdomain.EventResult, error) { + if err := s.requireRepository(); err != nil { + return taskdomain.EventResult{}, err + } + event.EventID = strings.TrimSpace(event.EventID) + event.EventType = strings.TrimSpace(event.EventType) + event.SourceService = strings.TrimSpace(event.SourceService) + event.MetricType = strings.TrimSpace(event.MetricType) + if event.EventID == "" || event.EventType == "" || event.SourceService == "" || event.UserID <= 0 || event.MetricType == "" || event.Value <= 0 { + return taskdomain.EventResult{}, xerr.New(xerr.InvalidArgument, "task event is incomplete") + } + if event.OccurredAtMS <= 0 { + event.OccurredAtMS = s.now().UnixMilli() + } + cycleKey := s.dailyCycle(time.UnixMilli(event.OccurredAtMS)).Key + return s.repository.ConsumeTaskEvent(ctx, event, cycleKey, s.now().UnixMilli()) +} + +// ListTaskDefinitions 返回后台任务配置列表;状态筛选不影响 App 查询口径。 +func (s *Service) ListTaskDefinitions(ctx context.Context, query taskdomain.DefinitionQuery) ([]taskdomain.Definition, int64, error) { + if err := s.requireRepository(); err != nil { + return nil, 0, err + } + query.TaskType = normalizeTaskType(query.TaskType) + query.Category = strings.TrimSpace(query.Category) + query.Status = strings.TrimSpace(query.Status) + query.Keyword = strings.TrimSpace(query.Keyword) + if query.Page <= 0 { + query.Page = 1 + } + if query.PageSize <= 0 { + query.PageSize = 20 + } + if query.PageSize > 100 { + query.PageSize = 100 + } + return s.repository.ListTaskDefinitions(ctx, query) +} + +// UpsertTaskDefinition 创建或编辑后台任务配置;关键领奖字段变更由 repository 生成版本快照。 +func (s *Service) UpsertTaskDefinition(ctx context.Context, command taskdomain.DefinitionCommand) (taskdomain.Definition, bool, error) { + if err := s.requireRepository(); err != nil { + return taskdomain.Definition{}, false, err + } + command = normalizeDefinitionCommand(command) + if err := validateDefinitionCommand(command); err != nil { + return taskdomain.Definition{}, false, err + } + return s.repository.UpsertTaskDefinition(ctx, command, s.now().UnixMilli()) +} + +// SetTaskDefinitionStatus 只做状态流转,不删除历史进度和领奖事实。 +func (s *Service) SetTaskDefinitionStatus(ctx context.Context, taskID string, status string, operatorAdminID int64) (taskdomain.Definition, error) { + if err := s.requireRepository(); err != nil { + return taskdomain.Definition{}, err + } + taskID = strings.TrimSpace(taskID) + status = strings.TrimSpace(status) + if taskID == "" || !validDefinitionStatus(status) || operatorAdminID <= 0 { + return taskdomain.Definition{}, xerr.New(xerr.InvalidArgument, "task status command is invalid") + } + return s.repository.SetTaskDefinitionStatus(ctx, taskID, status, operatorAdminID, s.now().UnixMilli()) +} + +func (s *Service) requireRepository() error { + if s == nil || s.repository == nil { + return xerr.New(xerr.Unavailable, "task repository is not configured") + } + return nil +} + +func (s *Service) itemFromDefinition(def taskdomain.Definition, progress taskdomain.Progress, cycleKey string, nowMS int64, nextRefreshMS int64) taskdomain.Item { + status := taskdomain.StatusInProgress + progressValue := int64(0) + if progress.TaskID != "" { + status = progress.Status + progressValue = progress.ProgressValue + } + if status == "" { + status = taskdomain.StatusInProgress + } + return taskdomain.Item{ + Definition: def, + ProgressValue: progressValue, + UserStatus: status, + Claimable: status == taskdomain.StatusCompleted, + TaskDay: cycleKey, + ServerTimeMS: nowMS, + NextRefreshAtMS: nextRefreshMS, + } +} + +type dailyCycle struct { + Key string + NextRefreshMS int64 +} + +func (s *Service) dailyCycle(t time.Time) dailyCycle { + location := s.location + if location == nil { + location, _ = time.LoadLocation(defaultTaskTimezone) + } + if location == nil { + location = time.FixedZone(defaultTaskTimezone, defaultTaskOffset) + } + local := t.In(location) + start := time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, location) + next := start.AddDate(0, 0, 1) + return dailyCycle{Key: start.Format("2006-01-02"), NextRefreshMS: next.UnixMilli()} +} + +func progressKey(taskID string, cycleKey string) string { + return taskID + "\x00" + cycleKey +} + +func sortTaskItems(items []taskdomain.Item) { + sort.SliceStable(items, func(i, j int) bool { + leftRank := taskStatusRank(items[i]) + rightRank := taskStatusRank(items[j]) + if leftRank != rightRank { + return leftRank < rightRank + } + if items[i].SortOrder != items[j].SortOrder { + return items[i].SortOrder < items[j].SortOrder + } + return items[i].TaskID < items[j].TaskID + }) +} + +func taskStatusRank(item taskdomain.Item) int { + if item.Claimable { + return 0 + } + switch item.UserStatus { + case taskdomain.StatusCompleted: + return 1 + case taskdomain.StatusInProgress: + return 2 + case taskdomain.StatusClaimed: + return 3 + default: + return 4 + } +} + +func normalizeDefinitionCommand(command taskdomain.DefinitionCommand) taskdomain.DefinitionCommand { + command.TaskID = strings.TrimSpace(command.TaskID) + command.TaskType = normalizeTaskType(command.TaskType) + command.Category = strings.ToLower(strings.TrimSpace(command.Category)) + command.MetricType = strings.ToLower(strings.TrimSpace(command.MetricType)) + command.Title = strings.TrimSpace(command.Title) + command.Description = strings.TrimSpace(command.Description) + command.TargetUnit = strings.ToLower(strings.TrimSpace(command.TargetUnit)) + command.Status = strings.TrimSpace(command.Status) + if command.Status == "" { + command.Status = taskdomain.StatusDraft + } + return command +} + +func normalizeTaskType(value string) string { + switch strings.ToLower(strings.TrimSpace(value)) { + case taskdomain.TypeDaily: + return taskdomain.TypeDaily + case taskdomain.TypeExclusive: + return taskdomain.TypeExclusive + default: + return strings.ToLower(strings.TrimSpace(value)) + } +} + +func validateDefinitionCommand(command taskdomain.DefinitionCommand) error { + if command.TaskType != taskdomain.TypeDaily && command.TaskType != taskdomain.TypeExclusive { + return xerr.New(xerr.InvalidArgument, "task_type is invalid") + } + if command.Category == "" || command.MetricType == "" || command.Title == "" { + return xerr.New(xerr.InvalidArgument, "task category, metric_type and title are required") + } + if command.TargetValue <= 0 || command.RewardCoinAmount <= 0 { + return xerr.New(xerr.InvalidArgument, "target_value and reward_coin_amount must be positive") + } + if !validTargetUnit(command.TargetUnit) { + return xerr.New(xerr.InvalidArgument, "target_unit is invalid") + } + if !validDefinitionStatus(command.Status) { + return xerr.New(xerr.InvalidArgument, "task status is invalid") + } + if command.EffectiveToMS > 0 && command.EffectiveFromMS > 0 && command.EffectiveToMS <= command.EffectiveFromMS { + return xerr.New(xerr.InvalidArgument, "effective_to_ms must be after effective_from_ms") + } + if command.OperatorAdminID <= 0 { + return xerr.New(xerr.InvalidArgument, "operator_admin_id is required") + } + return nil +} + +func validTargetUnit(value string) bool { + switch value { + case "count", "minute", "coin": + return true + default: + return false + } +} + +func validDefinitionStatus(value string) bool { + switch value { + case taskdomain.StatusDraft, taskdomain.StatusActive, taskdomain.StatusPaused, taskdomain.StatusArchived: + return true + default: + return false + } +} diff --git a/services/activity-service/internal/service/task/service_test.go b/services/activity-service/internal/service/task/service_test.go new file mode 100644 index 00000000..94f6fcdf --- /dev/null +++ b/services/activity-service/internal/service/task/service_test.go @@ -0,0 +1,116 @@ +package task_test + +import ( + "context" + "testing" + "time" + + "google.golang.org/grpc" + walletv1 "hyapp.local/api/proto/wallet/v1" + taskdomain "hyapp/services/activity-service/internal/domain/task" + taskservice "hyapp/services/activity-service/internal/service/task" + "hyapp/services/activity-service/internal/testutil/mysqltest" +) + +// TestTaskEventQueryAndClaimFlow 覆盖任务定义、事件进度、App 查询、领奖和领奖幂等的主闭环。 +func TestTaskEventQueryAndClaimFlow(t *testing.T) { + repository := mysqltest.NewRepository(t) + wallet := &fakeWalletClient{} + svc := taskservice.New(taskservice.Config{TaskTimezone: "Asia/Shanghai"}, repository, wallet) + now := time.Date(2026, 5, 9, 10, 0, 0, 0, time.FixedZone("CST", 8*60*60)) + svc.SetClock(func() time.Time { return now }) + + definition, created, err := svc.UpsertTaskDefinition(context.Background(), taskdomain.DefinitionCommand{ + TaskType: taskdomain.TypeDaily, + Category: "gift", + MetricType: "gift_spend_coin", + Title: "send gift", + TargetValue: 100, + TargetUnit: "coin", + RewardCoinAmount: 10, + Status: taskdomain.StatusActive, + SortOrder: 1, + OperatorAdminID: 90001, + }) + if err != nil || !created { + t.Fatalf("create task definition failed: created=%v err=%v", created, err) + } + + result, err := svc.ConsumeTaskEvent(context.Background(), taskdomain.Event{ + EventID: "evt-gift-1", + EventType: "WalletGiftDebited", + SourceService: "wallet", + UserID: 10001, + MetricType: "gift_spend_coin", + Value: 120, + OccurredAtMS: now.UnixMilli(), + }) + if err != nil { + t.Fatalf("ConsumeTaskEvent failed: %v", err) + } + if result.Status != taskdomain.EventStatusConsumed || result.MatchedTaskCount != 1 { + t.Fatalf("event result mismatch: %+v", result) + } + duplicate, err := svc.ConsumeTaskEvent(context.Background(), taskdomain.Event{ + EventID: "evt-gift-1", + EventType: "WalletGiftDebited", + SourceService: "wallet", + UserID: 10001, + MetricType: "gift_spend_coin", + Value: 120, + OccurredAtMS: now.UnixMilli(), + }) + if err != nil || duplicate.Status != taskdomain.EventStatusConsumed { + t.Fatalf("duplicate event should be acknowledged: result=%+v err=%v", duplicate, err) + } + + list, err := svc.ListUserTasks(context.Background(), 10001) + if err != nil { + t.Fatalf("ListUserTasks failed: %v", err) + } + daily := list.Sections[0] + if len(daily.Items) != 1 || daily.Items[0].TaskID != definition.TaskID || daily.Items[0].ProgressValue != 120 || daily.Items[0].UserStatus != taskdomain.StatusCompleted || !daily.Items[0].Claimable { + t.Fatalf("daily task item mismatch: %+v", daily.Items) + } + + claim, err := svc.ClaimTaskReward(context.Background(), taskdomain.ClaimCommand{ + UserID: 10001, + TaskID: definition.TaskID, + TaskType: taskdomain.TypeDaily, + CycleKey: "2026-05-09", + CommandID: "cmd-claim-1", + }) + if err != nil { + t.Fatalf("ClaimTaskReward failed: %v", err) + } + if claim.Status != taskdomain.ClaimStatusGranted || claim.WalletTransactionID != "wtx-task-1" || wallet.calls != 1 { + t.Fatalf("claim result mismatch: claim=%+v wallet_calls=%d", claim, wallet.calls) + } + again, err := svc.ClaimTaskReward(context.Background(), taskdomain.ClaimCommand{ + UserID: 10001, + TaskID: definition.TaskID, + TaskType: taskdomain.TypeDaily, + CycleKey: "2026-05-09", + CommandID: "cmd-claim-1", + }) + if err != nil { + t.Fatalf("ClaimTaskReward retry failed: %v", err) + } + if again.ClaimID != claim.ClaimID || wallet.calls != 1 { + t.Fatalf("claim idempotency mismatch: first=%+v second=%+v calls=%d", claim, again, wallet.calls) + } +} + +type fakeWalletClient struct { + calls int +} + +func (f *fakeWalletClient) CreditTaskReward(context.Context, *walletv1.CreditTaskRewardRequest, ...grpc.CallOption) (*walletv1.CreditTaskRewardResponse, error) { + f.calls++ + return &walletv1.CreditTaskRewardResponse{ + TransactionId: "wtx-task-1", + Amount: 10, + GrantedAtMs: 1778292000000, + Balance: &walletv1.AssetBalance{AssetType: "COIN", AvailableAmount: 10}, + }, nil +} diff --git a/services/activity-service/internal/service/task/service_timezone_test.go b/services/activity-service/internal/service/task/service_timezone_test.go new file mode 100644 index 00000000..fe5b45cd --- /dev/null +++ b/services/activity-service/internal/service/task/service_timezone_test.go @@ -0,0 +1,14 @@ +package task + +import ( + "testing" + "time" +) + +func TestDailyCycleFallsBackWhenLocationMissing(t *testing.T) { + svc := &Service{} + cycle := svc.dailyCycle(time.Date(2026, 5, 8, 16, 30, 0, 0, time.UTC)) + if cycle.Key != "2026-05-09" { + t.Fatalf("daily cycle must use UTC+8 fallback when tzdata is missing: %+v", cycle) + } +} diff --git a/services/activity-service/internal/storage/mysql/message_repository.go b/services/activity-service/internal/storage/mysql/message_repository.go index 747e0004..3cea559d 100644 --- a/services/activity-service/internal/storage/mysql/message_repository.go +++ b/services/activity-service/internal/storage/mysql/message_repository.go @@ -338,11 +338,13 @@ func (r *Repository) ClaimFanoutJob(ctx context.Context, workerID string, nowMS CAST(template_snapshot_json AS CHAR), status, attempt_count, cursor_user_id, total_count, success_count, failure_count, batch_size, next_run_at_ms, locked_by, locked_until_ms, error_message, created_by, created_at_ms, updated_at_ms FROM message_fanout_jobs - WHERE next_run_at_ms <= ? AND attempt_count < ? + WHERE app_code = ? + AND next_run_at_ms <= ? AND attempt_count < ? AND (status IN (?, ?) OR (status = ? AND locked_until_ms < ?)) ORDER BY next_run_at_ms ASC, updated_at_ms ASC, job_id ASC LIMIT 1 FOR UPDATE SKIP LOCKED`, + appcode.FromContext(ctx), nowMS, maxRetry, messagedomain.FanoutStatusPending, diff --git a/services/activity-service/internal/storage/mysql/task_repository.go b/services/activity-service/internal/storage/mysql/task_repository.go new file mode 100644 index 00000000..f64f67ef --- /dev/null +++ b/services/activity-service/internal/storage/mysql/task_repository.go @@ -0,0 +1,792 @@ +package mysql + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "strings" + + "hyapp/pkg/appcode" + "hyapp/pkg/idgen" + "hyapp/pkg/xerr" + taskdomain "hyapp/services/activity-service/internal/domain/task" +) + +// ListVisibleDefinitions 返回 App 当前可见任务;查询路径不初始化用户进度,避免 daily 刷新产生批量写。 +func (r *Repository) ListVisibleDefinitions(ctx context.Context, nowMS int64) ([]taskdomain.Definition, error) { + if r == nil || r.db == nil { + return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + rows, err := r.db.QueryContext(ctx, ` + SELECT app_code, task_id, task_type, category, metric_type, title, description, + target_value, target_unit, reward_coin_amount, status, sort_order, version, + current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id, + updated_by_admin_id, created_at_ms, updated_at_ms + FROM task_definitions + WHERE app_code = ? AND status = 'active' + AND (effective_from_ms = 0 OR effective_from_ms <= ?) + AND (effective_to_ms = 0 OR effective_to_ms > ?) + ORDER BY sort_order ASC, task_id ASC`, + appcode.FromContext(ctx), nowMS, nowMS, + ) + if err != nil { + return nil, err + } + defer rows.Close() + return scanDefinitions(rows) +} + +// ListUserProgress 批量读取当前用户 daily/lifetime 进度,缺失进度由 service 按 0 渲染。 +func (r *Repository) ListUserProgress(ctx context.Context, userID int64, cycleKeys []string) ([]taskdomain.Progress, error) { + if r == nil || r.db == nil { + return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + cycleKeys = normalizeCycleKeys(cycleKeys) + if len(cycleKeys) == 0 { + return nil, nil + } + placeholders := strings.TrimRight(strings.Repeat("?,", len(cycleKeys)), ",") + args := []any{appcode.FromContext(ctx), userID} + for _, cycleKey := range cycleKeys { + args = append(args, cycleKey) + } + rows, err := r.db.QueryContext(ctx, ` + SELECT app_code, user_id, task_id, task_version_id, cycle_key, progress_value, + target_value, reward_coin_amount, status, completed_at_ms, claimed_at_ms, updated_at_ms + FROM user_task_progress + WHERE app_code = ? AND user_id = ? AND cycle_key IN (`+placeholders+`)`, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + progresses := make([]taskdomain.Progress, 0) + for rows.Next() { + progress, err := scanProgress(rows) + if err != nil { + return nil, err + } + progresses = append(progresses, progress) + } + if err := rows.Err(); err != nil { + return nil, err + } + return progresses, nil +} + +// ConsumeTaskEvent 在单事务内完成事件幂等、任务匹配和用户进度累加。 +func (r *Repository) ConsumeTaskEvent(ctx context.Context, event taskdomain.Event, cycleKey string, nowMS int64) (taskdomain.EventResult, error) { + if r == nil || r.db == nil { + return taskdomain.EventResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return taskdomain.EventResult{}, err + } + defer func() { _ = tx.Rollback() }() + + inserted, existingStatus, err := r.insertTaskEventConsumption(ctx, tx, event, cycleKey, nowMS) + if err != nil { + return taskdomain.EventResult{}, err + } + if !inserted { + return taskdomain.EventResult{EventID: event.EventID, Status: existingStatus}, nil + } + + definitions, err := r.listDefinitionsForEvent(ctx, tx, event.MetricType, event.OccurredAtMS) + if err != nil { + return taskdomain.EventResult{}, err + } + matched := int32(0) + for _, definition := range definitions { + progressCycle := cycleKey + if definition.TaskType == taskdomain.TypeExclusive { + progressCycle = taskdomain.CycleLifetime + } + changed, err := r.applyTaskProgressDelta(ctx, tx, event.UserID, definition, progressCycle, event.Value, nowMS) + if err != nil { + return taskdomain.EventResult{}, err + } + if changed { + matched++ + } + } + status := taskdomain.EventStatusConsumed + skipReason := "" + if matched == 0 { + status = taskdomain.EventStatusSkipped + skipReason = "no_matching_task" + } + if _, err := tx.ExecContext(ctx, ` + UPDATE task_event_consumption + SET status = ?, skip_reason = ?, consumed_at_ms = ? + WHERE app_code = ? AND event_id = ?`, + status, skipReason, nowMS, appcode.FromContext(ctx), event.EventID, + ); err != nil { + return taskdomain.EventResult{}, err + } + if err := tx.Commit(); err != nil { + return taskdomain.EventResult{}, err + } + return taskdomain.EventResult{EventID: event.EventID, Status: status, MatchedTaskCount: matched}, nil +} + +// ListTaskDefinitions 是后台任务配置分页查询;keyword 只匹配任务 ID、标题和指标,避免扫描大 JSON。 +func (r *Repository) ListTaskDefinitions(ctx context.Context, query taskdomain.DefinitionQuery) ([]taskdomain.Definition, int64, error) { + if r == nil || r.db == nil { + return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + where, args := taskDefinitionWhere(ctx, query) + var total int64 + if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM task_definitions `+where, args...).Scan(&total); err != nil { + return nil, 0, err + } + pageSize := normalizePageSize(query.PageSize) + page := query.Page + if page <= 0 { + page = 1 + } + args = append(args, pageSize, (page-1)*pageSize) + rows, err := r.db.QueryContext(ctx, ` + SELECT app_code, task_id, task_type, category, metric_type, title, description, + target_value, target_unit, reward_coin_amount, status, sort_order, version, + current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id, + updated_by_admin_id, created_at_ms, updated_at_ms + FROM task_definitions `+where+` + ORDER BY sort_order ASC, updated_at_ms DESC, task_id ASC + LIMIT ? OFFSET ?`, args...) + if err != nil { + return nil, 0, err + } + defer rows.Close() + definitions, err := scanDefinitions(rows) + if err != nil { + return nil, 0, err + } + return definitions, total, nil +} + +// UpsertTaskDefinition 创建或更新任务定义;领奖关键字段变更时写新版本快照。 +func (r *Repository) UpsertTaskDefinition(ctx context.Context, command taskdomain.DefinitionCommand, nowMS int64) (taskdomain.Definition, bool, error) { + if r == nil || r.db == nil { + return taskdomain.Definition{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return taskdomain.Definition{}, false, err + } + defer func() { _ = tx.Rollback() }() + + if strings.TrimSpace(command.TaskID) == "" { + definition, err := r.createTaskDefinition(ctx, tx, command, nowMS) + if err != nil { + return taskdomain.Definition{}, false, err + } + if err := tx.Commit(); err != nil { + return taskdomain.Definition{}, false, err + } + return definition, true, nil + } + + current, err := r.getTaskDefinitionForUpdate(ctx, tx, command.TaskID) + if err != nil { + return taskdomain.Definition{}, false, err + } + next := current + next.TaskType = command.TaskType + next.Category = command.Category + next.MetricType = command.MetricType + next.Title = command.Title + next.Description = command.Description + next.TargetValue = command.TargetValue + next.TargetUnit = command.TargetUnit + next.RewardCoinAmount = command.RewardCoinAmount + next.Status = command.Status + next.SortOrder = command.SortOrder + next.EffectiveFromMS = command.EffectiveFromMS + next.EffectiveToMS = command.EffectiveToMS + next.UpdatedByAdminID = command.OperatorAdminID + next.UpdatedAtMS = nowMS + if taskDefinitionNeedsVersion(current, next) { + next.Version = current.Version + 1 + versionID, err := r.insertTaskDefinitionVersion(ctx, tx, next, nowMS) + if err != nil { + return taskdomain.Definition{}, false, err + } + next.CurrentVersionID = versionID + } + if _, err := tx.ExecContext(ctx, ` + UPDATE task_definitions + SET task_type = ?, category = ?, metric_type = ?, title = ?, description = ?, + target_value = ?, target_unit = ?, reward_coin_amount = ?, status = ?, sort_order = ?, + version = ?, current_task_version_id = ?, effective_from_ms = ?, effective_to_ms = ?, + updated_by_admin_id = ?, updated_at_ms = ? + WHERE app_code = ? AND task_id = ?`, + next.TaskType, next.Category, next.MetricType, next.Title, next.Description, + next.TargetValue, next.TargetUnit, next.RewardCoinAmount, next.Status, next.SortOrder, + next.Version, next.CurrentVersionID, next.EffectiveFromMS, next.EffectiveToMS, + next.UpdatedByAdminID, next.UpdatedAtMS, appcode.FromContext(ctx), next.TaskID, + ); err != nil { + return taskdomain.Definition{}, false, err + } + if err := tx.Commit(); err != nil { + return taskdomain.Definition{}, false, err + } + return next, false, nil +} + +// SetTaskDefinitionStatus 更新后台状态;archived 也只是不再展示,不删除历史事实。 +func (r *Repository) SetTaskDefinitionStatus(ctx context.Context, taskID string, status string, operatorAdminID int64, nowMS int64) (taskdomain.Definition, error) { + if r == nil || r.db == nil { + return taskdomain.Definition{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + result, err := r.db.ExecContext(ctx, ` + UPDATE task_definitions + SET status = ?, updated_by_admin_id = ?, updated_at_ms = ? + WHERE app_code = ? AND task_id = ?`, + status, operatorAdminID, nowMS, appcode.FromContext(ctx), taskID, + ) + if err != nil { + return taskdomain.Definition{}, err + } + affected, err := result.RowsAffected() + if err != nil { + return taskdomain.Definition{}, err + } + if affected == 0 { + return taskdomain.Definition{}, xerr.New(xerr.NotFound, "task definition not found") + } + return r.getTaskDefinition(ctx, taskID) +} + +// PrepareTaskClaim 锁定完成进度并创建 pending claim;钱包调用在事务外执行。 +func (r *Repository) PrepareTaskClaim(ctx context.Context, command taskdomain.ClaimCommand, nowMS int64) (taskdomain.Claim, error) { + if r == nil || r.db == nil { + return taskdomain.Claim{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return taskdomain.Claim{}, err + } + defer func() { _ = tx.Rollback() }() + + if existing, exists, err := r.getClaimByCommandForUpdate(ctx, tx, command.CommandID); err != nil || exists { + if err != nil { + return taskdomain.Claim{}, err + } + if existing.UserID != command.UserID || existing.TaskID != command.TaskID || existing.TaskType != command.TaskType || existing.CycleKey != command.CycleKey { + return taskdomain.Claim{}, xerr.New(xerr.RequestConflict, "claim command payload conflicts") + } + if err := tx.Commit(); err != nil { + return taskdomain.Claim{}, err + } + return existing, nil + } + + if existing, exists, err := r.getClaimByTaskCycleForUpdate(ctx, tx, command.UserID, command.TaskID, command.CycleKey); err != nil || exists { + if err != nil { + return taskdomain.Claim{}, err + } + if existing.Status == taskdomain.ClaimStatusGranted { + if err := tx.Commit(); err != nil { + return taskdomain.Claim{}, err + } + return existing, nil + } + return taskdomain.Claim{}, xerr.New(xerr.RewardPending, "task reward claim is pending") + } + + definition, err := r.getTaskDefinitionForUpdate(ctx, tx, command.TaskID) + if err != nil { + return taskdomain.Claim{}, err + } + if definition.TaskType != command.TaskType { + return taskdomain.Claim{}, xerr.New(xerr.InvalidArgument, "task_type does not match task definition") + } + progress, err := r.getProgressForUpdate(ctx, tx, command.UserID, command.TaskID, command.CycleKey) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return taskdomain.Claim{}, xerr.New(xerr.Conflict, "task is not completed") + } + return taskdomain.Claim{}, err + } + if progress.Status == taskdomain.StatusClaimed { + return taskdomain.Claim{}, xerr.New(xerr.Conflict, "task reward already claimed") + } + if progress.Status != taskdomain.StatusCompleted { + return taskdomain.Claim{}, xerr.New(xerr.Conflict, "task is not completed") + } + + claim := taskdomain.Claim{ + ClaimID: idgen.New("tclaim"), + CommandID: command.CommandID, + UserID: command.UserID, + TaskID: command.TaskID, + TaskType: command.TaskType, + CycleKey: command.CycleKey, + RewardCoinAmount: progress.RewardCoinAmount, + Status: taskdomain.ClaimStatusPending, + CreatedAtMS: nowMS, + UpdatedAtMS: nowMS, + } + claim.WalletCommandID = walletTaskCommandID(claim.ClaimID) + if _, err := tx.ExecContext(ctx, ` + INSERT INTO task_reward_claims ( + app_code, claim_id, command_id, user_id, task_id, task_type, cycle_key, + reward_coin_amount, wallet_command_id, status, failure_reason, created_at_ms, updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, ?)`, + appcode.FromContext(ctx), claim.ClaimID, claim.CommandID, claim.UserID, claim.TaskID, claim.TaskType, + claim.CycleKey, claim.RewardCoinAmount, claim.WalletCommandID, claim.Status, claim.CreatedAtMS, claim.UpdatedAtMS, + ); err != nil { + return taskdomain.Claim{}, err + } + if err := tx.Commit(); err != nil { + return taskdomain.Claim{}, err + } + return claim, nil +} + +// MarkTaskClaimGranted 把钱包成功回执和用户任务 claimed 状态放在同一 activity 事务里。 +func (r *Repository) MarkTaskClaimGranted(ctx context.Context, claimID string, walletTransactionID string, grantedAtMS int64) (taskdomain.Claim, error) { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return taskdomain.Claim{}, err + } + defer func() { _ = tx.Rollback() }() + + claim, err := r.getClaimByIDForUpdate(ctx, tx, claimID) + if err != nil { + return taskdomain.Claim{}, err + } + if claim.Status == taskdomain.ClaimStatusGranted { + if err := tx.Commit(); err != nil { + return taskdomain.Claim{}, err + } + return claim, nil + } + if _, err := tx.ExecContext(ctx, ` + UPDATE task_reward_claims + SET status = 'granted', wallet_transaction_id = ?, failure_reason = '', updated_at_ms = ? + WHERE app_code = ? AND claim_id = ?`, + walletTransactionID, grantedAtMS, appcode.FromContext(ctx), claimID, + ); err != nil { + return taskdomain.Claim{}, err + } + if _, err := tx.ExecContext(ctx, ` + UPDATE user_task_progress + SET status = 'claimed', claimed_at_ms = ?, updated_at_ms = ? + WHERE app_code = ? AND user_id = ? AND task_id = ? AND cycle_key = ?`, + grantedAtMS, grantedAtMS, appcode.FromContext(ctx), claim.UserID, claim.TaskID, claim.CycleKey, + ); err != nil { + return taskdomain.Claim{}, err + } + if err := tx.Commit(); err != nil { + return taskdomain.Claim{}, err + } + claim.Status = taskdomain.ClaimStatusGranted + claim.WalletTransactionID = walletTransactionID + claim.UpdatedAtMS = grantedAtMS + return claim, nil +} + +// MarkTaskClaimFailed 记录钱包失败原因但不修改 progress,用户后续可用同 command_id 重试。 +func (r *Repository) MarkTaskClaimFailed(ctx context.Context, claimID string, failureReason string, nowMS int64) error { + if r == nil || r.db == nil { + return xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + _, err := r.db.ExecContext(ctx, ` + UPDATE task_reward_claims + SET status = 'failed', failure_reason = ?, updated_at_ms = ? + WHERE app_code = ? AND claim_id = ? AND status <> 'granted'`, + truncateTaskFailure(failureReason), nowMS, appcode.FromContext(ctx), claimID, + ) + return err +} + +func (r *Repository) insertTaskEventConsumption(ctx context.Context, tx *sql.Tx, event taskdomain.Event, cycleKey string, nowMS int64) (bool, string, error) { + _, err := tx.ExecContext(ctx, ` + INSERT INTO task_event_consumption ( + app_code, event_id, event_type, source_service, user_id, task_day, status, + skip_reason, created_at_ms, consumed_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, 'consumed', '', ?, 0)`, + appcode.FromContext(ctx), event.EventID, event.EventType, event.SourceService, event.UserID, cycleKey, nowMS, + ) + if err == nil { + return true, "", nil + } + if !isMySQLDuplicate(err) { + return false, "", err + } + var status string + if err := tx.QueryRowContext(ctx, ` + SELECT status FROM task_event_consumption + WHERE app_code = ? AND event_id = ?`, + appcode.FromContext(ctx), event.EventID, + ).Scan(&status); err != nil { + return false, "", err + } + return false, status, nil +} + +func (r *Repository) listDefinitionsForEvent(ctx context.Context, tx *sql.Tx, metricType string, occurredAtMS int64) ([]taskdomain.Definition, error) { + rows, err := tx.QueryContext(ctx, ` + SELECT app_code, task_id, task_type, category, metric_type, title, description, + target_value, target_unit, reward_coin_amount, status, sort_order, version, + current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id, + updated_by_admin_id, created_at_ms, updated_at_ms + FROM task_definitions + WHERE app_code = ? AND status = 'active' AND metric_type = ? + AND (effective_from_ms = 0 OR effective_from_ms <= ?) + AND (effective_to_ms = 0 OR effective_to_ms > ?) + ORDER BY sort_order ASC, task_id ASC + FOR UPDATE`, + appcode.FromContext(ctx), metricType, occurredAtMS, occurredAtMS, + ) + if err != nil { + return nil, err + } + defer rows.Close() + return scanDefinitions(rows) +} + +func (r *Repository) applyTaskProgressDelta(ctx context.Context, tx *sql.Tx, userID int64, definition taskdomain.Definition, cycleKey string, delta int64, nowMS int64) (bool, error) { + progress, err := r.getProgressForUpdate(ctx, tx, userID, definition.TaskID, cycleKey) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return false, err + } + status := taskdomain.StatusInProgress + completedAtMS := int64(0) + if delta >= definition.TargetValue { + status = taskdomain.StatusCompleted + completedAtMS = nowMS + } + _, err := tx.ExecContext(ctx, ` + INSERT INTO user_task_progress ( + app_code, user_id, task_id, task_version_id, cycle_key, progress_value, + target_value, reward_coin_amount, status, completed_at_ms, claimed_at_ms, updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`, + appcode.FromContext(ctx), userID, definition.TaskID, definition.CurrentVersionID, cycleKey, delta, + definition.TargetValue, definition.RewardCoinAmount, status, completedAtMS, nowMS, + ) + return err == nil, err + } + if definition.TaskType == taskdomain.TypeExclusive && (progress.Status == taskdomain.StatusCompleted || progress.Status == taskdomain.StatusClaimed) { + return false, nil + } + nextValue := progress.ProgressValue + delta + nextStatus := progress.Status + completedAtMS := progress.CompletedAtMS + if nextStatus == taskdomain.StatusInProgress && nextValue >= progress.TargetValue { + nextStatus = taskdomain.StatusCompleted + completedAtMS = nowMS + } + _, err = tx.ExecContext(ctx, ` + UPDATE user_task_progress + SET progress_value = ?, status = ?, completed_at_ms = ?, updated_at_ms = ? + WHERE app_code = ? AND user_id = ? AND task_id = ? AND cycle_key = ?`, + nextValue, nextStatus, completedAtMS, nowMS, appcode.FromContext(ctx), userID, definition.TaskID, cycleKey, + ) + return err == nil, err +} + +func (r *Repository) createTaskDefinition(ctx context.Context, tx *sql.Tx, command taskdomain.DefinitionCommand, nowMS int64) (taskdomain.Definition, error) { + definition := taskdomain.Definition{ + AppCode: appcode.FromContext(ctx), + TaskID: idgen.New("task"), + TaskType: command.TaskType, + Category: command.Category, + MetricType: command.MetricType, + Title: command.Title, + Description: command.Description, + TargetValue: command.TargetValue, + TargetUnit: command.TargetUnit, + RewardCoinAmount: command.RewardCoinAmount, + Status: command.Status, + SortOrder: command.SortOrder, + Version: 1, + EffectiveFromMS: command.EffectiveFromMS, + EffectiveToMS: command.EffectiveToMS, + CreatedByAdminID: command.OperatorAdminID, + UpdatedByAdminID: command.OperatorAdminID, + CreatedAtMS: nowMS, + UpdatedAtMS: nowMS, + } + versionID, err := r.insertTaskDefinitionVersion(ctx, tx, definition, nowMS) + if err != nil { + return taskdomain.Definition{}, err + } + definition.CurrentVersionID = versionID + _, err = tx.ExecContext(ctx, ` + INSERT INTO task_definitions ( + app_code, task_id, task_type, category, metric_type, title, description, + target_value, target_unit, reward_coin_amount, status, sort_order, version, + current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id, + updated_by_admin_id, created_at_ms, updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + definition.AppCode, definition.TaskID, definition.TaskType, definition.Category, definition.MetricType, + definition.Title, definition.Description, definition.TargetValue, definition.TargetUnit, + definition.RewardCoinAmount, definition.Status, definition.SortOrder, definition.Version, + definition.CurrentVersionID, definition.EffectiveFromMS, definition.EffectiveToMS, + definition.CreatedByAdminID, definition.UpdatedByAdminID, definition.CreatedAtMS, definition.UpdatedAtMS, + ) + return definition, err +} + +func (r *Repository) insertTaskDefinitionVersion(ctx context.Context, tx *sql.Tx, definition taskdomain.Definition, nowMS int64) (int64, error) { + payload, err := json.Marshal(definition) + if err != nil { + return 0, err + } + effectiveCycle := taskdomain.CycleLifetime + if definition.TaskType == taskdomain.TypeDaily { + effectiveCycle = "current" + } + result, err := tx.ExecContext(ctx, ` + INSERT INTO task_definition_versions ( + app_code, task_id, version, snapshot_json, effective_from_cycle, created_at_ms + ) VALUES (?, ?, ?, ?, ?, ?)`, + appcode.FromContext(ctx), definition.TaskID, definition.Version, string(payload), effectiveCycle, nowMS, + ) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +func (r *Repository) getTaskDefinition(ctx context.Context, taskID string) (taskdomain.Definition, error) { + row := r.db.QueryRowContext(ctx, ` + SELECT app_code, task_id, task_type, category, metric_type, title, description, + target_value, target_unit, reward_coin_amount, status, sort_order, version, + current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id, + updated_by_admin_id, created_at_ms, updated_at_ms + FROM task_definitions + WHERE app_code = ? AND task_id = ?`, + appcode.FromContext(ctx), taskID, + ) + return scanDefinition(row) +} + +func (r *Repository) getTaskDefinitionForUpdate(ctx context.Context, tx *sql.Tx, taskID string) (taskdomain.Definition, error) { + row := tx.QueryRowContext(ctx, ` + SELECT app_code, task_id, task_type, category, metric_type, title, description, + target_value, target_unit, reward_coin_amount, status, sort_order, version, + current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id, + updated_by_admin_id, created_at_ms, updated_at_ms + FROM task_definitions + WHERE app_code = ? AND task_id = ? + FOR UPDATE`, + appcode.FromContext(ctx), taskID, + ) + definition, err := scanDefinition(row) + if errors.Is(err, sql.ErrNoRows) { + return taskdomain.Definition{}, xerr.New(xerr.NotFound, "task definition not found") + } + return definition, err +} + +func (r *Repository) getProgressForUpdate(ctx context.Context, tx *sql.Tx, userID int64, taskID string, cycleKey string) (taskdomain.Progress, error) { + row := tx.QueryRowContext(ctx, ` + SELECT app_code, user_id, task_id, task_version_id, cycle_key, progress_value, + target_value, reward_coin_amount, status, completed_at_ms, claimed_at_ms, updated_at_ms + FROM user_task_progress + WHERE app_code = ? AND user_id = ? AND task_id = ? AND cycle_key = ? + FOR UPDATE`, + appcode.FromContext(ctx), userID, taskID, cycleKey, + ) + return scanProgress(row) +} + +func (r *Repository) getClaimByCommandForUpdate(ctx context.Context, tx *sql.Tx, commandID string) (taskdomain.Claim, bool, error) { + row := tx.QueryRowContext(ctx, taskClaimSelectSQL()+` + WHERE app_code = ? AND command_id = ? + FOR UPDATE`, appcode.FromContext(ctx), commandID) + claim, err := scanClaim(row) + if errors.Is(err, sql.ErrNoRows) { + return taskdomain.Claim{}, false, nil + } + return claim, err == nil, err +} + +func (r *Repository) getClaimByTaskCycleForUpdate(ctx context.Context, tx *sql.Tx, userID int64, taskID string, cycleKey string) (taskdomain.Claim, bool, error) { + row := tx.QueryRowContext(ctx, taskClaimSelectSQL()+` + WHERE app_code = ? AND user_id = ? AND task_id = ? AND cycle_key = ? + FOR UPDATE`, appcode.FromContext(ctx), userID, taskID, cycleKey) + claim, err := scanClaim(row) + if errors.Is(err, sql.ErrNoRows) { + return taskdomain.Claim{}, false, nil + } + return claim, err == nil, err +} + +func (r *Repository) getClaimByIDForUpdate(ctx context.Context, tx *sql.Tx, claimID string) (taskdomain.Claim, error) { + row := tx.QueryRowContext(ctx, taskClaimSelectSQL()+` + WHERE app_code = ? AND claim_id = ? + FOR UPDATE`, appcode.FromContext(ctx), claimID) + claim, err := scanClaim(row) + if errors.Is(err, sql.ErrNoRows) { + return taskdomain.Claim{}, xerr.New(xerr.NotFound, "task reward claim not found") + } + return claim, err +} + +func scanDefinitions(rows *sql.Rows) ([]taskdomain.Definition, error) { + definitions := make([]taskdomain.Definition, 0) + for rows.Next() { + definition, err := scanDefinition(rows) + if err != nil { + return nil, err + } + definitions = append(definitions, definition) + } + if err := rows.Err(); err != nil { + return nil, err + } + return definitions, nil +} + +type rowScanner interface { + Scan(dest ...any) error +} + +func scanDefinition(row rowScanner) (taskdomain.Definition, error) { + var definition taskdomain.Definition + err := row.Scan( + &definition.AppCode, + &definition.TaskID, + &definition.TaskType, + &definition.Category, + &definition.MetricType, + &definition.Title, + &definition.Description, + &definition.TargetValue, + &definition.TargetUnit, + &definition.RewardCoinAmount, + &definition.Status, + &definition.SortOrder, + &definition.Version, + &definition.CurrentVersionID, + &definition.EffectiveFromMS, + &definition.EffectiveToMS, + &definition.CreatedByAdminID, + &definition.UpdatedByAdminID, + &definition.CreatedAtMS, + &definition.UpdatedAtMS, + ) + return definition, err +} + +func scanProgress(row rowScanner) (taskdomain.Progress, error) { + var progress taskdomain.Progress + err := row.Scan( + &progress.AppCode, + &progress.UserID, + &progress.TaskID, + &progress.TaskVersionID, + &progress.CycleKey, + &progress.ProgressValue, + &progress.TargetValue, + &progress.RewardCoinAmount, + &progress.Status, + &progress.CompletedAtMS, + &progress.ClaimedAtMS, + &progress.UpdatedAtMS, + ) + return progress, err +} + +func scanClaim(row rowScanner) (taskdomain.Claim, error) { + var claim taskdomain.Claim + err := row.Scan( + &claim.ClaimID, + &claim.CommandID, + &claim.UserID, + &claim.TaskID, + &claim.TaskType, + &claim.CycleKey, + &claim.RewardCoinAmount, + &claim.WalletCommandID, + &claim.WalletTransactionID, + &claim.Status, + &claim.FailureReason, + &claim.CreatedAtMS, + &claim.UpdatedAtMS, + ) + return claim, err +} + +func taskClaimSelectSQL() string { + return `SELECT claim_id, command_id, user_id, task_id, task_type, cycle_key, reward_coin_amount, + wallet_command_id, wallet_transaction_id, status, failure_reason, created_at_ms, updated_at_ms + FROM task_reward_claims ` +} + +func taskDefinitionWhere(ctx context.Context, query taskdomain.DefinitionQuery) (string, []any) { + conditions := []string{"app_code = ?"} + args := []any{appcode.FromContext(ctx)} + if query.TaskType != "" { + conditions = append(conditions, "task_type = ?") + args = append(args, query.TaskType) + } + if query.Category != "" { + conditions = append(conditions, "category = ?") + args = append(args, query.Category) + } + if query.Status != "" { + conditions = append(conditions, "status = ?") + args = append(args, query.Status) + } + if query.Keyword != "" { + conditions = append(conditions, "(task_id LIKE ? OR title LIKE ? OR metric_type LIKE ?)") + like := "%" + query.Keyword + "%" + args = append(args, like, like, like) + } + return "WHERE " + strings.Join(conditions, " AND "), args +} + +func taskDefinitionNeedsVersion(current taskdomain.Definition, next taskdomain.Definition) bool { + return current.TaskType != next.TaskType || + current.Category != next.Category || + current.MetricType != next.MetricType || + current.TargetValue != next.TargetValue || + current.TargetUnit != next.TargetUnit || + current.RewardCoinAmount != next.RewardCoinAmount +} + +func normalizePageSize(value int32) int32 { + if value <= 0 { + return 20 + } + if value > 100 { + return 100 + } + return value +} + +func normalizeCycleKeys(values []string) []string { + seen := make(map[string]bool, len(values)) + result := make([]string, 0, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" || seen[value] { + continue + } + seen[value] = true + result = append(result, value) + } + return result +} + +func walletTaskCommandID(claimID string) string { + return fmt.Sprintf("wtask_%s", claimID) +} + +func truncateTaskFailure(value string) string { + value = strings.TrimSpace(value) + if len(value) <= 255 { + return value + } + return value[:255] +} diff --git a/services/activity-service/internal/transport/grpc/cron_server.go b/services/activity-service/internal/transport/grpc/cron_server.go new file mode 100644 index 00000000..5a026627 --- /dev/null +++ b/services/activity-service/internal/transport/grpc/cron_server.go @@ -0,0 +1,55 @@ +package grpc + +import ( + "context" + "time" + + activityv1 "hyapp.local/api/proto/activity/v1" + "hyapp/pkg/appcode" + "hyapp/pkg/xerr" + messageservice "hyapp/services/activity-service/internal/service/message" +) + +// CronServer exposes activity-service owned background batches to cron-service. +type CronServer struct { + activityv1.UnimplementedActivityCronServiceServer + + message *messageservice.Service +} + +// NewCronServer creates the internal cron adapter without exposing message storage. +func NewCronServer(messageSvc *messageservice.Service) *CronServer { + return &CronServer{message: messageSvc} +} + +// ProcessMessageFanoutBatch claims and processes one message fanout page. +func (s *CronServer) ProcessMessageFanoutBatch(ctx context.Context, req *activityv1.CronBatchRequest) (*activityv1.CronBatchResponse, error) { + ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) + if s.message == nil { + return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "message service is not configured")) + } + processed, err := s.message.ProcessNextFanoutJob(ctx, messageservice.FanoutWorkerOptions{ + WorkerID: req.GetWorkerId(), + BatchSize: int(req.GetBatchSize()), + LockTTL: durationFromMillis(req.GetLockTtlMs()), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + if !processed { + return &activityv1.CronBatchResponse{}, nil + } + return &activityv1.CronBatchResponse{ + ClaimedCount: 1, + ProcessedCount: 1, + SuccessCount: 1, + HasMore: true, + }, nil +} + +func durationFromMillis(value int64) time.Duration { + if value <= 0 { + return 0 + } + return time.Duration(value) * time.Millisecond +} diff --git a/services/activity-service/internal/transport/grpc/task_server.go b/services/activity-service/internal/transport/grpc/task_server.go new file mode 100644 index 00000000..708281fa --- /dev/null +++ b/services/activity-service/internal/transport/grpc/task_server.go @@ -0,0 +1,219 @@ +package grpc + +import ( + "context" + + activityv1 "hyapp.local/api/proto/activity/v1" + "hyapp/pkg/appcode" + "hyapp/pkg/xerr" + taskdomain "hyapp/services/activity-service/internal/domain/task" + taskservice "hyapp/services/activity-service/internal/service/task" +) + +// TaskServer 把任务查询、事件消费和领奖用例适配为 activity-service gRPC。 +type TaskServer struct { + activityv1.UnimplementedTaskServiceServer + + svc *taskservice.Service +} + +// NewTaskServer 创建 App 任务服务 gRPC adapter。 +func NewTaskServer(svc *taskservice.Service) *TaskServer { + return &TaskServer{svc: svc} +} + +// ListUserTasks 返回当前用户 daily/exclusive 任务页。 +func (s *TaskServer) ListUserTasks(ctx context.Context, req *activityv1.ListUserTasksRequest) (*activityv1.ListUserTasksResponse, error) { + ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) + result, err := s.svc.ListUserTasks(ctx, req.GetUserId()) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + return taskListToProto(result), nil +} + +// ClaimTaskReward 领取已完成任务奖励,金币入账仍由 wallet-service 完成。 +func (s *TaskServer) ClaimTaskReward(ctx context.Context, req *activityv1.ClaimTaskRewardRequest) (*activityv1.ClaimTaskRewardResponse, error) { + ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) + claim, err := s.svc.ClaimTaskReward(ctx, taskdomain.ClaimCommand{ + UserID: req.GetUserId(), + TaskID: req.GetTaskId(), + TaskType: req.GetTaskType(), + CycleKey: req.GetTaskDay(), + CommandID: req.GetCommandId(), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + return claimToProto(claim), nil +} + +// ConsumeTaskEvent 消费由 outbox worker 投递的服务端事实事件。 +func (s *TaskServer) ConsumeTaskEvent(ctx context.Context, req *activityv1.ConsumeTaskEventRequest) (*activityv1.ConsumeTaskEventResponse, error) { + ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) + result, err := s.svc.ConsumeTaskEvent(ctx, taskdomain.Event{ + EventID: req.GetEventId(), + EventType: req.GetEventType(), + SourceService: req.GetSourceService(), + UserID: req.GetUserId(), + MetricType: req.GetMetricType(), + Value: req.GetValue(), + OccurredAtMS: req.GetOccurredAtMs(), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + return &activityv1.ConsumeTaskEventResponse{ + EventId: result.EventID, + Status: result.Status, + MatchedTaskCount: result.MatchedTaskCount, + }, nil +} + +// AdminTaskServer 暴露后台任务配置管理入口。 +type AdminTaskServer struct { + activityv1.UnimplementedAdminTaskServiceServer + + svc *taskservice.Service +} + +// NewAdminTaskServer 创建后台任务配置 gRPC adapter。 +func NewAdminTaskServer(svc *taskservice.Service) *AdminTaskServer { + return &AdminTaskServer{svc: svc} +} + +// ListTaskDefinitions 返回后台任务定义列表。 +func (s *AdminTaskServer) ListTaskDefinitions(ctx context.Context, req *activityv1.ListTaskDefinitionsRequest) (*activityv1.ListTaskDefinitionsResponse, error) { + ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) + items, total, err := s.svc.ListTaskDefinitions(ctx, taskdomain.DefinitionQuery{ + TaskType: req.GetTaskType(), + Category: req.GetCategory(), + Status: req.GetStatus(), + Keyword: req.GetKeyword(), + Page: req.GetPage(), + PageSize: req.GetPageSize(), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + resp := &activityv1.ListTaskDefinitionsResponse{Tasks: make([]*activityv1.TaskDefinition, 0, len(items)), Total: total} + for _, item := range items { + resp.Tasks = append(resp.Tasks, definitionToProto(item)) + } + return resp, nil +} + +// UpsertTaskDefinition 创建或编辑任务配置。 +func (s *AdminTaskServer) UpsertTaskDefinition(ctx context.Context, req *activityv1.UpsertTaskDefinitionRequest) (*activityv1.UpsertTaskDefinitionResponse, error) { + ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) + item, created, err := s.svc.UpsertTaskDefinition(ctx, taskdomain.DefinitionCommand{ + TaskID: req.GetTaskId(), + TaskType: req.GetTaskType(), + Category: req.GetCategory(), + MetricType: req.GetMetricType(), + Title: req.GetTitle(), + Description: req.GetDescription(), + TargetValue: req.GetTargetValue(), + TargetUnit: req.GetTargetUnit(), + RewardCoinAmount: req.GetRewardCoinAmount(), + Status: req.GetStatus(), + SortOrder: req.GetSortOrder(), + EffectiveFromMS: req.GetEffectiveFromMs(), + EffectiveToMS: req.GetEffectiveToMs(), + OperatorAdminID: req.GetOperatorAdminId(), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + return &activityv1.UpsertTaskDefinitionResponse{Task: definitionToProto(item), Created: created}, nil +} + +// SetTaskDefinitionStatus 启停或归档任务。 +func (s *AdminTaskServer) SetTaskDefinitionStatus(ctx context.Context, req *activityv1.SetTaskDefinitionStatusRequest) (*activityv1.SetTaskDefinitionStatusResponse, error) { + ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) + item, err := s.svc.SetTaskDefinitionStatus(ctx, req.GetTaskId(), req.GetStatus(), req.GetOperatorAdminId()) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + return &activityv1.SetTaskDefinitionStatusResponse{Task: definitionToProto(item)}, nil +} + +func taskListToProto(result taskdomain.ListResult) *activityv1.ListUserTasksResponse { + resp := &activityv1.ListUserTasksResponse{ + Sections: make([]*activityv1.TaskSection, 0, len(result.Sections)), + ServerTimeMs: result.ServerTimeMS, + NextRefreshAtMs: result.NextRefreshAtMS, + } + for _, section := range result.Sections { + target := &activityv1.TaskSection{ + Section: section.Section, + Items: make([]*activityv1.TaskItem, 0, len(section.Items)), + ServerTimeMs: section.ServerTimeMS, + NextRefreshAtMs: section.NextRefreshAtMS, + } + for _, item := range section.Items { + target.Items = append(target.Items, itemToProto(item)) + } + resp.Sections = append(resp.Sections, target) + } + return resp +} + +func itemToProto(item taskdomain.Item) *activityv1.TaskItem { + return &activityv1.TaskItem{ + TaskId: item.TaskID, + TaskType: item.TaskType, + Category: item.Category, + MetricType: item.MetricType, + Title: item.Title, + Description: item.Description, + TargetValue: item.TargetValue, + TargetUnit: item.TargetUnit, + ProgressValue: item.ProgressValue, + RewardCoinAmount: item.RewardCoinAmount, + Status: item.UserStatus, + Claimable: item.Claimable, + TaskDay: item.TaskDay, + ServerTimeMs: item.ServerTimeMS, + NextRefreshAtMs: item.NextRefreshAtMS, + SortOrder: item.SortOrder, + Version: item.Version, + } +} + +func claimToProto(claim taskdomain.Claim) *activityv1.ClaimTaskRewardResponse { + return &activityv1.ClaimTaskRewardResponse{ + ClaimId: claim.ClaimID, + TaskId: claim.TaskID, + TaskType: claim.TaskType, + TaskDay: claim.CycleKey, + RewardCoinAmount: claim.RewardCoinAmount, + Status: claim.Status, + WalletTransactionId: claim.WalletTransactionID, + GrantedAtMs: claim.UpdatedAtMS, + Claimed: claim.Status == taskdomain.ClaimStatusGranted, + } +} + +func definitionToProto(item taskdomain.Definition) *activityv1.TaskDefinition { + return &activityv1.TaskDefinition{ + TaskId: item.TaskID, + TaskType: item.TaskType, + Category: item.Category, + MetricType: item.MetricType, + Title: item.Title, + Description: item.Description, + TargetValue: item.TargetValue, + TargetUnit: item.TargetUnit, + RewardCoinAmount: item.RewardCoinAmount, + Status: item.Status, + SortOrder: item.SortOrder, + Version: item.Version, + EffectiveFromMs: item.EffectiveFromMS, + EffectiveToMs: item.EffectiveToMS, + CreatedByAdminId: item.CreatedByAdminID, + UpdatedByAdminId: item.UpdatedByAdminID, + CreatedAtMs: item.CreatedAtMS, + UpdatedAtMs: item.UpdatedAtMS, + } +} diff --git a/services/cron-service/Dockerfile b/services/cron-service/Dockerfile new file mode 100644 index 00000000..9d54b056 --- /dev/null +++ b/services/cron-service/Dockerfile @@ -0,0 +1,30 @@ +FROM golang:1.23-alpine AS builder + +WORKDIR /src + +COPY go.mod go.sum ./ +COPY api/go.mod api/go.sum ./api/ +RUN GOWORK=off go mod download + +COPY . . + +RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./services/cron-service/cmd/server +RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/grpc-health-probe ./cmd/grpc-health-probe + +FROM alpine:3.20 + +WORKDIR /app + +RUN apk add --no-cache ca-certificates && adduser -D -g '' appuser + +COPY --from=builder /out/server /app/server +COPY --from=builder /out/grpc-health-probe /app/grpc-health-probe +COPY services/cron-service/configs/config.docker.yaml /app/config.yaml + +USER appuser + +EXPOSE 13007 + +ENTRYPOINT ["/app/server"] +CMD ["-config", "/app/config.yaml"] + diff --git a/services/cron-service/cmd/server/main.go b/services/cron-service/cmd/server/main.go new file mode 100644 index 00000000..59270b1b --- /dev/null +++ b/services/cron-service/cmd/server/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "flag" + "log" + "os" + "os/signal" + "syscall" + + "hyapp/pkg/logx" + "hyapp/services/cron-service/internal/app" + "hyapp/services/cron-service/internal/config" +) + +// main 启动 cron-service 调度进程;业务批处理通过 owner service gRPC 执行。 +func main() { + configPath := flag.String("config", "services/cron-service/configs/config.yaml", "path to cron-service config") + flag.Parse() + + cfg, err := config.Load(*configPath) + 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 { + fatalRuntime("app_new_failed", err) + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + errCh := make(chan error, 1) + go func() { + errCh <- application.Run() + }() + + select { + case <-ctx.Done(): + application.Close() + case err := <-errCh: + if err != nil { + fatalRuntime("service_run_failed", err) + } + } +} + +func fatalRuntime(msg string, err error) { + logx.Error(context.Background(), msg, err) + os.Exit(1) +} diff --git a/services/cron-service/configs/config.docker.yaml b/services/cron-service/configs/config.docker.yaml new file mode 100644 index 00000000..abea2ea2 --- /dev/null +++ b/services/cron-service/configs/config.docker.yaml @@ -0,0 +1,45 @@ +service_name: cron-service +node_id: cron-docker +environment: local +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 +grpc_addr: ":13007" +mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_cron?parseTime=true&charset=utf8mb4&loc=Local" +user_service_addr: "user-service:13005" +activity_service_addr: "activity-service:13006" +wallet_service_addr: "wallet-service:13004" +tasks: + login_ip_risk: + enabled: true + interval: "5s" + timeout: "3s" + lock_ttl: "30s" + batch_size: 100 + app_codes: ["lalu"] + user_region_rebuild: + enabled: true + interval: "5s" + timeout: "5s" + lock_ttl: "30s" + batch_size: 500 + app_codes: ["lalu"] + message_fanout: + enabled: true + interval: "1s" + timeout: "10s" + lock_ttl: "30s" + batch_size: 500 + app_codes: ["lalu"] + mic_open_session_compensation: + enabled: true + interval: "60s" + timeout: "10s" + lock_ttl: "60s" + batch_size: 100 + pending_publish_max_age: "2m" + publishing_session_max_age: "12h" + app_codes: ["lalu"] diff --git a/services/cron-service/configs/config.tencent.example.yaml b/services/cron-service/configs/config.tencent.example.yaml new file mode 100644 index 00000000..f732cbdf --- /dev/null +++ b/services/cron-service/configs/config.tencent.example.yaml @@ -0,0 +1,45 @@ +service_name: cron-service +node_id: "cron-node-prod-1" +environment: prod +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 +grpc_addr: ":13007" +mysql_dsn: "USER:PASSWORD@tcp(TENCENT_CDB_HOST:3306)/hyapp_cron?parseTime=true&charset=utf8mb4&loc=Local&tls=false" +user_service_addr: "user-service.internal:13005" +activity_service_addr: "activity-service.internal:13006" +wallet_service_addr: "wallet-service.internal:13004" +tasks: + login_ip_risk: + enabled: true + interval: "5s" + timeout: "3s" + lock_ttl: "30s" + batch_size: 100 + app_codes: ["lalu"] + user_region_rebuild: + enabled: true + interval: "5s" + timeout: "5s" + lock_ttl: "30s" + batch_size: 500 + app_codes: ["lalu"] + message_fanout: + enabled: true + interval: "1s" + timeout: "10s" + lock_ttl: "30s" + batch_size: 500 + app_codes: ["lalu"] + mic_open_session_compensation: + enabled: true + interval: "60s" + timeout: "10s" + lock_ttl: "60s" + batch_size: 100 + pending_publish_max_age: "2m" + publishing_session_max_age: "12h" + app_codes: ["lalu"] diff --git a/services/cron-service/configs/config.yaml b/services/cron-service/configs/config.yaml new file mode 100644 index 00000000..5167578e --- /dev/null +++ b/services/cron-service/configs/config.yaml @@ -0,0 +1,45 @@ +service_name: cron-service +node_id: cron-local +environment: local +log: + level: info + format: json + include_request_body: false + include_response_body: false + max_payload_bytes: 2048 +grpc_addr: ":13007" +mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_cron?parseTime=true&charset=utf8mb4&loc=Local" +user_service_addr: "127.0.0.1:13005" +activity_service_addr: "127.0.0.1:13006" +wallet_service_addr: "127.0.0.1:13004" +tasks: + login_ip_risk: + enabled: true + interval: "5s" + timeout: "3s" + lock_ttl: "30s" + batch_size: 100 + app_codes: ["lalu"] + user_region_rebuild: + enabled: true + interval: "5s" + timeout: "5s" + lock_ttl: "30s" + batch_size: 500 + app_codes: ["lalu"] + message_fanout: + enabled: true + interval: "1s" + timeout: "10s" + lock_ttl: "30s" + batch_size: 500 + app_codes: ["lalu"] + mic_open_session_compensation: + enabled: true + interval: "60s" + timeout: "10s" + lock_ttl: "60s" + batch_size: 100 + pending_publish_max_age: "2m" + publishing_session_max_age: "12h" + app_codes: ["lalu"] diff --git a/services/cron-service/deploy/mysql/initdb/001_cron_service.sql b/services/cron-service/deploy/mysql/initdb/001_cron_service.sql new file mode 100644 index 00000000..a7bf35c1 --- /dev/null +++ b/services/cron-service/deploy/mysql/initdb/001_cron_service.sql @@ -0,0 +1,35 @@ +SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE DATABASE IF NOT EXISTS hyapp_cron DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hyapp_cron; + +CREATE TABLE IF NOT EXISTS cron_task_leases ( + task_name VARCHAR(96) NOT NULL, + app_code VARCHAR(32) NOT NULL, + owner_node_id VARCHAR(128) NOT NULL, + lease_until_ms BIGINT NOT NULL, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (task_name, app_code), + KEY idx_cron_task_leases_expire (lease_until_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS cron_task_runs ( + run_id VARCHAR(96) NOT NULL, + task_name VARCHAR(96) NOT NULL, + app_code VARCHAR(32) NOT NULL, + owner_node_id VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL, + claimed_count INT NOT NULL DEFAULT 0, + processed_count INT NOT NULL DEFAULT 0, + success_count INT NOT NULL DEFAULT 0, + failure_count INT NOT NULL DEFAULT 0, + started_at_ms BIGINT NOT NULL, + finished_at_ms BIGINT NULL, + duration_ms BIGINT NOT NULL DEFAULT 0, + error_message VARCHAR(512) NOT NULL DEFAULT '', + PRIMARY KEY (run_id), + KEY idx_cron_task_runs_task_time (task_name, app_code, started_at_ms), + KEY idx_cron_task_runs_status (status, started_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + diff --git a/services/cron-service/internal/app/app.go b/services/cron-service/internal/app/app.go new file mode 100644 index 00000000..2ad9fc42 --- /dev/null +++ b/services/cron-service/internal/app/app.go @@ -0,0 +1,140 @@ +// Package app wires cron-service configuration, scheduler, storage and gRPC health. +package app + +import ( + "context" + "errors" + "net" + "sync" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + healthgrpc "google.golang.org/grpc/health/grpc_health_v1" + activityv1 "hyapp.local/api/proto/activity/v1" + userv1 "hyapp.local/api/proto/user/v1" + "hyapp/pkg/grpchealth" + "hyapp/pkg/logx" + "hyapp/services/cron-service/internal/config" + "hyapp/services/cron-service/internal/integration" + "hyapp/services/cron-service/internal/scheduler" + mysqlstorage "hyapp/services/cron-service/internal/storage/mysql" +) + +// App owns the cron-service process lifecycle. +type App struct { + server *grpc.Server + listener net.Listener + health *grpchealth.ServingChecker + repository *mysqlstorage.Repository + scheduler *scheduler.Scheduler + userConn *grpc.ClientConn + activityConn *grpc.ClientConn + workerCtx context.Context + workerCancel context.CancelFunc + workerWG sync.WaitGroup + closeOnce sync.Once +} + +// New initializes cron metadata storage, health service and scheduler. +func New(cfg config.Config) (*App, error) { + startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // cron-service owns only scheduler metadata. Business data stays in owner service databases. + repository, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN) + if err != nil { + return nil, err + } + + listener, err := net.Listen("tcp", cfg.GRPCAddr) + if err != nil { + _ = repository.Close() + return nil, err + } + userConn, err := grpc.Dial(cfg.UserServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + _ = listener.Close() + _ = repository.Close() + return nil, err + } + activityConn, err := grpc.Dial(cfg.ActivityServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + _ = userConn.Close() + _ = listener.Close() + _ = repository.Close() + return nil, err + } + + server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("cron-service"))) + health := grpchealth.NewServingChecker("cron-service") + healthgrpc.RegisterHealthServer(server, grpchealth.NewServer(health)) + + workerCtx, workerCancel := context.WithCancel(context.Background()) + userCron := integration.NewUserCronClient(userv1.NewUserCronServiceClient(userConn)) + activityCron := integration.NewActivityCronClient(activityv1.NewActivityCronServiceClient(activityConn)) + taskScheduler := scheduler.New(cfg.NodeID, cfg.Tasks, repository, map[string]scheduler.Handler{ + "login_ip_risk": userCron.ProcessLoginIPRiskBatch, + "user_region_rebuild": userCron.ProcessRegionRebuildBatch, + "mic_open_session_compensation": userCron.CompensateMicOpenSessions, + "message_fanout": activityCron.ProcessMessageFanoutBatch, + }) + + return &App{ + server: server, + listener: listener, + health: health, + repository: repository, + scheduler: taskScheduler, + userConn: userConn, + activityConn: activityConn, + workerCtx: workerCtx, + workerCancel: workerCancel, + }, nil +} + +// Run starts scheduler loops and gRPC health serving. +func (a *App) Run() error { + a.health.MarkServing() + defer func() { + if a.workerCancel != nil { + a.workerCancel() + } + a.workerWG.Wait() + }() + if a.scheduler != nil { + a.workerWG.Add(1) + go func() { + defer a.workerWG.Done() + a.scheduler.Run(a.workerCtx) + }() + } + + err := a.server.Serve(a.listener) + a.health.MarkStopped() + if errors.Is(err, grpc.ErrServerStopped) { + return nil + } + return err +} + +// Close drains scheduler loops before closing gRPC and MySQL resources. +func (a *App) Close() { + a.closeOnce.Do(func() { + a.health.MarkDraining() + if a.workerCancel != nil { + a.workerCancel() + } + a.workerWG.Wait() + a.server.GracefulStop() + if a.repository != nil { + _ = a.repository.Close() + } + if a.userConn != nil { + _ = a.userConn.Close() + } + if a.activityConn != nil { + _ = a.activityConn.Close() + } + }) +} diff --git a/services/cron-service/internal/config/config.go b/services/cron-service/internal/config/config.go new file mode 100644 index 00000000..405945e8 --- /dev/null +++ b/services/cron-service/internal/config/config.go @@ -0,0 +1,218 @@ +// Package config defines cron-service runtime configuration. +package config + +import ( + "strings" + + "hyapp/pkg/appcode" + "hyapp/pkg/configx" + "hyapp/pkg/logx" +) + +const defaultAppCode = "lalu" + +// Config describes cron-service startup and schedule settings. +type Config struct { + // ServiceName is written into logs and health metadata. + ServiceName string `yaml:"service_name"` + // NodeID identifies this scheduler instance in run history and task leases. + NodeID string `yaml:"node_id"` + // Environment is the runtime environment tag used by structured logs. + Environment string `yaml:"environment"` + // GRPCAddr exposes gRPC health and future internal management APIs. + GRPCAddr string `yaml:"grpc_addr"` + // MySQLDSN stores cron run history and schedule-level leases only. + MySQLDSN string `yaml:"mysql_dsn"` + // UserServiceAddr is the owner endpoint for user/auth/host batch tasks. + UserServiceAddr string `yaml:"user_service_addr"` + // ActivityServiceAddr is the owner endpoint for activity/message batch tasks. + ActivityServiceAddr string `yaml:"activity_service_addr"` + // WalletServiceAddr is the owner endpoint for wallet/VIP/settlement tasks. + WalletServiceAddr string `yaml:"wallet_service_addr"` + // Tasks contains per-task schedule knobs. Disabled tasks are documented but not started. + Tasks map[string]TaskConfig `yaml:"tasks"` + // Log controls stdout structured logging. + Log logx.Config `yaml:"log"` +} + +// TaskConfig keeps scheduler knobs as strings so YAML stays easy to read. +type TaskConfig struct { + // Enabled controls whether cron-service starts a loop for this task. + Enabled bool `yaml:"enabled"` + // Interval is the delay between successful schedule attempts. + Interval string `yaml:"interval"` + // Timeout bounds the owner service RPC call for one batch. + Timeout string `yaml:"timeout"` + // LockTTL is the owner service task claim lock budget. + LockTTL string `yaml:"lock_ttl"` + // BatchSize is passed to owner service batch RPCs. + BatchSize int `yaml:"batch_size"` + // AppCodes scopes the task by tenant; every app_code gets an independent run. + AppCodes []string `yaml:"app_codes"` + // PendingPublishMaxAge is used by mic_open_session_compensation for pending_publish sessions. + PendingPublishMaxAge string `yaml:"pending_publish_max_age"` + // PublishingSessionMaxAge is used by mic_open_session_compensation for publishing sessions. + PublishingSessionMaxAge string `yaml:"publishing_session_max_age"` +} + +// Default returns local development defaults that follow the 13xxx port rule. +func Default() Config { + return Config{ + ServiceName: "cron-service", + NodeID: "cron-local", + Environment: "local", + GRPCAddr: ":13007", + MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_cron?parseTime=true&charset=utf8mb4&loc=Local", + UserServiceAddr: "127.0.0.1:13005", + ActivityServiceAddr: "127.0.0.1:13006", + WalletServiceAddr: "127.0.0.1:13004", + Tasks: defaultTasks(), + Log: logx.Config{ + Level: "info", + Format: "json", + MaxPayloadBytes: 2048, + }, + } +} + +// Load reads a YAML file into Config and normalizes empty fields. +func Load(path string) (Config, error) { + cfg := Default() + if path == "" { + return cfg, nil + } + if err := configx.LoadYAML(path, &cfg); err != nil { + return Config{}, err + } + normalize(&cfg) + return cfg, nil +} + +func normalize(cfg *Config) { + cfg.ServiceName = strings.TrimSpace(cfg.ServiceName) + if cfg.ServiceName == "" { + cfg.ServiceName = "cron-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.GRPCAddr = strings.TrimSpace(cfg.GRPCAddr) + if cfg.GRPCAddr == "" { + cfg.GRPCAddr = ":13007" + } + cfg.Tasks = normalizeTasks(cfg.Tasks) +} + +func defaultTasks() map[string]TaskConfig { + return map[string]TaskConfig{ + "login_ip_risk": { + Enabled: true, + Interval: "5s", + Timeout: "3s", + LockTTL: "30s", + BatchSize: 100, + AppCodes: []string{defaultAppCode}, + }, + "user_region_rebuild": { + Enabled: true, + Interval: "5s", + Timeout: "5s", + LockTTL: "30s", + BatchSize: 500, + AppCodes: []string{defaultAppCode}, + }, + "message_fanout": { + Enabled: true, + Interval: "1s", + Timeout: "10s", + LockTTL: "30s", + BatchSize: 500, + AppCodes: []string{defaultAppCode}, + }, + "mic_open_session_compensation": { + Enabled: true, + Interval: "60s", + Timeout: "10s", + LockTTL: "60s", + BatchSize: 100, + AppCodes: []string{defaultAppCode}, + PendingPublishMaxAge: "2m", + PublishingSessionMaxAge: "12h", + }, + } +} + +func normalizeTasks(tasks map[string]TaskConfig) map[string]TaskConfig { + defaults := defaultTasks() + if tasks == nil { + tasks = map[string]TaskConfig{} + } + for name, def := range defaults { + current, ok := tasks[name] + if !ok { + tasks[name] = def + continue + } + tasks[name] = mergeTaskConfig(def, current) + } + for name, task := range tasks { + tasks[name] = mergeTaskConfig(TaskConfig{ + Interval: "5s", + Timeout: "5s", + LockTTL: "30s", + BatchSize: 100, + AppCodes: []string{defaultAppCode}, + }, task) + } + return tasks +} + +func mergeTaskConfig(def TaskConfig, current TaskConfig) TaskConfig { + current.Interval = strings.TrimSpace(current.Interval) + if current.Interval == "" { + current.Interval = def.Interval + } + current.Timeout = strings.TrimSpace(current.Timeout) + if current.Timeout == "" { + current.Timeout = def.Timeout + } + current.LockTTL = strings.TrimSpace(current.LockTTL) + if current.LockTTL == "" { + current.LockTTL = def.LockTTL + } + current.PendingPublishMaxAge = strings.TrimSpace(current.PendingPublishMaxAge) + if current.PendingPublishMaxAge == "" { + current.PendingPublishMaxAge = def.PendingPublishMaxAge + } + current.PublishingSessionMaxAge = strings.TrimSpace(current.PublishingSessionMaxAge) + if current.PublishingSessionMaxAge == "" { + current.PublishingSessionMaxAge = def.PublishingSessionMaxAge + } + if current.BatchSize <= 0 { + current.BatchSize = def.BatchSize + } + current.AppCodes = normalizeAppCodes(current.AppCodes) + if len(current.AppCodes) == 0 { + current.AppCodes = def.AppCodes + } + return current +} + +func normalizeAppCodes(values []string) []string { + seen := map[string]bool{} + result := make([]string, 0, len(values)) + for _, value := range values { + code := appcode.Normalize(value) + if code == "" || seen[code] { + continue + } + seen[code] = true + result = append(result, code) + } + return result +} diff --git a/services/cron-service/internal/integration/activity.go b/services/cron-service/internal/integration/activity.go new file mode 100644 index 00000000..43eab5ca --- /dev/null +++ b/services/cron-service/internal/integration/activity.go @@ -0,0 +1,45 @@ +package integration + +import ( + "context" + "time" + + activityv1 "hyapp.local/api/proto/activity/v1" + "hyapp/services/cron-service/internal/scheduler" +) + +// ActivityCronClient delegates activity-owned background batches to activity-service. +type ActivityCronClient struct { + client activityv1.ActivityCronServiceClient +} + +// NewActivityCronClient creates scheduler handlers backed by activity-service gRPC. +func NewActivityCronClient(client activityv1.ActivityCronServiceClient) *ActivityCronClient { + return &ActivityCronClient{client: client} +} + +// ProcessMessageFanoutBatch handles message_fanout_jobs. +func (c *ActivityCronClient) ProcessMessageFanoutBatch(ctx context.Context, req scheduler.BatchRequest) (scheduler.BatchResult, error) { + resp, err := c.client.ProcessMessageFanoutBatch(ctx, &activityv1.CronBatchRequest{ + Meta: &activityv1.RequestMeta{ + RequestId: req.RunID, + Caller: "cron-service", + SentAtMs: time.Now().UnixMilli(), + AppCode: req.AppCode, + }, + RunId: req.RunID, + WorkerId: req.WorkerID, + BatchSize: int32(req.BatchSize), + LockTtlMs: req.LockTTL.Milliseconds(), + }) + if err != nil { + return scheduler.BatchResult{}, err + } + return scheduler.BatchResult{ + ClaimedCount: int(resp.GetClaimedCount()), + ProcessedCount: int(resp.GetProcessedCount()), + SuccessCount: int(resp.GetSuccessCount()), + FailureCount: int(resp.GetFailureCount()), + HasMore: resp.GetHasMore(), + }, nil +} diff --git a/services/cron-service/internal/integration/user.go b/services/cron-service/internal/integration/user.go new file mode 100644 index 00000000..e7315d12 --- /dev/null +++ b/services/cron-service/internal/integration/user.go @@ -0,0 +1,70 @@ +// Package integration adapts owner-service gRPC APIs into scheduler handlers. +package integration + +import ( + "context" + "time" + + userv1 "hyapp.local/api/proto/user/v1" + "hyapp/services/cron-service/internal/scheduler" +) + +// UserCronClient delegates user-owned background batches to user-service. +type UserCronClient struct { + client userv1.UserCronServiceClient +} + +// NewUserCronClient creates scheduler handlers backed by user-service gRPC. +func NewUserCronClient(client userv1.UserCronServiceClient) *UserCronClient { + return &UserCronClient{client: client} +} + +// ProcessLoginIPRiskBatch handles login_ip_risk_jobs. +func (c *UserCronClient) ProcessLoginIPRiskBatch(ctx context.Context, req scheduler.BatchRequest) (scheduler.BatchResult, error) { + resp, err := c.client.ProcessLoginIPRiskBatch(ctx, userCronRequest(req)) + return userCronResult(resp), err +} + +// ProcessRegionRebuildBatch handles user_region_rebuild_tasks. +func (c *UserCronClient) ProcessRegionRebuildBatch(ctx context.Context, req scheduler.BatchRequest) (scheduler.BatchResult, error) { + resp, err := c.client.ProcessRegionRebuildBatch(ctx, userCronRequest(req)) + return userCronResult(resp), err +} + +// CompensateMicOpenSessions handles abnormal user_mic_sessions closure. +func (c *UserCronClient) CompensateMicOpenSessions(ctx context.Context, req scheduler.BatchRequest) (scheduler.BatchResult, error) { + protoReq := userCronRequest(req) + resp, err := c.client.CompensateMicOpenSessions(ctx, protoReq) + return userCronResult(resp), err +} + +func userCronRequest(req scheduler.BatchRequest) *userv1.CronBatchRequest { + return &userv1.CronBatchRequest{ + Meta: &userv1.RequestMeta{ + RequestId: req.RunID, + Caller: "cron-service", + SentAtMs: time.Now().UnixMilli(), + AppCode: req.AppCode, + GatewayNodeId: req.WorkerID, + }, + RunId: req.RunID, + WorkerId: req.WorkerID, + BatchSize: int32(req.BatchSize), + LockTtlMs: req.LockTTL.Milliseconds(), + PendingPublishMaxAgeMs: req.PendingPublishMaxAge.Milliseconds(), + PublishingSessionMaxAgeMs: req.PublishingSessionMaxAge.Milliseconds(), + } +} + +func userCronResult(resp *userv1.CronBatchResponse) scheduler.BatchResult { + if resp == nil { + return scheduler.BatchResult{} + } + return scheduler.BatchResult{ + ClaimedCount: int(resp.GetClaimedCount()), + ProcessedCount: int(resp.GetProcessedCount()), + SuccessCount: int(resp.GetSuccessCount()), + FailureCount: int(resp.GetFailureCount()), + HasMore: resp.GetHasMore(), + } +} diff --git a/services/cron-service/internal/scheduler/scheduler.go b/services/cron-service/internal/scheduler/scheduler.go new file mode 100644 index 00000000..4fd38ea0 --- /dev/null +++ b/services/cron-service/internal/scheduler/scheduler.go @@ -0,0 +1,201 @@ +// Package scheduler runs configured cron tasks and delegates business work to handlers. +package scheduler + +import ( + "context" + "log/slog" + "sort" + "strings" + "sync" + "time" + + "hyapp/pkg/idgen" + "hyapp/pkg/logx" + "hyapp/services/cron-service/internal/config" + mysqlstorage "hyapp/services/cron-service/internal/storage/mysql" +) + +const ( + runStatusRunning = "running" + runStatusSucceeded = "succeeded" + runStatusFailed = "failed" +) + +// Store is the cron metadata boundary used by scheduler loops. +type Store interface { + TryAcquireTaskLease(ctx context.Context, taskName string, appCode string, ownerNodeID string, nowMs int64, leaseTTL time.Duration) (bool, error) + StartTaskRun(ctx context.Context, run mysqlstorage.TaskRun) error + FinishTaskRun(ctx context.Context, run mysqlstorage.TaskRun) error +} + +// BatchRequest is the stable input passed to task handlers. +type BatchRequest struct { + TaskName string + AppCode string + RunID string + WorkerID string + BatchSize int + LockTTL time.Duration + PendingPublishMaxAge time.Duration + PublishingSessionMaxAge time.Duration +} + +// BatchResult summarizes one owner-service batch response. +type BatchResult struct { + ClaimedCount int + ProcessedCount int + SuccessCount int + FailureCount int + HasMore bool +} + +// Handler executes one batch through an owner service client. +type Handler func(ctx context.Context, request BatchRequest) (BatchResult, error) + +// Scheduler owns task loops. It never implements domain logic directly. +type Scheduler struct { + nodeID string + tasks map[string]config.TaskConfig + store Store + handlers map[string]Handler +} + +// New creates a scheduler with explicit handlers for tasks that are already migrated. +func New(nodeID string, tasks map[string]config.TaskConfig, store Store, handlers map[string]Handler) *Scheduler { + return &Scheduler{ + nodeID: strings.TrimSpace(nodeID), + tasks: tasks, + store: store, + handlers: handlers, + } +} + +// Run starts one loop per enabled task and app_code until ctx is cancelled. +func (s *Scheduler) Run(ctx context.Context) { + if s == nil { + return + } + taskNames := make([]string, 0, len(s.tasks)) + for name := range s.tasks { + taskNames = append(taskNames, name) + } + sort.Strings(taskNames) + + var wg sync.WaitGroup + for _, taskName := range taskNames { + task := s.tasks[taskName] + if !task.Enabled { + continue + } + handler := s.handlers[taskName] + if handler == nil { + // 配置可以先落地,但没有 owner-service RPC handler 时不能启动空跑循环。 + logx.Warn(ctx, "cron_task_handler_missing", slog.String("task_name", taskName)) + continue + } + for _, appCode := range task.AppCodes { + wg.Add(1) + go func(taskName string, appCode string, task config.TaskConfig, handler Handler) { + defer wg.Done() + s.runTaskLoop(ctx, taskName, appCode, task, handler) + }(taskName, appCode, task, handler) + } + } + wg.Wait() +} + +func (s *Scheduler) runTaskLoop(ctx context.Context, taskName string, appCode string, task config.TaskConfig, handler Handler) { + interval := parseDuration(task.Interval, 5*time.Second) + timer := time.NewTimer(0) + defer timer.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-timer.C: + hasMore := s.runOnce(ctx, taskName, appCode, task, handler) + if hasMore { + timer.Reset(0) + continue + } + timer.Reset(interval) + } + } +} + +func (s *Scheduler) runOnce(ctx context.Context, taskName string, appCode string, task config.TaskConfig, handler Handler) bool { + if s.store == nil { + logx.Error(ctx, "cron_store_missing", nil, slog.String("task_name", taskName), slog.String("app_code", appCode)) + return false + } + now := time.Now() + lockTTL := parseDuration(task.LockTTL, 30*time.Second) + acquired, err := s.store.TryAcquireTaskLease(ctx, taskName, appCode, s.nodeID, now.UnixMilli(), lockTTL) + if err != nil { + logx.Error(ctx, "cron_task_lease_failed", err, slog.String("task_name", taskName), slog.String("app_code", appCode), slog.String("worker_id", s.nodeID)) + return false + } + if !acquired { + return false + } + + runID := idgen.New("cronrun") + run := mysqlstorage.TaskRun{ + RunID: runID, + TaskName: taskName, + AppCode: appCode, + OwnerNodeID: s.nodeID, + Status: runStatusRunning, + StartedAtMs: now.UnixMilli(), + } + if err := s.store.StartTaskRun(ctx, run); err != nil { + logx.Error(ctx, "cron_task_run_start_failed", err, slog.String("task_name", taskName), slog.String("app_code", appCode), slog.String("run_id", runID)) + return false + } + + timeout := parseDuration(task.Timeout, 5*time.Second) + taskCtx, cancel := context.WithTimeout(ctx, timeout) + result, handlerErr := handler(taskCtx, BatchRequest{ + TaskName: taskName, + AppCode: appCode, + RunID: runID, + WorkerID: s.nodeID, + BatchSize: task.BatchSize, + LockTTL: lockTTL, + PendingPublishMaxAge: parseDuration(task.PendingPublishMaxAge, 0), + PublishingSessionMaxAge: parseDuration(task.PublishingSessionMaxAge, 0), + }) + cancel() + + finished := time.Now() + run.FinishedAtMs = finished.UnixMilli() + run.DurationMs = finished.Sub(now).Milliseconds() + run.ClaimedCount = result.ClaimedCount + run.ProcessedCount = result.ProcessedCount + run.SuccessCount = result.SuccessCount + run.FailureCount = result.FailureCount + run.Status = runStatusSucceeded + if handlerErr != nil { + run.Status = runStatusFailed + run.ErrorMessage = handlerErr.Error() + } + if err := s.store.FinishTaskRun(context.Background(), run); err != nil { + logx.Error(ctx, "cron_task_run_finish_failed", err, slog.String("task_name", taskName), slog.String("app_code", appCode), slog.String("run_id", runID)) + return false + } + if handlerErr != nil { + logx.Error(ctx, "cron_task_run_failed", handlerErr, slog.String("task_name", taskName), slog.String("app_code", appCode), slog.String("run_id", runID), slog.Int("processed_count", run.ProcessedCount), slog.Int64("duration_ms", run.DurationMs)) + return false + } + logx.Info(ctx, "cron_task_run_succeeded", slog.String("task_name", taskName), slog.String("app_code", appCode), slog.String("run_id", runID), slog.Int("processed_count", run.ProcessedCount), slog.Int("claimed_count", run.ClaimedCount), slog.Bool("has_more", result.HasMore), slog.Int64("duration_ms", run.DurationMs)) + return result.HasMore +} + +func parseDuration(raw string, fallback time.Duration) time.Duration { + duration, err := time.ParseDuration(strings.TrimSpace(raw)) + if err != nil || duration <= 0 { + return fallback + } + return duration +} diff --git a/services/cron-service/internal/storage/mysql/repository.go b/services/cron-service/internal/storage/mysql/repository.go new file mode 100644 index 00000000..7a7939ab --- /dev/null +++ b/services/cron-service/internal/storage/mysql/repository.go @@ -0,0 +1,143 @@ +// Package mysql stores cron-service scheduler leases and run history. +package mysql + +import ( + "context" + "database/sql" + "strings" + "time" + + _ "github.com/go-sql-driver/mysql" + "hyapp/pkg/xerr" +) + +// Repository owns cron-service metadata only; business data stays in owner services. +type Repository struct { + db *sql.DB +} + +// TaskRun is the durable audit row for one scheduled batch attempt. +type TaskRun struct { + RunID string + TaskName string + AppCode string + OwnerNodeID string + Status string + ClaimedCount int + ProcessedCount int + SuccessCount int + FailureCount int + StartedAtMs int64 + FinishedAtMs int64 + DurationMs int64 + ErrorMessage string +} + +// Open creates the cron metadata connection pool and verifies connectivity. +func Open(ctx context.Context, dsn string) (*Repository, error) { + if strings.TrimSpace(dsn) == "" { + return nil, xerr.New(xerr.InvalidArgument, "mysql_dsn is required") + } + db, err := sql.Open("mysql", dsn) + if err != nil { + return nil, err + } + if err := db.PingContext(ctx); err != nil { + _ = db.Close() + return nil, err + } + return &Repository{db: db}, nil +} + +// Close releases the cron metadata connection pool. +func (r *Repository) Close() error { + if r == nil || r.db == nil { + return nil + } + return r.db.Close() +} + +// Ping verifies that cron metadata storage is currently reachable. +func (r *Repository) Ping(ctx context.Context) error { + if r == nil || r.db == nil { + return xerr.New(xerr.Unavailable, "cron repository is not configured") + } + return r.db.PingContext(ctx) +} + +// TryAcquireTaskLease serializes schedule triggers per task and app_code. +func (r *Repository) TryAcquireTaskLease(ctx context.Context, taskName string, appCode string, ownerNodeID string, nowMs int64, leaseTTL time.Duration) (bool, error) { + if r == nil || r.db == nil { + return false, xerr.New(xerr.Unavailable, "cron repository is not configured") + } + if leaseTTL <= 0 { + leaseTTL = 30 * time.Second + } + + leaseUntilMs := nowMs + leaseTTL.Milliseconds() + result, err := r.db.ExecContext(ctx, ` + INSERT IGNORE INTO cron_task_leases (task_name, app_code, owner_node_id, lease_until_ms, updated_at_ms) + VALUES (?, ?, ?, ?, ?) + `, taskName, appCode, ownerNodeID, leaseUntilMs, nowMs) + if err != nil { + return false, err + } + inserted, err := result.RowsAffected() + if err != nil { + return false, err + } + if inserted > 0 { + return true, nil + } + + result, err = r.db.ExecContext(ctx, ` + UPDATE cron_task_leases + SET owner_node_id = ?, lease_until_ms = ?, updated_at_ms = ? + WHERE task_name = ? AND app_code = ? AND (lease_until_ms <= ? OR owner_node_id = ?) + `, ownerNodeID, leaseUntilMs, nowMs, taskName, appCode, nowMs, ownerNodeID) + if err != nil { + return false, err + } + updated, err := result.RowsAffected() + if err != nil { + return false, err + } + return updated > 0, nil +} + +// StartTaskRun inserts a running audit row before calling the owner service. +func (r *Repository) StartTaskRun(ctx context.Context, run TaskRun) error { + if r == nil || r.db == nil { + return xerr.New(xerr.Unavailable, "cron repository is not configured") + } + _, err := r.db.ExecContext(ctx, ` + INSERT INTO cron_task_runs ( + run_id, task_name, app_code, owner_node_id, status, + claimed_count, processed_count, success_count, failure_count, + started_at_ms, finished_at_ms, duration_ms, error_message + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, '') + `, run.RunID, run.TaskName, run.AppCode, run.OwnerNodeID, run.Status, run.ClaimedCount, run.ProcessedCount, run.SuccessCount, run.FailureCount, run.StartedAtMs) + return err +} + +// FinishTaskRun records the final outcome for one scheduled batch attempt. +func (r *Repository) FinishTaskRun(ctx context.Context, run TaskRun) error { + if r == nil || r.db == nil { + return xerr.New(xerr.Unavailable, "cron repository is not configured") + } + _, err := r.db.ExecContext(ctx, ` + UPDATE cron_task_runs + SET status = ?, claimed_count = ?, processed_count = ?, success_count = ?, failure_count = ?, + finished_at_ms = ?, duration_ms = ?, error_message = ? + WHERE run_id = ? + `, run.Status, run.ClaimedCount, run.ProcessedCount, run.SuccessCount, run.FailureCount, run.FinishedAtMs, run.DurationMs, truncateError(run.ErrorMessage), run.RunID) + return err +} + +func truncateError(value string) string { + value = strings.TrimSpace(value) + if len(value) <= 512 { + return value + } + return value[:512] +} diff --git a/services/gateway-service/configs/config.docker.yaml b/services/gateway-service/configs/config.docker.yaml index 31f7f07a..c00f17a7 100644 --- a/services/gateway-service/configs/config.docker.yaml +++ b/services/gateway-service/configs/config.docker.yaml @@ -30,6 +30,17 @@ auth_rate_limit: display_user_id_ip_limit: 10 display_user_id_limit: 50 session_id_ip_limit: 30 +login_risk: + enabled: true + redis_addr: "redis:6379" + redis_password: "" + redis_db: 0 + key_prefix: "login_risk:ip:" + fast_guard: + blocked_languages: [] + blocked_language_primary_tags: [] + blocked_timezones: [] + blocked_timezone_prefixes: [] tencent_im: # Docker 本地联调腾讯云 IM UserSig;必须与 room-service 使用同一组应用配置。 sdk_app_id: 20036101 diff --git a/services/gateway-service/configs/config.tencent.example.yaml b/services/gateway-service/configs/config.tencent.example.yaml index 9c61bf76..8943bbac 100644 --- a/services/gateway-service/configs/config.tencent.example.yaml +++ b/services/gateway-service/configs/config.tencent.example.yaml @@ -30,6 +30,17 @@ auth_rate_limit: display_user_id_ip_limit: 10 display_user_id_limit: 50 session_id_ip_limit: 30 +login_risk: + enabled: true + redis_addr: "TENCENT_REDIS_HOST:6379" + redis_password: "TENCENT_REDIS_PASSWORD" + redis_db: 0 + key_prefix: "login_risk:ip:" + fast_guard: + blocked_languages: [] + blocked_language_primary_tags: [] + blocked_timezones: [] + blocked_timezone_prefixes: [] tencent_im: # 腾讯云 IM 应用 ID,来自 Chat/IM 控制台的 SDKAppID。 diff --git a/services/gateway-service/configs/config.yaml b/services/gateway-service/configs/config.yaml index 52c33b5e..5d961f33 100644 --- a/services/gateway-service/configs/config.yaml +++ b/services/gateway-service/configs/config.yaml @@ -30,6 +30,17 @@ auth_rate_limit: display_user_id_ip_limit: 10 display_user_id_limit: 50 session_id_ip_limit: 30 +login_risk: + enabled: true + redis_addr: "127.0.0.1:13379" + redis_password: "" + redis_db: 0 + key_prefix: "login_risk:ip:" + fast_guard: + blocked_languages: [] + blocked_language_primary_tags: [] + blocked_timezones: [] + blocked_timezone_prefixes: [] tencent_im: # 本地联调腾讯云 IM UserSig;SDKAppID 和密钥必须与 room-service 保持一致。 sdk_app_id: 20036101 diff --git a/services/gateway-service/internal/app/app.go b/services/gateway-service/internal/app/app.go index 65ae34b3..45bf8c83 100644 --- a/services/gateway-service/internal/app/app.go +++ b/services/gateway-service/internal/app/app.go @@ -79,6 +79,7 @@ func New(cfg config.Config) (*App, error) { var appRegistryClient client.AppRegistryClient = client.NewGRPCAppRegistryClient(userConn) var walletClient client.WalletClient = client.NewGRPCWalletClient(walletConn) var messageClient client.MessageInboxClient = client.NewGRPCMessageInboxClient(activityConn) + var taskClient client.TaskClient = client.NewGRPCTaskClient(activityConn) appConfigReader, err := openAppConfigReader(cfg.AppConfig) if err != nil { _ = roomConn.Close() @@ -100,6 +101,7 @@ func New(cfg config.Config) (*App, error) { handler.SetAppRegistryClient(appRegistryClient) handler.SetWalletClient(walletClient) handler.SetMessageInboxClient(messageClient) + handler.SetTaskClient(taskClient) if appConfigReader != nil { handler.SetAppConfigReader(appConfigReader) } @@ -143,6 +145,17 @@ func New(cfg config.Config) (*App, error) { DisplayUserIDLimit: cfg.AuthRateLimit.DisplayUserIDLimit, SessionIDIPLimit: cfg.AuthRateLimit.SessionIDIPLimit, } + loginRiskConfig := httptransport.LoginRiskConfig{ + Enabled: cfg.LoginRisk.Enabled, + KeyPrefix: cfg.LoginRisk.KeyPrefix, + FastGuard: httptransport.LoginRiskFastGuardConfig{ + BlockedLanguages: cfg.LoginRisk.FastGuard.BlockedLanguages, + BlockedLanguagePrimaryTags: cfg.LoginRisk.FastGuard.BlockedLanguagePrimaryTags, + BlockedTimezones: cfg.LoginRisk.FastGuard.BlockedTimezones, + BlockedTimezonePrefixes: cfg.LoginRisk.FastGuard.BlockedTimezonePrefixes, + }, + } + handler.SetLoginRisk(loginRiskConfig) redisClose, err := configureAuthRateLimit(handler, cfg.AuthRateLimit, rateLimitConfig) if err != nil { if appConfigReader != nil { @@ -154,6 +167,21 @@ func New(cfg config.Config) (*App, error) { _ = activityConn.Close() return nil, err } + loginRiskRedisClose, err := configureLoginRisk(handler, cfg.LoginRisk, loginRiskConfig) + if err != nil { + if redisClose != nil { + _ = redisClose() + } + if appConfigReader != nil { + _ = appConfigReader.Close() + } + _ = roomConn.Close() + _ = userConn.Close() + _ = walletConn.Close() + _ = activityConn.Close() + return nil, err + } + redisClose = joinClose(redisClose, loginRiskRedisClose) verifier := auth.NewVerifier(cfg.JWTSecret) healthState := healthcheck.NewState(nodeID(), cfg.JWTSecret, roomConn) @@ -227,6 +255,40 @@ func configureAuthRateLimit(handler *httptransport.Handler, cfg config.AuthRateL return redisClient.Close, nil } +func configureLoginRisk(handler *httptransport.Handler, cfg config.LoginRiskConfig, riskConfig httptransport.LoginRiskConfig) (func() error, error) { + if !riskConfig.Enabled { + handler.SetLoginRisk(riskConfig) + return nil, nil + } + redisAddr := strings.TrimSpace(cfg.RedisAddr) + if redisAddr == "" { + handler.SetLoginRisk(riskConfig) + return nil, nil + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + redisClient, err := httptransport.NewLoginRiskRedisClient(ctx, redisAddr, cfg.RedisPassword, cfg.RedisDB) + if err != nil { + // IP 风控缓存按文档 fail-open:Redis 不可用不能阻断 gateway 启动或登录。 + handler.SetLoginRisk(riskConfig) + return nil, nil + } + handler.SetLoginRiskCache(riskConfig, httptransport.NewRedisLoginRiskCache(redisClient, riskConfig)) + return redisClient.Close, nil +} + +func joinClose(left func() error, right func() error) func() error { + if left == nil { + return right + } + if right == nil { + return left + } + return func() error { + return errors.Join(left(), right()) + } +} + // Run 启动 HTTP 服务。 func (a *App) Run() error { a.health.MarkHTTPServing() diff --git a/services/gateway-service/internal/client/task_client.go b/services/gateway-service/internal/client/task_client.go new file mode 100644 index 00000000..bde245c5 --- /dev/null +++ b/services/gateway-service/internal/client/task_client.go @@ -0,0 +1,32 @@ +package client + +import ( + "context" + + activityv1 "hyapp.local/api/proto/activity/v1" + + "google.golang.org/grpc" +) + +// TaskClient abstracts gateway access to activity-service task read and claim APIs. +type TaskClient interface { + ListUserTasks(ctx context.Context, req *activityv1.ListUserTasksRequest) (*activityv1.ListUserTasksResponse, error) + ClaimTaskReward(ctx context.Context, req *activityv1.ClaimTaskRewardRequest) (*activityv1.ClaimTaskRewardResponse, error) +} + +type grpcTaskClient struct { + client activityv1.TaskServiceClient +} + +// NewGRPCTaskClient builds a task client backed by activity-service TaskService. +func NewGRPCTaskClient(conn *grpc.ClientConn) TaskClient { + return &grpcTaskClient{client: activityv1.NewTaskServiceClient(conn)} +} + +func (c *grpcTaskClient) ListUserTasks(ctx context.Context, req *activityv1.ListUserTasksRequest) (*activityv1.ListUserTasksResponse, error) { + return c.client.ListUserTasks(ctx, req) +} + +func (c *grpcTaskClient) ClaimTaskReward(ctx context.Context, req *activityv1.ClaimTaskRewardRequest) (*activityv1.ClaimTaskRewardResponse, error) { + return c.client.ClaimTaskReward(ctx, req) +} diff --git a/services/gateway-service/internal/client/user_client.go b/services/gateway-service/internal/client/user_client.go index 4c157bbb..ab6eb487 100644 --- a/services/gateway-service/internal/client/user_client.go +++ b/services/gateway-service/internal/client/user_client.go @@ -15,6 +15,7 @@ type UserAuthClient interface { SetPassword(ctx context.Context, req *userv1.SetPasswordRequest) (*userv1.SetPasswordResponse, error) RefreshToken(ctx context.Context, req *userv1.RefreshTokenRequest) (*userv1.RefreshTokenResponse, error) Logout(ctx context.Context, req *userv1.LogoutRequest) (*userv1.LogoutResponse, error) + RecordLoginBlocked(ctx context.Context, req *userv1.RecordLoginBlockedRequest) (*userv1.RecordLoginBlockedResponse, error) } // UserIdentityClient 抽象 gateway 对 user-service 短号能力的依赖。 @@ -154,6 +155,10 @@ func (c *grpcUserAuthClient) Logout(ctx context.Context, req *userv1.LogoutReque return c.client.Logout(ctx, req) } +func (c *grpcUserAuthClient) RecordLoginBlocked(ctx context.Context, req *userv1.RecordLoginBlockedRequest) (*userv1.RecordLoginBlockedResponse, error) { + return c.client.RecordLoginBlocked(ctx, req) +} + func (c *grpcUserIdentityClient) GetUserIdentity(ctx context.Context, req *userv1.GetUserIdentityRequest) (*userv1.GetUserIdentityResponse, error) { return c.client.GetUserIdentity(ctx, req) } diff --git a/services/gateway-service/internal/config/config.go b/services/gateway-service/internal/config/config.go index 456698e1..c3924ba2 100644 --- a/services/gateway-service/internal/config/config.go +++ b/services/gateway-service/internal/config/config.go @@ -22,6 +22,7 @@ type Config struct { WalletServiceAddr string `yaml:"wallet_service_addr"` ActivityServiceAddr string `yaml:"activity_service_addr"` AuthRateLimit AuthRateLimitConfig `yaml:"auth_rate_limit"` + LoginRisk LoginRiskConfig `yaml:"login_risk"` AppConfig AppConfigConfig `yaml:"app_config"` TencentIM TencentIMConfig `yaml:"tencent_im"` TencentRTC TencentRTCConfig `yaml:"tencent_rtc"` @@ -29,6 +30,24 @@ type Config struct { Log logx.Config `yaml:"log"` } +// LoginRiskConfig 控制 gateway 登录入口快拦和 Redis 风控缓存读取。 +type LoginRiskConfig struct { + Enabled bool `yaml:"enabled"` + RedisAddr string `yaml:"redis_addr"` + RedisPassword string `yaml:"redis_password"` + RedisDB int `yaml:"redis_db"` + KeyPrefix string `yaml:"key_prefix"` + FastGuard LoginRiskFastGuardConfig `yaml:"fast_guard"` +} + +// LoginRiskFastGuardConfig 保存语言和时区弱信号阻断配置。 +type LoginRiskFastGuardConfig struct { + BlockedLanguages []string `yaml:"blocked_languages"` + BlockedLanguagePrimaryTags []string `yaml:"blocked_language_primary_tags"` + BlockedTimezones []string `yaml:"blocked_timezones"` + BlockedTimezonePrefixes []string `yaml:"blocked_timezone_prefixes"` +} + // AppConfigConfig 描述 gateway 读取后台 App 运行时配置的只读数据源。 type AppConfigConfig struct { // MySQLDSN 指向后台管理库 hyapp_admin;gateway 只读 admin_app_configs。 @@ -139,6 +158,11 @@ func Default() Config { DisplayUserIDLimit: 50, SessionIDIPLimit: 30, }, + LoginRisk: LoginRiskConfig{ + Enabled: true, + RedisAddr: "127.0.0.1:13379", + KeyPrefix: "login_risk:ip:", + }, TencentIM: TencentIMConfig{ UserSigTTL: 24 * time.Hour, }, @@ -202,6 +226,16 @@ func (cfg *Config) Normalize() error { cfg.ActivityServiceAddr = "127.0.0.1:13006" } cfg.AppConfig.MySQLDSN = strings.TrimSpace(cfg.AppConfig.MySQLDSN) + cfg.LoginRisk.RedisAddr = strings.TrimSpace(cfg.LoginRisk.RedisAddr) + cfg.LoginRisk.KeyPrefix = strings.TrimSpace(cfg.LoginRisk.KeyPrefix) + if cfg.LoginRisk.Enabled && cfg.LoginRisk.RedisAddr == "" { + cfg.LoginRisk.RedisAddr = strings.TrimSpace(cfg.AuthRateLimit.RedisAddr) + cfg.LoginRisk.RedisPassword = cfg.AuthRateLimit.RedisPassword + cfg.LoginRisk.RedisDB = cfg.AuthRateLimit.RedisDB + } + if cfg.LoginRisk.KeyPrefix == "" { + cfg.LoginRisk.KeyPrefix = "login_risk:ip:" + } cfg.TencentCOS.SecretID = strings.TrimSpace(cfg.TencentCOS.SecretID) cfg.TencentCOS.SecretKey = strings.TrimSpace(cfg.TencentCOS.SecretKey) cfg.TencentCOS.BucketName = strings.TrimSpace(cfg.TencentCOS.BucketName) diff --git a/services/gateway-service/internal/transport/http/auth_handler.go b/services/gateway-service/internal/transport/http/auth_handler.go index 1de2a15e..1ca02b51 100644 --- a/services/gateway-service/internal/transport/http/auth_handler.go +++ b/services/gateway-service/internal/transport/http/auth_handler.go @@ -3,6 +3,7 @@ package http import ( "net" "net/http" + "net/netip" "strings" "time" @@ -35,6 +36,9 @@ func (h *Handler) loginPassword(writer http.ResponseWriter, request *http.Reques Account string `json:"account"` Password string `json:"password"` DeviceID string `json:"device_id"` + Platform string `json:"platform"` + Language string `json:"language"` + Timezone string `json:"timezone"` } if !decode(writer, request, &body) { return @@ -47,13 +51,21 @@ func (h *Handler) loginPassword(writer http.ResponseWriter, request *http.Reques }) { return } + if !h.allowLoginRisk(writer, request, loginRiskInput{ + Channel: "account", + Platform: body.Platform, + Language: body.Language, + Timezone: body.Timezone, + }) { + return + } if h.userClient == nil { writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error") return } resp, err := h.userClient.LoginPassword(request.Context(), &userv1.LoginPasswordRequest{ - Meta: authRequestMeta(request, body.DeviceID), + Meta: authRequestMetaWithLoginContext(request, body.DeviceID, body.Platform, body.Language, body.Timezone), DisplayUserId: account, Password: body.Password, }) @@ -98,13 +110,21 @@ func (h *Handler) loginThirdParty(writer http.ResponseWriter, request *http.Requ }) { return } + if !h.allowLoginRisk(writer, request, loginRiskInput{ + Channel: body.Provider, + Platform: body.Platform, + Language: body.Language, + Timezone: body.Timezone, + }) { + return + } if h.userClient == nil { writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error") return } resp, err := h.userClient.LoginThirdParty(request.Context(), &userv1.LoginThirdPartyRequest{ - Meta: authRequestMeta(request, body.DeviceID), + Meta: authRequestMetaWithLoginContext(request, body.DeviceID, body.Platform, body.Language, body.Timezone), Provider: body.Provider, Credential: body.Credential, Username: body.Username, @@ -227,6 +247,10 @@ func (h *Handler) logout(writer http.ResponseWriter, request *http.Request) { // authRequestMeta 生成 user-service auth RPC 的统一追踪和审计元信息。 func authRequestMeta(request *http.Request, deviceID string) *userv1.RequestMeta { + return authRequestMetaWithLoginContext(request, deviceID, "", "", "") +} + +func authRequestMetaWithLoginContext(request *http.Request, deviceID string, platform string, language string, timezone string) *userv1.RequestMeta { return &userv1.RequestMeta{ RequestId: requestIDFromContext(request.Context()), Caller: "gateway-service", @@ -238,6 +262,9 @@ func authRequestMeta(request *http.Request, deviceID string) *userv1.RequestMeta CountryByIp: countryByIP(request), SessionId: auth.SessionIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), + Platform: strings.ToLower(strings.TrimSpace(platform)), + Language: strings.TrimSpace(language), + Timezone: strings.TrimSpace(timezone), } } @@ -267,20 +294,58 @@ func authData(token *userv1.AuthToken, isNewUser *bool) authTokenData { // clientIP 提取 gateway 看到的客户端地址。 func clientIP(request *http.Request) string { + for _, header := range []string{"X-Trusted-Client-IP", "X-Real-IP"} { + if ip := normalizeIP(request.Header.Get(header)); ip != "" { + return ip + } + } forwarded := strings.TrimSpace(request.Header.Get("X-Forwarded-For")) if forwarded != "" { parts := strings.Split(forwarded, ",") - return strings.TrimSpace(parts[0]) + for _, part := range parts { + ip := normalizeIP(part) + if ip == "" { + continue + } + if addr, err := netip.ParseAddr(ip); err == nil && isPublicClientAddr(addr) { + return ip + } + } } host, _, err := net.SplitHostPort(request.RemoteAddr) if err != nil { - return request.RemoteAddr + return normalizeIP(request.RemoteAddr) } return host } +func normalizeIP(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + if host, _, err := net.SplitHostPort(value); err == nil { + value = host + } + if addr, err := netip.ParseAddr(value); err == nil { + return addr.String() + } + return value +} + +func isPublicClientAddr(addr netip.Addr) bool { + return addr.IsValid() && + addr.IsGlobalUnicast() && + !addr.IsPrivate() && + !addr.IsLoopback() && + !addr.IsLinkLocalUnicast() && + !addr.IsLinkLocalMulticast() && + !addr.IsMulticast() && + !addr.IsUnspecified() +} + func countryByIP(request *http.Request) string { // country_by_ip 不接受 JSON body;只采信 gateway/边缘层写入的地域头。 for _, header := range []string{"CF-IPCountry", "CloudFront-Viewer-Country", "X-Vercel-IP-Country", "X-AppEngine-Country", "X-Country-Code"} { diff --git a/services/gateway-service/internal/transport/http/auth_rate_limiter_test.go b/services/gateway-service/internal/transport/http/auth_rate_limiter_test.go index 95fa476b..d331b8cc 100644 --- a/services/gateway-service/internal/transport/http/auth_rate_limiter_test.go +++ b/services/gateway-service/internal/transport/http/auth_rate_limiter_test.go @@ -92,7 +92,7 @@ func TestAuthRateKeysHashUntrustedComponents(t *testing.T) { if len(keys) != 4 { t.Fatalf("unexpected key count: got=%d keys=%v", len(keys), keys) } - if keys[0].key != "test:ip:"+authRateKeyPart(untrusted) { + if keys[0].key != "test:ip:"+authRateKeyPart(clientIP(request)) { t.Fatalf("ip key must use hashed component: %q", keys[0].key) } for _, key := range keys { diff --git a/services/gateway-service/internal/transport/http/handler.go b/services/gateway-service/internal/transport/http/handler.go index 2c97e2fb..7372f4b0 100644 --- a/services/gateway-service/internal/transport/http/handler.go +++ b/services/gateway-service/internal/transport/http/handler.go @@ -30,12 +30,15 @@ type Handler struct { appRegistryClient client.AppRegistryClient walletClient client.WalletClient messageClient client.MessageInboxClient + taskClient client.TaskClient appConfigReader appConfigReader tencentIM TencentIMConfig tencentRTC TencentRTCConfig objectUploader ObjectUploader authRateLimitConfig AuthRateLimitConfig authRateLimiter publicAuthRateLimiter + loginRiskConfig LoginRiskConfig + loginRiskCache loginRiskCache } // TencentIMConfig 是 gateway transport 层签发客户端腾讯云 IM 登录票据所需的配置。 @@ -65,7 +68,7 @@ type ObjectUploader interface { // NewHandler 装配 gateway HTTP handler 的内部依赖。 func NewHandler(roomClient client.RoomClient, userClient ...client.UserAuthClient) *Handler { config := defaultAuthRateLimitConfig() - handler := &Handler{roomClient: roomClient, authRateLimitConfig: config, authRateLimiter: newMemoryAuthRateLimiter(config)} + handler := &Handler{roomClient: roomClient, authRateLimitConfig: config, authRateLimiter: newMemoryAuthRateLimiter(config), loginRiskConfig: defaultLoginRiskConfig()} if len(userClient) > 0 { handler.userClient = userClient[0] } @@ -79,6 +82,7 @@ func NewHandlerWithClients(roomClient client.RoomClient, userClient client.UserA roomClient: roomClient, userClient: userClient, userIdentityClient: userIdentityClient, + loginRiskConfig: defaultLoginRiskConfig(), } handler.SetAuthRateLimit(defaultAuthRateLimitConfig()) if len(userProfileClient) > 0 { @@ -96,6 +100,7 @@ func NewHandlerWithConfig(roomClient client.RoomClient, roomGuardClient client.R userClient: userClient, userIdentityClient: userIdentityClient, tencentIM: tencentIM, + loginRiskConfig: defaultLoginRiskConfig(), } handler.SetAuthRateLimit(defaultAuthRateLimitConfig()) if len(userProfileClient) > 0 { @@ -150,6 +155,11 @@ func (h *Handler) SetMessageInboxClient(messageClient client.MessageInboxClient) h.messageClient = messageClient } +// SetTaskClient 注入 activity-service task client。 +func (h *Handler) SetTaskClient(taskClient client.TaskClient) { + h.taskClient = taskClient +} + // SetAppConfigReader 注入后台 App 配置读取依赖。 func (h *Handler) SetAppConfigReader(reader appConfigReader) { h.appConfigReader = reader diff --git a/services/gateway-service/internal/transport/http/login_risk.go b/services/gateway-service/internal/transport/http/login_risk.go new file mode 100644 index 00000000..82d9aff5 --- /dev/null +++ b/services/gateway-service/internal/transport/http/login_risk.go @@ -0,0 +1,281 @@ +package http + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "log/slog" + "net/http" + "regexp" + "strings" + "sync" + "time" + + "github.com/redis/go-redis/v9" + userv1 "hyapp.local/api/proto/user/v1" + "hyapp/pkg/appcode" + "hyapp/pkg/logx" +) + +const ( + loginRiskBlockLanguage = "language" + loginRiskBlockTimezone = "timezone" + loginRiskBlockIPCache = "ip_cache" +) + +var languageTagPattern = regexp.MustCompile(`^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8}){0,3}$`) + +// LoginRiskConfig 描述 gateway 登录入口快拦策略。 +type LoginRiskConfig struct { + Enabled bool + KeyPrefix string + FastGuard LoginRiskFastGuardConfig +} + +// LoginRiskFastGuardConfig 保存弱信号阻断列表;命中才拒绝,缺失或非法只审计不推断。 +type LoginRiskFastGuardConfig struct { + BlockedLanguages []string + BlockedLanguagePrimaryTags []string + BlockedTimezones []string + BlockedTimezonePrefixes []string +} + +type loginRiskInput struct { + Channel string + Platform string + Language string + Timezone string +} + +type loginRiskDecision struct { + Decision string `json:"decision"` + Country string `json:"country"` + Source string `json:"source"` + CheckedAtMs int64 `json:"checked_at_ms"` + ExpiresAtMs int64 `json:"expires_at_ms"` +} + +type loginRiskCache interface { + GetIPDecision(ctx context.Context, appCode string, clientIPHash string) (loginRiskDecision, bool, error) + RevokedSessionExists(ctx context.Context, appCode string, sessionID string) (bool, error) +} + +type redisLoginRiskCache struct { + client *redis.Client + keyPrefix string +} + +type memoryLoginRiskCache struct { + mu sync.Mutex + ip map[string]loginRiskDecision + revoked map[string]bool +} + +func defaultLoginRiskConfig() LoginRiskConfig { + return LoginRiskConfig{ + Enabled: true, + KeyPrefix: "login_risk:ip:", + } +} + +func normalizeLoginRiskConfig(config LoginRiskConfig) LoginRiskConfig { + defaults := defaultLoginRiskConfig() + if strings.TrimSpace(config.KeyPrefix) == "" { + config.KeyPrefix = defaults.KeyPrefix + } + return config +} + +func NewLoginRiskRedisClient(ctx context.Context, addr string, password string, db int) (*redis.Client, error) { + client := redis.NewClient(&redis.Options{ + Addr: strings.TrimSpace(addr), + Password: password, + DB: db, + }) + if err := client.Ping(ctx).Err(); err != nil { + _ = client.Close() + return nil, err + } + return client, nil +} + +func NewRedisLoginRiskCache(client *redis.Client, config LoginRiskConfig) loginRiskCache { + config = normalizeLoginRiskConfig(config) + return &redisLoginRiskCache{client: client, keyPrefix: config.KeyPrefix} +} + +func newMemoryLoginRiskCache() *memoryLoginRiskCache { + return &memoryLoginRiskCache{ + ip: make(map[string]loginRiskDecision), + revoked: make(map[string]bool), + } +} + +func (c *redisLoginRiskCache) GetIPDecision(ctx context.Context, appCode string, clientIPHash string) (loginRiskDecision, bool, error) { + raw, err := c.client.Get(ctx, ipDecisionKey(c.keyPrefix, appCode, clientIPHash)).Result() + if errors.Is(err, redis.Nil) { + return loginRiskDecision{}, false, nil + } + if err != nil { + return loginRiskDecision{}, false, err + } + var decision loginRiskDecision + if err := json.Unmarshal([]byte(raw), &decision); err != nil { + return loginRiskDecision{}, false, err + } + return decision, true, nil +} + +func (c *redisLoginRiskCache) RevokedSessionExists(ctx context.Context, appCode string, sessionID string) (bool, error) { + result, err := c.client.Exists(ctx, revokedSessionKey(appCode, sessionID)).Result() + if err != nil { + return false, err + } + return result > 0, nil +} + +func (c *memoryLoginRiskCache) GetIPDecision(_ context.Context, appCode string, clientIPHash string) (loginRiskDecision, bool, error) { + c.mu.Lock() + defer c.mu.Unlock() + decision, ok := c.ip[ipDecisionKey(defaultLoginRiskConfig().KeyPrefix, appCode, clientIPHash)] + return decision, ok, nil +} + +func (c *memoryLoginRiskCache) RevokedSessionExists(_ context.Context, appCode string, sessionID string) (bool, error) { + c.mu.Lock() + defer c.mu.Unlock() + return c.revoked[revokedSessionKey(appCode, sessionID)], nil +} + +func (c *memoryLoginRiskCache) setIPDecision(appCode string, clientIP string, decision loginRiskDecision) { + c.mu.Lock() + defer c.mu.Unlock() + c.ip[ipDecisionKey(defaultLoginRiskConfig().KeyPrefix, appCode, hashLoginRiskIP(clientIP))] = decision +} + +func (c *memoryLoginRiskCache) setRevokedSession(appCode string, sessionID string) { + c.mu.Lock() + defer c.mu.Unlock() + c.revoked[revokedSessionKey(appCode, sessionID)] = true +} + +func ipDecisionKey(prefix string, appCode string, clientIPHash string) string { + return strings.TrimSpace(prefix) + appcode.Normalize(appCode) + ":" + strings.TrimSpace(clientIPHash) +} + +func revokedSessionKey(appCode string, sessionID string) string { + return "auth:revoked_session:" + appcode.Normalize(appCode) + ":" + strings.TrimSpace(sessionID) +} + +func hashLoginRiskIP(ip string) string { + sum := sha256.Sum256([]byte(strings.TrimSpace(ip))) + return hex.EncodeToString(sum[:]) +} + +func (h *Handler) SetLoginRisk(config LoginRiskConfig) { + h.loginRiskConfig = normalizeLoginRiskConfig(config) +} + +func (h *Handler) SetLoginRiskCache(config LoginRiskConfig, cache loginRiskCache) { + h.loginRiskConfig = normalizeLoginRiskConfig(config) + h.loginRiskCache = cache +} + +func (h *Handler) allowLoginRisk(writer http.ResponseWriter, request *http.Request, input loginRiskInput) bool { + config := normalizeLoginRiskConfig(h.loginRiskConfig) + if !config.Enabled { + return true + } + if reason := config.FastGuard.blockReason(input.Language, input.Timezone); reason != "" { + h.recordLoginBlocked(request, input, reason) + writeError(writer, request, http.StatusForbidden, codeAuthLoginBlocked, "login blocked") + return false + } + if h.loginRiskCache == nil { + return true + } + + clientIPHash := hashLoginRiskIP(clientIP(request)) + decision, ok, err := h.loginRiskCache.GetIPDecision(request.Context(), appcode.FromContext(request.Context()), clientIPHash) + if err != nil { + logx.Warn(request.Context(), "login_ip_decision_cache_read_failed", slog.String("component", "gateway_login_risk"), slog.String("client_ip_hash", clientIPHash), slog.String("error", err.Error())) + return true + } + if ok && strings.EqualFold(decision.Decision, "blocked") { + h.recordLoginBlocked(request, input, loginRiskBlockIPCache) + writeError(writer, request, http.StatusForbidden, codeAuthLoginBlocked, "login blocked") + return false + } + return true +} + +func (h *Handler) recordLoginBlocked(request *http.Request, input loginRiskInput, reason string) { + if h.userClient == nil { + return + } + ctx, cancel := context.WithTimeout(request.Context(), 500*time.Millisecond) + defer cancel() + _, err := h.userClient.RecordLoginBlocked(ctx, &userv1.RecordLoginBlockedRequest{ + Meta: authRequestMetaWithLoginContext(request, "", input.Platform, input.Language, input.Timezone), + Channel: input.Channel, + Platform: input.Platform, + Language: input.Language, + Timezone: input.Timezone, + BlockReason: reason, + }) + if err != nil { + logx.Warn(request.Context(), "login_fast_guard_audit_failed", slog.String("component", "gateway_login_risk"), slog.String("block_reason", reason), slog.String("error", err.Error())) + } +} + +func (g LoginRiskFastGuardConfig) blockReason(language string, timezone string) string { + if blockedLanguage(language, g.BlockedLanguages, g.BlockedLanguagePrimaryTags) { + return loginRiskBlockLanguage + } + if blockedTimezone(timezone, g.BlockedTimezones, g.BlockedTimezonePrefixes) { + return loginRiskBlockTimezone + } + return "" +} + +func blockedLanguage(language string, exact []string, primaryTags []string) bool { + language = strings.TrimSpace(language) + if language == "" || !languageTagPattern.MatchString(language) { + return false + } + for _, blocked := range exact { + if strings.EqualFold(strings.TrimSpace(blocked), language) { + return true + } + } + primary := strings.ToLower(strings.Split(language, "-")[0]) + for _, blocked := range primaryTags { + if primary == strings.ToLower(strings.TrimSpace(blocked)) { + return true + } + } + return false +} + +func blockedTimezone(timezoneName string, exact []string, prefixes []string) bool { + timezoneName = strings.TrimSpace(timezoneName) + if timezoneName == "" { + return false + } + if _, err := time.LoadLocation(timezoneName); err != nil { + return false + } + for _, blocked := range exact { + if strings.EqualFold(strings.TrimSpace(blocked), timezoneName) { + return true + } + } + for _, prefix := range prefixes { + if strings.HasPrefix(timezoneName, strings.TrimSpace(prefix)) { + return true + } + } + return false +} diff --git a/services/gateway-service/internal/transport/http/middleware.go b/services/gateway-service/internal/transport/http/middleware.go index 49e1be80..b9092db9 100644 --- a/services/gateway-service/internal/transport/http/middleware.go +++ b/services/gateway-service/internal/transport/http/middleware.go @@ -2,6 +2,7 @@ package http import ( "context" + "log/slog" "net/http" "strings" "time" @@ -9,17 +10,18 @@ import ( userv1 "hyapp.local/api/proto/user/v1" "hyapp/pkg/appcode" "hyapp/pkg/logx" + "hyapp/pkg/xerr" "hyapp/services/gateway-service/internal/auth" ) // apiHandler 包装需要 app 解析和 access token 的业务 API。 func (h *Handler) apiHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler { - return withRequestID(withAccessLog(h.withResolvedApp(withAuth(jwtVerifier, next)))) + return withRequestID(withAccessLog(h.withResolvedApp(h.withAuth(jwtVerifier, next)))) } // profileAPIHandler 包装需要 app 解析、access token 和完整资料门禁的业务 API。 func (h *Handler) profileAPIHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler { - return withRequestID(withAccessLog(h.withResolvedApp(withAuth(jwtVerifier, requireCompletedProfile(next))))) + return withRequestID(withAccessLog(h.withResolvedApp(h.withAuth(jwtVerifier, requireCompletedProfile(next))))) } // publicAPIHandler 包装只需要 app 解析的公开 API;登录/注册会用解析结果创建对应 App 的用户。 @@ -35,19 +37,38 @@ func withAccessLog(next http.Handler) http.Handler { // withAuth 在 transport 层完成鉴权失败响应写入。 // auth 包只返回身份校验结果,避免底层鉴权模块绑定 gateway 的 JSON envelope。 -func withAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler { +func (h *Handler) withAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { claims, err := jwtVerifier.Verify(request.Header.Get("Authorization")) if err != nil { writeError(writer, request, http.StatusUnauthorized, codeUnauthorized, "unauthorized") return } + if strings.TrimSpace(claims.SessionID) == "" { + writeError(writer, request, http.StatusUnauthorized, string(xerr.SessionRevoked), "unauthorized") + return + } ctx := auth.WithClaims(request.Context(), claims) + revoked, err := h.revokedSession(ctx, claims.AppCode, claims.SessionID) + if err != nil { + logx.Warn(ctx, "session_denylist_read_failed", slog.String("component", "gateway_auth"), slog.String("session_id", claims.SessionID), slog.String("error", err.Error())) + } + if revoked { + writeError(writer, request.WithContext(ctx), http.StatusUnauthorized, string(xerr.SessionRevoked), "unauthorized") + return + } next.ServeHTTP(writer, request.WithContext(ctx)) }) } +func (h *Handler) revokedSession(ctx context.Context, appCode string, sessionID string) (bool, error) { + if h.loginRiskCache == nil { + return false, nil + } + return h.loginRiskCache.RevokedSessionExists(ctx, appCode, sessionID) +} + // withResolvedApp 在入口处把包名或显式 app_code 解析成内部租户键并写入 context。 // 公开接口依赖请求头解析;鉴权接口稍后会由 token claims 再覆盖一次,防止跨 App 串 token。 func (h *Handler) withResolvedApp(next http.Handler) http.Handler { diff --git a/services/gateway-service/internal/transport/http/response.go b/services/gateway-service/internal/transport/http/response.go index 22b7e542..3e292cd3 100644 --- a/services/gateway-service/internal/transport/http/response.go +++ b/services/gateway-service/internal/transport/http/response.go @@ -22,6 +22,7 @@ const ( codeUnauthorized = "UNAUTHORIZED" codePermissionDenied = "PERMISSION_DENIED" codeProfileRequired = "PROFILE_REQUIRED" + codeAuthLoginBlocked = "AUTH_LOGIN_BLOCKED" codeNotFound = "NOT_FOUND" codeRateLimited = "RATE_LIMITED" codeUpstreamError = "UPSTREAM_ERROR" diff --git a/services/gateway-service/internal/transport/http/response_test.go b/services/gateway-service/internal/transport/http/response_test.go index af76b00b..4193b9bd 100644 --- a/services/gateway-service/internal/transport/http/response_test.go +++ b/services/gateway-service/internal/transport/http/response_test.go @@ -152,10 +152,12 @@ type fakeUserAuthClient struct { lastSetPassword *userv1.SetPasswordRequest lastRefresh *userv1.RefreshTokenRequest lastLogout *userv1.LogoutRequest + lastBlocked *userv1.RecordLoginBlockedRequest loginPasswordCount int loginThirdCount int refreshCount int logoutCount int + blockedCount int loginErr error } @@ -239,6 +241,14 @@ type fakeMessageInboxClient struct { err error } +type fakeTaskClient struct { + lastList *activityv1.ListUserTasksRequest + lastClaim *activityv1.ClaimTaskRewardRequest + listResp *activityv1.ListUserTasksResponse + claimResp *activityv1.ClaimTaskRewardResponse + err error +} + func (f *fakeUserAuthClient) LoginPassword(_ context.Context, req *userv1.LoginPasswordRequest) (*userv1.AuthResponse, error) { f.lastLoginPassword = req f.loginPasswordCount++ @@ -287,6 +297,12 @@ func (f *fakeUserAuthClient) Logout(_ context.Context, req *userv1.LogoutRequest return &userv1.LogoutResponse{Revoked: true}, nil } +func (f *fakeUserAuthClient) RecordLoginBlocked(_ context.Context, req *userv1.RecordLoginBlockedRequest) (*userv1.RecordLoginBlockedResponse, error) { + f.lastBlocked = req + f.blockedCount++ + return &userv1.RecordLoginBlockedResponse{Recorded: true}, nil +} + func (f *fakeUserProfileClient) GetUser(_ context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) { f.lastGet = req f.getRequests = append(f.getRequests, req) @@ -559,6 +575,28 @@ func (f *fakeMessageInboxClient) DeleteInboxMessage(_ context.Context, req *acti return &activityv1.DeleteInboxMessageResponse{MessageId: req.GetMessageId(), Deleted: true}, nil } +func (f *fakeTaskClient) ListUserTasks(_ context.Context, req *activityv1.ListUserTasksRequest) (*activityv1.ListUserTasksResponse, error) { + f.lastList = req + if f.err != nil { + return nil, f.err + } + if f.listResp != nil { + return f.listResp, nil + } + return &activityv1.ListUserTasksResponse{}, nil +} + +func (f *fakeTaskClient) ClaimTaskReward(_ context.Context, req *activityv1.ClaimTaskRewardRequest) (*activityv1.ClaimTaskRewardResponse, error) { + f.lastClaim = req + if f.err != nil { + return nil, f.err + } + if f.claimResp != nil { + return f.claimResp, nil + } + return &activityv1.ClaimTaskRewardResponse{}, nil +} + func (f *fakeUserHostClient) GetHostProfile(_ context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) { f.lastHost = req if f.hostErr != nil { @@ -1539,7 +1577,7 @@ func TestAccountLoginRateLimitByAccount(t *testing.T) { func TestAccountLoginAcceptsAccountField(t *testing.T) { userClient := &fakeUserAuthClient{} router := NewHandler(&fakeRoomClient{}, userClient).Routes(auth.NewVerifier("secret")) - body := []byte(`{"account":"123456","password":"1234567","device_id":"ios"}`) + body := []byte(`{"account":"123456","password":"1234567","device_id":"ios","platform":"ios","language":"en-US","timezone":"America/Los_Angeles"}`) request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/account/login", bytes.NewReader(body)) request.Header.Set("X-Request-ID", "req-account-login") recorder := httptest.NewRecorder() @@ -1553,6 +1591,76 @@ func TestAccountLoginAcceptsAccountField(t *testing.T) { if userClient.lastLoginPassword.GetPassword() != "1234567" || userClient.lastLoginPassword.GetMeta().GetDeviceId() != "ios" { t.Fatalf("password login payload mismatch: %+v", userClient.lastLoginPassword) } + if userClient.lastLoginPassword.GetMeta().GetPlatform() != "ios" || userClient.lastLoginPassword.GetMeta().GetLanguage() != "en-US" || userClient.lastLoginPassword.GetMeta().GetTimezone() != "America/Los_Angeles" { + t.Fatalf("password login risk meta mismatch: %+v", userClient.lastLoginPassword.GetMeta()) + } +} + +func TestLoginRiskFastGuardBlocksLanguage(t *testing.T) { + userClient := &fakeUserAuthClient{} + handler := NewHandler(&fakeRoomClient{}, userClient) + handler.SetLoginRisk(LoginRiskConfig{ + Enabled: true, + FastGuard: LoginRiskFastGuardConfig{ + BlockedLanguagePrimaryTags: []string{"zh"}, + }, + }) + router := handler.Routes(auth.NewVerifier("secret")) + body := []byte(`{"account":"123456","password":"1234567","device_id":"ios","platform":"ios","language":"zh-CN","timezone":"America/Los_Angeles"}`) + request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/account/login", bytes.NewReader(body)) + request.Header.Set("X-Request-ID", "req-login-risk-language") + request.Header.Set("X-Forwarded-For", "203.0.113.50") + recorder := httptest.NewRecorder() + + router.ServeHTTP(recorder, request) + + assertEnvelope(t, recorder, http.StatusForbidden, codeAuthLoginBlocked, "req-login-risk-language") + if userClient.loginPasswordCount != 0 { + t.Fatalf("blocked login must not reach LoginPassword, count=%d", userClient.loginPasswordCount) + } + if userClient.blockedCount != 1 || userClient.lastBlocked.GetBlockReason() != loginRiskBlockLanguage { + t.Fatalf("blocked audit mismatch: count=%d req=%+v", userClient.blockedCount, userClient.lastBlocked) + } +} + +func TestLoginRiskIPCacheBlocksLogin(t *testing.T) { + userClient := &fakeUserAuthClient{} + cache := newMemoryLoginRiskCache() + cache.setIPDecision("lalu", "203.0.113.51", loginRiskDecision{Decision: "blocked", Country: "CN"}) + handler := NewHandler(&fakeRoomClient{}, userClient) + handler.SetLoginRiskCache(defaultLoginRiskConfig(), cache) + router := handler.Routes(auth.NewVerifier("secret")) + body := []byte(`{"provider":"google","credential":"token","device_id":"dev-1","platform":"ios","language":"en-US","timezone":"America/Los_Angeles"}`) + request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/third-party/login", bytes.NewReader(body)) + request.Header.Set("X-Request-ID", "req-login-risk-cache") + request.Header.Set("X-Forwarded-For", "203.0.113.51") + recorder := httptest.NewRecorder() + + router.ServeHTTP(recorder, request) + + assertEnvelope(t, recorder, http.StatusForbidden, codeAuthLoginBlocked, "req-login-risk-cache") + if userClient.loginThirdCount != 0 { + t.Fatalf("blocked login must not reach LoginThirdParty, count=%d", userClient.loginThirdCount) + } + if userClient.lastBlocked.GetBlockReason() != loginRiskBlockIPCache { + t.Fatalf("blocked audit reason mismatch: %+v", userClient.lastBlocked) + } +} + +func TestSessionDenylistRejectsAuthenticatedRequest(t *testing.T) { + cache := newMemoryLoginRiskCache() + cache.setRevokedSession("lalu", "sess-test") + handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}) + handler.SetLoginRiskCache(defaultLoginRiskConfig(), cache) + router := handler.Routes(auth.NewVerifier("secret")) + request := httptest.NewRequest(http.MethodGet, "/api/v1/users/me", nil) + request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) + request.Header.Set("X-Request-ID", "req-session-revoked") + recorder := httptest.NewRecorder() + + router.ServeHTTP(recorder, request) + + assertEnvelope(t, recorder, http.StatusUnauthorized, string(xerr.SessionRevoked), "req-session-revoked") } func TestRefreshRateLimitByDeviceID(t *testing.T) { @@ -1737,6 +1845,81 @@ func TestMessageTabsAndInboxRoutesUseAuthenticatedUser(t *testing.T) { } } +func TestTaskTabsAndClaimRoutesUseAuthenticatedUser(t *testing.T) { + taskClient := &fakeTaskClient{listResp: &activityv1.ListUserTasksResponse{ + Sections: []*activityv1.TaskSection{{ + Section: "daily", + Items: []*activityv1.TaskItem{{ + TaskId: "daily-send-message", + TaskType: "daily", + Category: "interaction", + MetricType: "send_message", + Title: "发消息", + TargetValue: 1, + TargetUnit: "次", + ProgressValue: 1, + RewardCoinAmount: 10, + Status: "claimable", + Claimable: true, + TaskDay: "2026-05-09", + ServerTimeMs: 1778256000000, + NextRefreshAtMs: 1778342400000, + SortOrder: 10, + Version: 2, + }}, + ServerTimeMs: 1778256000000, + NextRefreshAtMs: 1778342400000, + }}, + ServerTimeMs: 1778256000000, + NextRefreshAtMs: 1778342400000, + }, claimResp: &activityv1.ClaimTaskRewardResponse{ + ClaimId: "claim-1", + TaskId: "daily-send-message", + TaskType: "daily", + TaskDay: "2026-05-09", + RewardCoinAmount: 10, + Status: "granted", + WalletTransactionId: "wtx-1", + GrantedAtMs: 1778256000100, + Claimed: true, + }} + handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}) + handler.SetTaskClient(taskClient) + router := handler.Routes(auth.NewVerifier("secret")) + + request := httptest.NewRequest(http.MethodGet, "/api/v1/tasks/tabs", nil) + request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) + request.Header.Set("X-Request-ID", "req-task-tabs") + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Fatalf("task tabs status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) + } + if taskClient.lastList == nil || taskClient.lastList.GetUserId() != 42 || taskClient.lastList.GetMeta().GetAppCode() != "lalu" || taskClient.lastList.GetMeta().GetRequestId() != "req-task-tabs" { + t.Fatalf("task tabs request mismatch: %+v", taskClient.lastList) + } + + request = httptest.NewRequest(http.MethodPost, "/api/v1/tasks/claim", bytes.NewReader([]byte(`{"task_id":"daily-send-message","task_type":"daily","task_day":"2026-05-09","command_id":"cmd-task-claim"}`))) + request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) + request.Header.Set("X-Request-ID", "req-task-claim") + recorder = httptest.NewRecorder() + router.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Fatalf("task claim status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) + } + if taskClient.lastClaim == nil || taskClient.lastClaim.GetUserId() != 42 || taskClient.lastClaim.GetTaskId() != "daily-send-message" || taskClient.lastClaim.GetTaskType() != "daily" || taskClient.lastClaim.GetTaskDay() != "2026-05-09" || taskClient.lastClaim.GetCommandId() != "cmd-task-claim" { + t.Fatalf("task claim request mismatch: %+v", taskClient.lastClaim) + } + var response responseEnvelope + if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { + t.Fatalf("decode task claim response failed: %v", err) + } + data, ok := response.Data.(map[string]any) + if response.Code != codeOK || !ok || data["task_id"] != "daily-send-message" || data["status"] != "granted" || data["wallet_transaction_id"] != "wtx-1" { + t.Fatalf("task claim envelope mismatch: %+v", response) + } +} + func TestMessageReadAllReadAndDeleteRoutes(t *testing.T) { messageClient := &fakeMessageInboxClient{} handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}) diff --git a/services/gateway-service/internal/transport/http/router.go b/services/gateway-service/internal/transport/http/router.go index 3f455ed9..672580c7 100644 --- a/services/gateway-service/internal/transport/http/router.go +++ b/services/gateway-service/internal/transport/http/router.go @@ -71,6 +71,8 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler { mux.Handle(apiV1Prefix+"/messages/read-all", h.profileAPIHandler(jwtVerifier, h.markInboxSectionRead)) mux.Handle(apiV1Prefix+"/messages/", h.profileAPIHandler(jwtVerifier, h.inboxMessageByID)) mux.Handle(apiV1Prefix+"/messages", h.profileAPIHandler(jwtVerifier, h.listInboxMessages)) + mux.Handle(apiV1Prefix+"/tasks/tabs", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.listTaskTabs))) + mux.Handle(apiV1Prefix+"/tasks/claim", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.claimTaskReward))) mux.Handle(apiV1Prefix+"/wallet/me/balances", h.profileAPIHandler(jwtVerifier, h.getMyBalances)) mux.Handle(apiV1Prefix+"/wallet/coin-seller/transfer", h.profileAPIHandler(jwtVerifier, h.transferCoinFromSeller)) diff --git a/services/gateway-service/internal/transport/http/task_handler.go b/services/gateway-service/internal/transport/http/task_handler.go new file mode 100644 index 00000000..f66e5d2e --- /dev/null +++ b/services/gateway-service/internal/transport/http/task_handler.go @@ -0,0 +1,186 @@ +package http + +import ( + "net/http" + "strings" + + activityv1 "hyapp.local/api/proto/activity/v1" + "hyapp/services/gateway-service/internal/auth" +) + +type taskSectionData struct { + Section string `json:"section"` + Items []taskItemData `json:"items"` + ServerTimeMS int64 `json:"server_time_ms"` + NextRefreshAtMS int64 `json:"next_refresh_at_ms"` +} + +type taskItemData struct { + TaskID string `json:"task_id"` + TaskType string `json:"task_type"` + Category string `json:"category"` + MetricType string `json:"metric_type"` + Title string `json:"title"` + Description string `json:"description"` + TargetValue int64 `json:"target_value"` + TargetUnit string `json:"target_unit"` + ProgressValue int64 `json:"progress_value"` + RewardCoinAmount int64 `json:"reward_coin_amount"` + Status string `json:"status"` + Claimable bool `json:"claimable"` + TaskDay string `json:"task_day"` + ServerTimeMS int64 `json:"server_time_ms"` + NextRefreshAtMS int64 `json:"next_refresh_at_ms"` + SortOrder int32 `json:"sort_order"` + Version int64 `json:"version"` +} + +type taskClaimData struct { + ClaimID string `json:"claim_id"` + TaskID string `json:"task_id"` + TaskType string `json:"task_type"` + TaskDay string `json:"task_day"` + RewardCoinAmount int64 `json:"reward_coin_amount"` + Status string `json:"status"` + WalletTransactionID string `json:"wallet_transaction_id"` + GrantedAtMS int64 `json:"granted_at_ms"` + Claimed bool `json:"claimed"` +} + +type taskClaimRequestBody struct { + TaskID string `json:"task_id"` + TaskIDCamel string `json:"taskId"` + TaskType string `json:"task_type"` + TaskTypeCamel string `json:"taskType"` + TaskDay string `json:"task_day"` + TaskDayCamel string `json:"taskDay"` + CommandID string `json:"command_id"` + CommandIDCamel string `json:"commandId"` +} + +func (b taskClaimRequestBody) normalizedTaskID() string { + if value := strings.TrimSpace(b.TaskID); value != "" { + return value + } + return strings.TrimSpace(b.TaskIDCamel) +} + +func (b taskClaimRequestBody) normalizedTaskType() string { + if value := strings.TrimSpace(b.TaskType); value != "" { + return value + } + return strings.TrimSpace(b.TaskTypeCamel) +} + +func (b taskClaimRequestBody) normalizedTaskDay() string { + if value := strings.TrimSpace(b.TaskDay); value != "" { + return value + } + return strings.TrimSpace(b.TaskDayCamel) +} + +func (b taskClaimRequestBody) normalizedCommandID() string { + if value := strings.TrimSpace(b.CommandID); value != "" { + return value + } + return strings.TrimSpace(b.CommandIDCamel) +} + +// listTaskTabs 返回当前用户任务页;进度由 activity-service 事实表决定。 +func (h *Handler) listTaskTabs(writer http.ResponseWriter, request *http.Request) { + if h.taskClient == nil { + writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error") + return + } + resp, err := h.taskClient.ListUserTasks(request.Context(), &activityv1.ListUserTasksRequest{ + Meta: activityMeta(request), + UserId: auth.UserIDFromContext(request.Context()), + }) + if err != nil { + writeRPCError(writer, request, err) + return + } + sections := make([]taskSectionData, 0, len(resp.GetSections())) + for _, section := range resp.GetSections() { + items := make([]taskItemData, 0, len(section.GetItems())) + for _, item := range section.GetItems() { + items = append(items, taskItemFromProto(item)) + } + sections = append(sections, taskSectionData{ + Section: section.GetSection(), + Items: items, + ServerTimeMS: section.GetServerTimeMs(), + NextRefreshAtMS: section.GetNextRefreshAtMs(), + }) + } + writeOK(writer, request, map[string]any{ + "sections": sections, + "server_time_ms": resp.GetServerTimeMs(), + "next_refresh_at_ms": resp.GetNextRefreshAtMs(), + }) +} + +// claimTaskReward 只透传当前用户身份和幂等命令,完成校验及发奖在 activity-service 内完成。 +func (h *Handler) claimTaskReward(writer http.ResponseWriter, request *http.Request) { + if h.taskClient == nil { + writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error") + return + } + var body taskClaimRequestBody + if !decode(writer, request, &body) { + return + } + taskID := body.normalizedTaskID() + taskType := body.normalizedTaskType() + taskDay := body.normalizedTaskDay() + commandID := commandIDOrNew(body.normalizedCommandID()) + if taskID == "" || taskType == "" || taskDay == "" { + writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument") + return + } + resp, err := h.taskClient.ClaimTaskReward(request.Context(), &activityv1.ClaimTaskRewardRequest{ + Meta: activityMeta(request), + UserId: auth.UserIDFromContext(request.Context()), + TaskId: taskID, + TaskType: taskType, + TaskDay: taskDay, + CommandId: commandID, + }) + if err != nil { + writeRPCError(writer, request, err) + return + } + writeOK(writer, request, taskClaimData{ + ClaimID: resp.GetClaimId(), + TaskID: resp.GetTaskId(), + TaskType: resp.GetTaskType(), + TaskDay: resp.GetTaskDay(), + RewardCoinAmount: resp.GetRewardCoinAmount(), + Status: resp.GetStatus(), + WalletTransactionID: resp.GetWalletTransactionId(), + GrantedAtMS: resp.GetGrantedAtMs(), + Claimed: resp.GetClaimed(), + }) +} + +func taskItemFromProto(item *activityv1.TaskItem) taskItemData { + return taskItemData{ + TaskID: item.GetTaskId(), + TaskType: item.GetTaskType(), + Category: item.GetCategory(), + MetricType: item.GetMetricType(), + Title: item.GetTitle(), + Description: item.GetDescription(), + TargetValue: item.GetTargetValue(), + TargetUnit: item.GetTargetUnit(), + ProgressValue: item.GetProgressValue(), + RewardCoinAmount: item.GetRewardCoinAmount(), + Status: item.GetStatus(), + Claimable: item.GetClaimable(), + TaskDay: item.GetTaskDay(), + ServerTimeMS: item.GetServerTimeMs(), + NextRefreshAtMS: item.GetNextRefreshAtMs(), + SortOrder: item.GetSortOrder(), + Version: item.GetVersion(), + } +} diff --git a/services/user-service/configs/config.docker.yaml b/services/user-service/configs/config.docker.yaml index b56e3bfb..0ce36c27 100644 --- a/services/user-service/configs/config.docker.yaml +++ b/services/user-service/configs/config.docker.yaml @@ -22,11 +22,6 @@ third_party: project_id: "lalu-party" allowed_sign_in_providers: - "google.com" -region_rebuild_worker: - enabled: true - poll_interval_sec: 60 - lock_ttl_sec: 30 - batch_size: 500 invite_recharge_worker: enabled: true wallet_mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=Local" @@ -37,11 +32,19 @@ 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 +login_risk: + enabled: true + redis_addr: "redis:6379" + redis_password: "" + redis_db: 0 + unsupported_countries: + - "CN" + blocked_ttl_sec: 2592000 + allowed_ttl_sec: 604800 + unknown_ttl_sec: 1800 + provider_timeout_ms: 800 + providers: + - "edge_country" 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 e2ca3627..ca97259c 100644 --- a/services/user-service/configs/config.tencent.example.yaml +++ b/services/user-service/configs/config.tencent.example.yaml @@ -22,11 +22,6 @@ third_party: project_id: "lalu-party" allowed_sign_in_providers: - "google.com" -region_rebuild_worker: - enabled: true - poll_interval_sec: 60 - lock_ttl_sec: 30 - batch_size: 500 invite_recharge_worker: enabled: true wallet_mysql_dsn: "${TENCENT_MYSQL_WALLET_DSN}" @@ -37,11 +32,22 @@ 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 +login_risk: + enabled: true + redis_addr: "TENCENT_REDIS_HOST:6379" + redis_password: "TENCENT_REDIS_PASSWORD" + redis_db: 0 + unsupported_countries: + - "CN" + blocked_ttl_sec: 2592000 + allowed_ttl_sec: 604800 + unknown_ttl_sec: 1800 + provider_timeout_ms: 800 + providers: + - "edge_country" + - "primary_geo" + - "fallback_geo_a" + - "fallback_geo_b" 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 a82d26c1..7f2985d2 100644 --- a/services/user-service/configs/config.yaml +++ b/services/user-service/configs/config.yaml @@ -22,11 +22,6 @@ third_party: project_id: "lalu-party" allowed_sign_in_providers: - "google.com" -region_rebuild_worker: - enabled: true - poll_interval_sec: 60 - lock_ttl_sec: 30 - batch_size: 500 invite_recharge_worker: enabled: true wallet_mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=Local" @@ -37,11 +32,19 @@ 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 +login_risk: + enabled: true + redis_addr: "127.0.0.1:13379" + redis_password: "" + redis_db: 0 + unsupported_countries: + - "CN" + blocked_ttl_sec: 2592000 + allowed_ttl_sec: 604800 + unknown_ttl_sec: 1800 + provider_timeout_ms: 800 + providers: + - "edge_country" 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 fd3b06c9..50891d32 100644 --- a/services/user-service/deploy/mysql/initdb/001_user_service.sql +++ b/services/user-service/deploy/mysql/initdb/001_user_service.sql @@ -1461,6 +1461,9 @@ CREATE TABLE IF NOT EXISTS auth_sessions ( device_id VARCHAR(128) NOT NULL, expires_at_ms BIGINT NOT NULL, revoked_at_ms BIGINT NULL, + revoked_reason VARCHAR(64) NOT NULL DEFAULT '', + revoked_request_id VARCHAR(96) NOT NULL DEFAULT '', + revoked_by VARCHAR(64) NOT NULL DEFAULT '', created_at_ms BIGINT NOT NULL, updated_at_ms BIGINT NOT NULL, UNIQUE KEY uk_auth_sessions_refresh_token_hash (app_code, refresh_token_hash), @@ -1475,14 +1478,69 @@ CREATE TABLE IF NOT EXISTS login_audit ( user_id BIGINT NULL, login_type VARCHAR(32) NOT NULL, provider VARCHAR(64) NULL, + channel VARCHAR(32) NOT NULL DEFAULT '', + platform VARCHAR(16) NOT NULL DEFAULT '', result VARCHAR(32) NOT NULL, failure_code VARCHAR(64) NULL, client_ip VARCHAR(64) NULL, + ip_country_code VARCHAR(3) NOT NULL DEFAULT '', + blocked TINYINT(1) NOT NULL DEFAULT 0, + block_reason VARCHAR(64) NOT NULL DEFAULT '', user_agent VARCHAR(512) NULL, created_at_ms BIGINT NOT NULL, KEY idx_login_audit_user_id (app_code, user_id), KEY idx_login_audit_request_id (app_code, request_id), - KEY idx_login_audit_created_at (app_code, created_at_ms) + KEY idx_login_audit_created_at (app_code, created_at_ms), + KEY idx_login_audit_user_created (app_code, user_id, created_at_ms), + KEY idx_login_audit_blocked (app_code, blocked, created_at_ms), + KEY idx_login_audit_ip_country (app_code, ip_country_code, created_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS login_ip_risk_jobs ( + app_code VARCHAR(32) NOT NULL, + job_id VARCHAR(96) NOT NULL, + session_id VARCHAR(96) NOT NULL, + user_id BIGINT NOT NULL, + client_ip VARCHAR(64) NOT NULL, + client_ip_hash VARCHAR(128) NOT NULL, + channel VARCHAR(32) NOT NULL, + platform VARCHAR(16) NOT NULL, + language VARCHAR(32) NOT NULL DEFAULT '', + timezone VARCHAR(64) NOT NULL DEFAULT '', + request_id VARCHAR(96) NOT NULL DEFAULT '', + edge_country_code VARCHAR(3) NOT NULL DEFAULT '', + status VARCHAR(32) NOT NULL, + decision VARCHAR(32) NOT NULL DEFAULT '', + country_code VARCHAR(3) NOT NULL DEFAULT '', + failure_reason VARCHAR(128) NOT NULL DEFAULT '', + locked_by VARCHAR(128) NULL, + locked_until_ms BIGINT NULL, + attempt_count INT NOT NULL DEFAULT 0, + created_at_ms BIGINT NOT NULL, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, job_id), + KEY idx_login_ip_risk_jobs_status (app_code, status, updated_at_ms), + KEY idx_login_ip_risk_jobs_session (app_code, session_id), + KEY idx_login_ip_risk_jobs_user (app_code, user_id, created_at_ms), + KEY idx_login_ip_risk_jobs_ip (app_code, client_ip_hash, updated_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS login_ip_risk_decisions ( + app_code VARCHAR(32) NOT NULL, + decision_id VARCHAR(96) NOT NULL, + client_ip VARCHAR(64) NOT NULL, + client_ip_hash VARCHAR(128) NOT NULL, + decision VARCHAR(32) NOT NULL, + country_code VARCHAR(3) NOT NULL DEFAULT '', + confidence INT NOT NULL DEFAULT 0, + provider_attempts_json JSON NOT NULL, + decided_at_ms BIGINT NOT NULL, + expires_at_ms BIGINT NOT NULL, + created_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, decision_id), + KEY idx_login_ip_risk_decisions_ip (app_code, client_ip_hash, decided_at_ms), + KEY idx_login_ip_risk_decisions_country (app_code, country_code, decided_at_ms), + KEY idx_login_ip_risk_decisions_expires (app_code, expires_at_ms) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 本地联调测试账号。 diff --git a/services/user-service/internal/app/app.go b/services/user-service/internal/app/app.go index 1b93049e..d4e0de62 100644 --- a/services/user-service/internal/app/app.go +++ b/services/user-service/internal/app/app.go @@ -42,17 +42,21 @@ type App struct { walletDB *sql.DB // roomDB 只用于读取 room_outbox 麦位事实,Room Cell 状态仍由 room-service owner。 roomDB *sql.DB - // userSvc 持有用户主数据用例,后台 region rebuild worker 复用它消费任务。 + // userSvc 持有用户主数据用例,cron RPC 复用它消费区域重算任务。 userSvc *userservice.Service + // authSvc 持有认证、登录 session 和登录 IP 风控用例。 + authSvc *authservice.Service // inviteSvc 消费 wallet recharge outbox,维护有效邀请 read model。 inviteSvc *inviteservice.Service // micTimeSvc 消费 room mic outbox,维护用户维度麦上时长 read model。 micTimeSvc *mictimeservice.Service + // loginRiskRedisClose 关闭登录风险 Redis cache;cache 不作为启动硬依赖。 + loginRiskRedisClose func() error // cfg 保存 worker 运行参数,避免 Run 阶段重新读配置。 cfg config.Config // workerCtx 统一控制后台 worker 生命周期。 workerCtx context.Context - // workerCancel 停止 region rebuild、invite recharge 和 mic time worker。 + // workerCancel 停止 invite recharge 和 room mic outbox worker。 workerCancel context.CancelFunc // workerWG 等待后台 worker 当前批次安全退出。 workerWG sync.WaitGroup @@ -98,6 +102,18 @@ func New(cfg config.Config) (*App, error) { hostRepo := mysqlRepo.HostRepository() inviteRepo := mysqlRepo.InviteRepository() micTimeRepo := mysqlRepo.MicTimeRepository() + var ipDecisionCache authservice.IPDecisionCache + var loginRiskRedisClose func() error + if cfg.LoginRisk.Enabled && cfg.LoginRisk.RedisAddr != "" { + redisClient, redisErr := authservice.NewLoginRiskRedisClient(startupCtx, cfg.LoginRisk.RedisAddr, cfg.LoginRisk.RedisPassword, cfg.LoginRisk.RedisDB) + if redisErr == nil { + ipDecisionCache = authservice.NewRedisIPDecisionCache(redisClient) + loginRiskRedisClose = redisClient.Close + } else { + // IP 风控 Redis 缓存按文档 fail-open;启动继续,worker 仍会写 MySQL 审计。 + logx.Warn(startupCtx, "login_risk_redis_unavailable") + } + } // auth service 负责登录、session 和 token;用户主数据和短号事务仍通过各自领域存储完成。 authSvc := authservice.New(authservice.Config{ @@ -114,6 +130,18 @@ func New(cfg config.Config) (*App, error) { authservice.WithIDGenerator(idgen.NewInt64Generator(cfg.IDGenerator.NodeID)), authservice.WithDisplayUserIDAllocateMaxAttempts(cfg.DisplayUserID.AllocateMaxAttempts), authservice.WithThirdPartyVerifier(thirdPartyVerifier), + authservice.WithLoginRiskPolicy(authservice.LoginRiskPolicy{ + Enabled: cfg.LoginRisk.Enabled, + UnsupportedCountries: cfg.LoginRisk.UnsupportedCountries, + BlockedTTL: time.Duration(cfg.LoginRisk.BlockedTTLSec) * time.Second, + AllowedTTL: time.Duration(cfg.LoginRisk.AllowedTTLSec) * time.Second, + UnknownTTL: time.Duration(cfg.LoginRisk.UnknownTTLSec) * time.Second, + ProviderTimeout: time.Duration(cfg.LoginRisk.ProviderTimeoutMs) * time.Millisecond, + ProviderNames: cfg.LoginRisk.Providers, + DenylistTTL: time.Duration(cfg.JWT.AccessTokenTTLSec+60) * time.Second, + }), + authservice.WithIPDecisionCache(ipDecisionCache), + authservice.WithIPGeoProviders(authservice.BuildIPGeoProviders(cfg.LoginRisk.Providers)), ) // user service 负责用户主状态和 display_user_id/靓号用例,不进入房间高频流程。 userSvc := userservice.New(userRepo, @@ -135,12 +163,18 @@ func New(cfg config.Config) (*App, error) { if cfg.InviteRechargeWorker.Enabled { walletDB, err = sql.Open("mysql", cfg.InviteRechargeWorker.WalletMySQLDSN) if err != nil { + if loginRiskRedisClose != nil { + _ = loginRiskRedisClose() + } _ = listener.Close() _ = mysqlRepo.Close() return nil, err } if err := walletDB.PingContext(startupCtx); err != nil { _ = walletDB.Close() + if loginRiskRedisClose != nil { + _ = loginRiskRedisClose() + } _ = listener.Close() _ = mysqlRepo.Close() return nil, err @@ -155,6 +189,9 @@ func New(cfg config.Config) (*App, error) { if walletDB != nil { _ = walletDB.Close() } + if loginRiskRedisClose != nil { + _ = loginRiskRedisClose() + } _ = listener.Close() _ = mysqlRepo.Close() return nil, err @@ -164,6 +201,9 @@ func New(cfg config.Config) (*App, error) { if walletDB != nil { _ = walletDB.Close() } + if loginRiskRedisClose != nil { + _ = loginRiskRedisClose() + } _ = listener.Close() _ = mysqlRepo.Close() return nil, err @@ -183,6 +223,7 @@ func New(cfg config.Config) (*App, error) { userv1.RegisterRegionAdminServiceServer(server, userServer) userv1.RegisterUserHostServiceServer(server, userServer) userv1.RegisterUserHostAdminServiceServer(server, userServer) + userv1.RegisterUserCronServiceServer(server, userServer) health := grpchealth.NewServingChecker("user-service") // user-service 目前使用简单 serving checker,存储探测由后续专用 health 可扩展。 @@ -190,18 +231,20 @@ func New(cfg config.Config) (*App, error) { workerCtx, workerCancel := context.WithCancel(context.Background()) return &App{ - server: server, - listener: listener, - health: health, - mysqlRepo: mysqlRepo, - walletDB: walletDB, - roomDB: roomDB, - userSvc: userSvc, - inviteSvc: inviteSvc, - micTimeSvc: micTimeSvc, - cfg: cfg, - workerCtx: workerCtx, - workerCancel: workerCancel, + server: server, + listener: listener, + health: health, + mysqlRepo: mysqlRepo, + walletDB: walletDB, + roomDB: roomDB, + authSvc: authSvc, + userSvc: userSvc, + inviteSvc: inviteSvc, + micTimeSvc: micTimeSvc, + loginRiskRedisClose: loginRiskRedisClose, + cfg: cfg, + workerCtx: workerCtx, + workerCancel: workerCancel, }, nil } @@ -215,18 +258,6 @@ func (a *App) Run() error { } a.workerWG.Wait() }() - if a.cfg.RegionRebuildWorker.Enabled { - a.workerWG.Add(1) - go func() { - defer a.workerWG.Done() - a.userSvc.RunRegionRebuildWorker(a.workerCtx, userservice.RegionRebuildWorkerOptions{ - WorkerID: a.cfg.NodeID, - PollInterval: time.Duration(a.cfg.RegionRebuildWorker.PollIntervalSec) * time.Second, - LockTTL: time.Duration(a.cfg.RegionRebuildWorker.LockTTLSec) * time.Second, - BatchSize: a.cfg.RegionRebuildWorker.BatchSize, - }) - }() - } if a.cfg.InviteRechargeWorker.Enabled && a.inviteSvc != nil { a.workerWG.Add(1) go func() { @@ -249,19 +280,6 @@ 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) { @@ -293,5 +311,8 @@ func (a *App) Close() { if a.roomDB != nil { _ = a.roomDB.Close() } + if a.loginRiskRedisClose != nil { + _ = a.loginRiskRedisClose() + } }) } diff --git a/services/user-service/internal/config/config.go b/services/user-service/internal/config/config.go index 72b2903f..a80f3975 100644 --- a/services/user-service/internal/config/config.go +++ b/services/user-service/internal/config/config.go @@ -28,18 +28,32 @@ type Config struct { DisplayUserID DisplayUserIDConfig `yaml:"display_user_id"` // ThirdParty 控制本地三方登录 provider allowlist。 ThirdParty ThirdPartyConfig `yaml:"third_party"` - // RegionRebuildWorker 控制历史用户 region_id 快照后台重算。 - RegionRebuildWorker RegionRebuildWorkerConfig `yaml:"region_rebuild_worker"` // InviteRechargeWorker 控制邀请有效人数的 wallet recharge outbox 消费。 InviteRechargeWorker InviteRechargeWorkerConfig `yaml:"invite_recharge_worker"` // MicTimeWorker 控制 room-service 麦位事件消费和用户麦上时长聚合。 MicTimeWorker MicTimeWorkerConfig `yaml:"mic_time_worker"` + // LoginRisk 控制登录后 IP 风控策略、Redis session denylist 和 IP 查询 provider。 + LoginRisk LoginRiskConfig `yaml:"login_risk"` // JWT 控制 access token 和 refresh session 的签发参数。 JWT JWTConfig `yaml:"jwt"` // Log 控制 stdout 结构化日志格式、等级和 access body 策略。 Log logx.Config `yaml:"log"` } +// LoginRiskConfig 保存登录地区风控的 Redis、TTL 和 provider 顺序;后台扫描由 cron-service 调度。 +type LoginRiskConfig struct { + Enabled bool `yaml:"enabled"` + RedisAddr string `yaml:"redis_addr"` + RedisPassword string `yaml:"redis_password"` + RedisDB int `yaml:"redis_db"` + UnsupportedCountries []string `yaml:"unsupported_countries"` + BlockedTTLSec int64 `yaml:"blocked_ttl_sec"` + AllowedTTLSec int64 `yaml:"allowed_ttl_sec"` + UnknownTTLSec int64 `yaml:"unknown_ttl_sec"` + ProviderTimeoutMs int64 `yaml:"provider_timeout_ms"` + Providers []string `yaml:"providers"` +} + // IDGeneratorConfig 保存 user_id 发号节点配置。 type IDGeneratorConfig struct { // NodeID 参与雪花 ID 发号,多个 user-service 节点必须不同。 @@ -72,18 +86,6 @@ type FirebaseConfig struct { CertsURL string `yaml:"certs_url"` } -// RegionRebuildWorkerConfig 保存区域映射变更后历史用户快照重算策略。 -type RegionRebuildWorkerConfig struct { - // Enabled 控制是否启动后台 worker;关闭后只创建 task,不消费。 - Enabled bool `yaml:"enabled"` - // PollIntervalSec 是两轮扫描间隔秒数。 - PollIntervalSec int64 `yaml:"poll_interval_sec"` - // LockTTLSec 是单个 task claim 锁定时间秒数,过期允许其他节点接管。 - LockTTLSec int64 `yaml:"lock_ttl_sec"` - // BatchSize 是单轮最多更新 users 数量。 - BatchSize int `yaml:"batch_size"` -} - // InviteRechargeWorkerConfig 保存钱包充值事实消费策略。 type InviteRechargeWorkerConfig struct { // Enabled 控制是否启动钱包充值事件轮询。 @@ -106,16 +108,6 @@ 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 的签发策略。 @@ -155,12 +147,6 @@ func Default() Config { AllowedSignInProviders: []string{"google.com"}, }, }, - RegionRebuildWorker: RegionRebuildWorkerConfig{ - Enabled: true, - PollIntervalSec: 5, - LockTTLSec: 30, - BatchSize: 500, - }, InviteRechargeWorker: InviteRechargeWorkerConfig{ Enabled: true, WalletMySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=Local", @@ -168,15 +154,20 @@ 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, - CompensationEnabled: true, - CompensationIntervalSec: 60, - CompensationBatchSize: 100, - PendingPublishMaxAgeSec: 120, - PublishingSessionMaxAgeSec: 43200, + Enabled: true, + RoomMySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_room?parseTime=true&charset=utf8mb4&loc=Local", + PollIntervalSec: 5, + BatchSize: 100, + }, + LoginRisk: LoginRiskConfig{ + Enabled: true, + RedisAddr: "127.0.0.1:13379", + UnsupportedCountries: []string{"CN"}, + BlockedTTLSec: 2592000, + AllowedTTLSec: 604800, + UnknownTTLSec: 1800, + ProviderTimeoutMs: 800, + Providers: []string{"edge_country"}, }, JWT: JWTConfig{ Issuer: "hyapp", @@ -218,6 +209,18 @@ func Load(path string) (Config, error) { if cfg.Environment == "" { cfg.Environment = "local" } - + cfg.LoginRisk.RedisAddr = strings.TrimSpace(cfg.LoginRisk.RedisAddr) + if cfg.LoginRisk.BlockedTTLSec <= 0 { + cfg.LoginRisk.BlockedTTLSec = 2592000 + } + if cfg.LoginRisk.AllowedTTLSec <= 0 { + cfg.LoginRisk.AllowedTTLSec = 604800 + } + if cfg.LoginRisk.UnknownTTLSec <= 0 { + cfg.LoginRisk.UnknownTTLSec = 1800 + } + if cfg.LoginRisk.ProviderTimeoutMs <= 0 { + cfg.LoginRisk.ProviderTimeoutMs = 800 + } return cfg, nil } diff --git a/services/user-service/internal/domain/auth/auth.go b/services/user-service/internal/domain/auth/auth.go index bf30cc5c..b859fc15 100644 --- a/services/user-service/internal/domain/auth/auth.go +++ b/services/user-service/internal/domain/auth/auth.go @@ -67,6 +67,12 @@ type Session struct { ExpiresAtMs int64 // RevokedAtMs 非 0 表示会话已吊销。 RevokedAtMs int64 + // RevokedReason 区分 logout、refresh 轮换、后台封禁和 IP 风控撤销。 + RevokedReason string + // RevokedRequestID 串联触发撤销的登录请求或后台任务。 + RevokedRequestID string + // RevokedBy 标识撤销来源,例如 user_logout、refresh_rotate、risk_worker。 + RevokedBy string // CreatedAtMs 是会话创建时间。 CreatedAtMs int64 // UpdatedAtMs 是会话更新时间。 @@ -104,12 +110,22 @@ type LoginAudit struct { LoginType string // Provider 只在三方登录链路中有值。 Provider string + // Channel 是后台展示维度,固定为 account/google/facebook/twitter/tiktok 等稳定渠道。 + Channel string + // Platform 是客户端平台快照,例如 android/ios。 + Platform string // Result 是 success 或 failed。 Result string // FailureCode 保存稳定错误 reason,不能写敏感凭证。 FailureCode string // ClientIP 是 gateway 观察到的客户端 IP。 ClientIP string + // IPCountryCode 是 IP Geo 解析出的国家码,未解析时为空。 + IPCountryCode string + // Blocked 标记本次登录是否被入口或异步风控阻断。 + Blocked bool + // BlockReason 保存 language/timezone/ip_cache/ip_async 等稳定阻断原因。 + BlockReason string // UserAgent 是客户端 UA。 UserAgent string // CreatedAtMs 是审计写入时间。 diff --git a/services/user-service/internal/domain/auth/risk.go b/services/user-service/internal/domain/auth/risk.go new file mode 100644 index 00000000..9ead9680 --- /dev/null +++ b/services/user-service/internal/domain/auth/risk.go @@ -0,0 +1,61 @@ +package auth + +const ( + // LoginIPRiskStatusPending 表示任务已落库但尚未被 worker claim。 + LoginIPRiskStatusPending = "pending" + // LoginIPRiskStatusRunning 表示任务已被某个 worker 租约锁定。 + LoginIPRiskStatusRunning = "running" + // LoginIPRiskStatusCompleted 表示任务已有 blocked/allowed/unknown 决策。 + LoginIPRiskStatusCompleted = "completed" + // LoginIPRiskStatusFailed 表示本次处理失败,后续可按锁和 attempt 重试。 + LoginIPRiskStatusFailed = "failed" + // LoginIPRiskStatusSkipped 表示已有新鲜决策,本任务不再访问 provider。 + LoginIPRiskStatusSkipped = "skipped" + + // LoginIPRiskDecisionBlocked 表示 IP 国家命中不支持国家。 + LoginIPRiskDecisionBlocked = "blocked" + // LoginIPRiskDecisionAllowed 表示 IP 国家未命中不支持国家。 + LoginIPRiskDecisionAllowed = "allowed" + // LoginIPRiskDecisionUnknown 表示 provider 全部不可用或没有可解析国家。 + LoginIPRiskDecisionUnknown = "unknown" +) + +// LoginIPRiskJob 是登录成功后异步复核入口 IP 的持久任务。 +type LoginIPRiskJob struct { + AppCode string + JobID string + SessionID string + UserID int64 + ClientIP string + ClientIPHash string + Channel string + Platform string + Language string + Timezone string + RequestID string + EdgeCountryCode string + Status string + Decision string + CountryCode string + FailureReason string + LockedBy string + LockedUntilMs int64 + AttemptCount int + CreatedAtMs int64 + UpdatedAtMs int64 +} + +// LoginIPRiskDecision 记录一次 IP Geo 决策,Redis 只保存它的快查投影。 +type LoginIPRiskDecision struct { + AppCode string + DecisionID string + ClientIP string + ClientIPHash string + Decision string + CountryCode string + Confidence int + ProviderAttemptsJSON string + DecidedAtMs int64 + ExpiresAtMs int64 + CreatedAtMs int64 +} diff --git a/services/user-service/internal/service/auth/audit.go b/services/user-service/internal/service/auth/audit.go index aa8f85a7..b9562034 100644 --- a/services/user-service/internal/service/auth/audit.go +++ b/services/user-service/internal/service/auth/audit.go @@ -3,8 +3,10 @@ package auth import ( "context" + "strings" "hyapp/pkg/appcode" + "hyapp/pkg/xerr" authdomain "hyapp/services/user-service/internal/domain/auth" ) @@ -20,25 +22,144 @@ type Meta struct { UserAgent string // CountryByIP 是 gateway 或可信边缘层根据入口 IP 得到的国家快照。 CountryByIP string + // Platform 是客户端平台快照,认证链路只做审计和风险任务透传。 + Platform string + // Language 是客户端语言偏好,gateway 只用作弱信号快拦。 + Language string + // Timezone 是客户端 IANA 时区,gateway 只用作弱信号快拦。 + Timezone string } func (s *Service) audit(ctx context.Context, meta Meta, userID int64, loginType string, provider string, result string, failureCode string) { + s.recordAudit(ctx, authdomain.LoginAudit{ + AppCode: appcode.Normalize(meta.AppCode), + RequestID: meta.RequestID, + UserID: userID, + LoginType: loginType, + Provider: provider, + Channel: auditChannel(loginType, provider), + Platform: normalizePlatform(meta.Platform), + Result: result, + FailureCode: failureCode, + ClientIP: meta.ClientIP, + IPCountryCode: normalizeCountryCode(meta.CountryByIP), + UserAgent: meta.UserAgent, + CreatedAtMs: s.now().UnixMilli(), + }) +} + +func (s *Service) recordAudit(ctx context.Context, audit authdomain.LoginAudit) { if s.authRepository == nil { // 没有 repository 时无法写审计,但不应让辅助审计路径触发 panic。 return } // 审计失败不影响登录主链路,所以这里有意忽略错误。 - _ = s.authRepository.RecordLoginAudit(ctx, authdomain.LoginAudit{ - AppCode: appcode.Normalize(meta.AppCode), - RequestID: meta.RequestID, - UserID: userID, - LoginType: loginType, - Provider: provider, - Result: result, - FailureCode: failureCode, - ClientIP: meta.ClientIP, - UserAgent: meta.UserAgent, - CreatedAtMs: s.now().UnixMilli(), - }) + _ = s.authRepository.RecordLoginAudit(ctx, audit) +} + +// RecordLoginBlocked 写入 gateway fast guard 拒绝的登录审计;失败不应影响 gateway 返回阻断响应。 +func (s *Service) RecordLoginBlocked(ctx context.Context, meta Meta, channel string, platform string, language string, timezone string, blockReason string) bool { + if s.authRepository == nil { + return false + } + nowMs := s.now().UnixMilli() + audit := authdomain.LoginAudit{ + AppCode: appcode.Normalize(meta.AppCode), + RequestID: meta.RequestID, + LoginType: blockedLoginType(channel), + Provider: blockedProvider(channel), + Channel: normalizeChannel(channel), + Platform: normalizePlatform(firstNonEmpty(platform, meta.Platform)), + Result: "blocked", + FailureCode: string(xerr.AuthLoginBlocked), + ClientIP: meta.ClientIP, + IPCountryCode: normalizeCountryCode(meta.CountryByIP), + Blocked: true, + BlockReason: strings.TrimSpace(blockReason), + UserAgent: meta.UserAgent, + CreatedAtMs: nowMs, + } + if audit.Channel == "" { + audit.Channel = "account" + } + return s.authRepository.RecordLoginAudit(ctx, audit) == nil +} + +func auditChannel(loginType string, provider string) string { + if loginType == loginPassword { + return "account" + } + if loginType == loginThird { + return normalizeThirdPartyChannel(provider) + } + return strings.TrimSpace(loginType) +} + +func blockedLoginType(channel string) string { + if normalizeChannel(channel) == "account" { + return loginPassword + } + return loginThird +} + +func blockedProvider(channel string) string { + channel = normalizeChannel(channel) + if channel == "account" { + return "" + } + return channel +} + +func normalizeThirdPartyChannel(provider string) string { + provider = strings.ToLower(strings.TrimSpace(provider)) + switch provider { + case "google", "google.com", "firebase": + return "google" + case "facebook", "facebook.com": + return "facebook" + case "twitter", "x": + return "twitter" + case "tiktok": + return "tiktok" + default: + return provider + } +} + +func normalizeChannel(channel string) string { + channel = strings.ToLower(strings.TrimSpace(channel)) + switch channel { + case "", "password": + return "account" + case "third_party": + return "" + default: + return normalizeThirdPartyChannel(channel) + } +} + +func normalizePlatform(platform string) string { + platform = strings.ToLower(strings.TrimSpace(platform)) + if platform == "android" || platform == "ios" { + return platform + } + return platform +} + +func normalizeCountryCode(country string) string { + country = strings.ToUpper(strings.TrimSpace(country)) + if len(country) < 2 || len(country) > 3 { + return "" + } + return country +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return value + } + } + return "" } diff --git a/services/user-service/internal/service/auth/ip_geo_provider.go b/services/user-service/internal/service/auth/ip_geo_provider.go new file mode 100644 index 00000000..bcecf580 --- /dev/null +++ b/services/user-service/internal/service/auth/ip_geo_provider.go @@ -0,0 +1,73 @@ +package auth + +import ( + "context" + "fmt" + "strings" + + "hyapp/pkg/xerr" + authdomain "hyapp/services/user-service/internal/domain/auth" +) + +type edgeCountryProvider struct{} + +type staticCountryProvider struct { + name string + countryCode string +} + +type unavailableIPGeoProvider struct { + name string +} + +func BuildIPGeoProviders(names []string) []IPGeoProvider { + providers := make([]IPGeoProvider, 0, len(names)) + for _, name := range names { + name = strings.TrimSpace(name) + if name == "" { + continue + } + switch { + case strings.EqualFold(name, "edge_country"): + providers = append(providers, edgeCountryProvider{}) + case strings.HasPrefix(strings.ToLower(name), "static:"): + country := normalizeCountryCode(strings.TrimPrefix(name, "static:")) + providers = append(providers, staticCountryProvider{name: name, countryCode: country}) + default: + // 未配置具体 provider 适配器时显式降级为 unavailable,worker 会按顺序继续尝试下一个。 + providers = append(providers, unavailableIPGeoProvider{name: name}) + } + } + return providers +} + +func (p edgeCountryProvider) Name() string { + return "edge_country" +} + +func (p edgeCountryProvider) ResolveCountry(_ context.Context, job authdomain.LoginIPRiskJob) (IPGeoResult, error) { + country := normalizeCountryCode(job.EdgeCountryCode) + if country == "" { + return IPGeoResult{}, xerr.New(xerr.Unavailable, "edge country is empty") + } + return IPGeoResult{CountryCode: country, Confidence: 60}, nil +} + +func (p staticCountryProvider) Name() string { + return p.name +} + +func (p staticCountryProvider) ResolveCountry(_ context.Context, _ authdomain.LoginIPRiskJob) (IPGeoResult, error) { + if p.countryCode == "" { + return IPGeoResult{}, xerr.New(xerr.Unavailable, "static country is empty") + } + return IPGeoResult{CountryCode: p.countryCode, Confidence: 100}, nil +} + +func (p unavailableIPGeoProvider) Name() string { + return p.name +} + +func (p unavailableIPGeoProvider) ResolveCountry(_ context.Context, _ authdomain.LoginIPRiskJob) (IPGeoResult, error) { + return IPGeoResult{}, fmt.Errorf("ip geo provider %s is not configured", p.name) +} diff --git a/services/user-service/internal/service/auth/password.go b/services/user-service/internal/service/auth/password.go index afde9612..a88d44bc 100644 --- a/services/user-service/internal/service/auth/password.go +++ b/services/user-service/internal/service/auth/password.go @@ -67,7 +67,12 @@ func (s *Service) LoginPassword(ctx context.Context, displayUserID string, passw } s.audit(ctx, meta, user.UserID, loginPassword, "", resultSuccess, "") - return s.issueToken(user, session.SessionID, refreshToken) + token, err := s.issueToken(user, session.SessionID, refreshToken) + if err != nil { + return authdomain.Token{}, err + } + s.enqueueLoginIPRiskJob(ctx, meta, token, "account", meta.Platform, meta.Language, meta.Timezone) + return token, nil } // SetPassword 为已经三方登录创建出的用户首次设置密码。 diff --git a/services/user-service/internal/service/auth/risk_cache.go b/services/user-service/internal/service/auth/risk_cache.go new file mode 100644 index 00000000..7ca54dc5 --- /dev/null +++ b/services/user-service/internal/service/auth/risk_cache.go @@ -0,0 +1,68 @@ +package auth + +import ( + "context" + "encoding/json" + "errors" + "strings" + "time" + + "github.com/redis/go-redis/v9" + "hyapp/pkg/appcode" +) + +type RedisIPDecisionCache struct { + client *redis.Client +} + +func NewRedisIPDecisionCache(client *redis.Client) *RedisIPDecisionCache { + return &RedisIPDecisionCache{client: client} +} + +func NewLoginRiskRedisClient(ctx context.Context, addr string, password string, db int) (*redis.Client, error) { + client := redis.NewClient(&redis.Options{ + Addr: strings.TrimSpace(addr), + Password: password, + DB: db, + }) + if err := client.Ping(ctx).Err(); err != nil { + _ = client.Close() + return nil, err + } + return client, nil +} + +func (c *RedisIPDecisionCache) GetIPDecision(ctx context.Context, appCode string, clientIPHash string) (IPDecision, bool, error) { + raw, err := c.client.Get(ctx, loginRiskIPDecisionKey(appCode, clientIPHash)).Result() + if errors.Is(err, redis.Nil) { + return IPDecision{}, false, nil + } + if err != nil { + return IPDecision{}, false, err + } + var decision IPDecision + if err := json.Unmarshal([]byte(raw), &decision); err != nil { + return IPDecision{}, false, err + } + return decision, true, nil +} + +func (c *RedisIPDecisionCache) SetIPDecision(ctx context.Context, appCode string, clientIPHash string, decision IPDecision, ttl time.Duration) error { + raw, err := json.Marshal(decision) + if err != nil { + return err + } + return c.client.Set(ctx, loginRiskIPDecisionKey(appCode, clientIPHash), raw, ttl).Err() +} + +func (c *RedisIPDecisionCache) SetRevokedSession(ctx context.Context, appCode string, sessionID string, reason string, ttl time.Duration) error { + return c.client.Set(ctx, revokedSessionKey(appCode, sessionID), strings.TrimSpace(reason), ttl).Err() +} + +func loginRiskIPDecisionKey(appCode string, clientIPHash string) string { + return "login_risk:ip:" + appcode.Normalize(appCode) + ":" + strings.TrimSpace(clientIPHash) +} + +func revokedSessionKey(appCode string, sessionID string) string { + return "auth:revoked_session:" + appcode.Normalize(appCode) + ":" + strings.TrimSpace(sessionID) +} diff --git a/services/user-service/internal/service/auth/risk_job.go b/services/user-service/internal/service/auth/risk_job.go new file mode 100644 index 00000000..d7b6419c --- /dev/null +++ b/services/user-service/internal/service/auth/risk_job.go @@ -0,0 +1,59 @@ +package auth + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "log/slog" + "strings" + + "hyapp/pkg/appcode" + "hyapp/pkg/idgen" + "hyapp/pkg/logx" + authdomain "hyapp/services/user-service/internal/domain/auth" +) + +const ( + riskBlockReasonIPAsync = "ip_async" +) + +func (s *Service) enqueueLoginIPRiskJob(ctx context.Context, meta Meta, token authdomain.Token, channel string, platform string, language string, timezone string) { + if s.authRepository == nil || strings.TrimSpace(meta.ClientIP) == "" || token.SessionID == "" || token.UserID <= 0 { + // 没有可复核 IP 或 session 时不创建任务;登录主链路不因此失败。 + return + } + nowMs := s.now().UnixMilli() + clientIP := strings.TrimSpace(meta.ClientIP) + job := authdomain.LoginIPRiskJob{ + AppCode: appcode.Normalize(token.AppCode), + JobID: idgen.New("iprisk"), + SessionID: token.SessionID, + UserID: token.UserID, + ClientIP: clientIP, + ClientIPHash: HashLoginRiskIP(clientIP), + Channel: normalizeChannel(channel), + Platform: normalizePlatform(firstNonEmpty(platform, meta.Platform)), + Language: strings.TrimSpace(firstNonEmpty(language, meta.Language)), + Timezone: strings.TrimSpace(firstNonEmpty(timezone, meta.Timezone)), + RequestID: meta.RequestID, + EdgeCountryCode: normalizeCountryCode(meta.CountryByIP), + Status: authdomain.LoginIPRiskStatusPending, + CreatedAtMs: nowMs, + UpdatedAtMs: nowMs, + } + if job.Channel == "" { + job.Channel = "account" + } + if err := s.authRepository.CreateLoginIPRiskJob(ctx, job); err != nil { + // 风控任务失败按文档 fail-open;日志只输出 IP hash,明文 IP 已在业务表审计。 + logx.Error(logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", job.AppCode)), "login_ip_risk_job_create_failed", err, slog.String("component", "user_auth"), slog.String("client_ip_hash", job.ClientIPHash), slog.String("session_id", job.SessionID)) + return + } + logx.Info(logx.With(ctx, slog.String("request_id", meta.RequestID), slog.String("app_code", job.AppCode)), "login_ip_risk_job_created", slog.String("component", "user_auth"), slog.String("job_id", job.JobID), slog.String("session_id", job.SessionID), slog.Int64("user_id", job.UserID), slog.String("client_ip_hash", job.ClientIPHash), slog.String("channel", job.Channel), slog.String("platform", job.Platform)) +} + +// HashLoginRiskIP 生成登录风险 Redis key 和 DB 索引用的稳定 IP 摘要。 +func HashLoginRiskIP(ip string) string { + sum := sha256.Sum256([]byte(strings.TrimSpace(ip))) + return hex.EncodeToString(sum[:]) +} diff --git a/services/user-service/internal/service/auth/risk_worker.go b/services/user-service/internal/service/auth/risk_worker.go new file mode 100644 index 00000000..be62f21f --- /dev/null +++ b/services/user-service/internal/service/auth/risk_worker.go @@ -0,0 +1,247 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + "time" + + "hyapp/pkg/appcode" + "hyapp/pkg/idgen" + "hyapp/pkg/logx" + authdomain "hyapp/services/user-service/internal/domain/auth" +) + +const revokeReasonGeoPolicyBlocked = "GEO_POLICY_BLOCKED" + +// LoginIPRiskWorkerOptions 控制 cron-service 触发的单批 IP 风控处理边界。 +type LoginIPRiskWorkerOptions struct { + WorkerID string + LockTTL time.Duration + BatchSize int +} + +type providerAttempt struct { + Provider string `json:"provider"` + Order int `json:"order"` + Country string `json:"country"` + Status string `json:"status"` + DurationMs int64 `json:"duration_ms"` + ErrorCode string `json:"error_code"` +} + +func (s *Service) ProcessLoginIPRiskBatch(ctx context.Context, options LoginIPRiskWorkerOptions) (int, error) { + if !s.loginRiskPolicy.Enabled || s.authRepository == nil { + return 0, nil + } + options = normalizeLoginIPRiskWorkerOptions(options) + nowMs := s.now().UnixMilli() + jobs, err := s.authRepository.ClaimLoginIPRiskJobs(ctx, options.WorkerID, nowMs, options.LockTTL, options.BatchSize) + if err != nil { + return 0, err + } + for _, job := range jobs { + jobCtx := appcode.WithContext(ctx, job.AppCode) + if err := s.processLoginIPRiskJob(jobCtx, job); err != nil { + logx.Error(jobCtx, "login_ip_risk_job_failed", err, slog.String("component", "user_auth_risk"), slog.String("job_id", job.JobID), slog.String("session_id", job.SessionID), slog.String("client_ip_hash", job.ClientIPHash)) + job.Status = authdomain.LoginIPRiskStatusFailed + job.FailureReason = trimRiskFailure(err.Error()) + job.UpdatedAtMs = s.now().UnixMilli() + _ = s.authRepository.CompleteLoginIPRiskJob(jobCtx, job) + } + } + return len(jobs), nil +} + +func normalizeLoginIPRiskWorkerOptions(options LoginIPRiskWorkerOptions) LoginIPRiskWorkerOptions { + if strings.TrimSpace(options.WorkerID) == "" { + options.WorkerID = "user-service" + } + if options.LockTTL <= 0 { + options.LockTTL = 30 * time.Second + } + if options.BatchSize <= 0 { + options.BatchSize = 100 + } + return options +} + +func (s *Service) processLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error { + policy := s.loginRiskPolicy + nowMs := s.now().UnixMilli() + if cached, ok := s.freshCachedIPDecision(ctx, job); ok { + return s.applyLoginIPRiskDecision(ctx, job, cached, nil, authdomain.LoginIPRiskStatusSkipped) + } + + result, attempts := s.resolveIPCountry(ctx, job, policy) + decision := s.decisionForCountry(result.CountryCode) + ttl := s.ttlForDecision(decision) + expiresAtMs := nowMs + ttl.Milliseconds() + ipDecision := IPDecision{ + Decision: decision, + Country: result.CountryCode, + Source: "geo_provider_chain", + CheckedAtMs: nowMs, + ExpiresAtMs: expiresAtMs, + } + if s.ipDecisionCache != nil { + if err := s.ipDecisionCache.SetIPDecision(ctx, job.AppCode, job.ClientIPHash, ipDecision, ttl); err != nil { + logx.Warn(ctx, "login_ip_decision_cache_write_failed", slog.String("component", "user_auth_risk"), slog.String("job_id", job.JobID), slog.String("client_ip_hash", job.ClientIPHash), slog.String("error", err.Error())) + } + } + + attemptsJSON, err := json.Marshal(attempts) + if err != nil { + return err + } + if err := s.authRepository.CreateLoginIPRiskDecision(ctx, authdomain.LoginIPRiskDecision{ + AppCode: job.AppCode, + DecisionID: idgen.New("ipdecision"), + ClientIP: job.ClientIP, + ClientIPHash: job.ClientIPHash, + Decision: decision, + CountryCode: result.CountryCode, + Confidence: result.Confidence, + ProviderAttemptsJSON: string(attemptsJSON), + DecidedAtMs: nowMs, + ExpiresAtMs: expiresAtMs, + CreatedAtMs: nowMs, + }); err != nil { + return err + } + + return s.applyLoginIPRiskDecision(ctx, job, ipDecision, attempts, authdomain.LoginIPRiskStatusCompleted) +} + +func (s *Service) freshCachedIPDecision(ctx context.Context, job authdomain.LoginIPRiskJob) (IPDecision, bool) { + if s.ipDecisionCache == nil { + return IPDecision{}, false + } + decision, ok, err := s.ipDecisionCache.GetIPDecision(ctx, job.AppCode, job.ClientIPHash) + if err != nil { + logx.Warn(ctx, "login_ip_decision_cache_read_failed", slog.String("component", "user_auth_risk"), slog.String("job_id", job.JobID), slog.String("client_ip_hash", job.ClientIPHash), slog.String("error", err.Error())) + return IPDecision{}, false + } + if !ok { + return IPDecision{}, false + } + if decision.ExpiresAtMs > 0 && decision.ExpiresAtMs <= s.now().UnixMilli() { + return IPDecision{}, false + } + if decision.Decision != authdomain.LoginIPRiskDecisionBlocked && decision.Decision != authdomain.LoginIPRiskDecisionAllowed { + return IPDecision{}, false + } + return decision, true +} + +func (s *Service) resolveIPCountry(ctx context.Context, job authdomain.LoginIPRiskJob, policy LoginRiskPolicy) (IPGeoResult, []providerAttempt) { + providers := s.ipGeoProviders + if len(providers) == 0 { + providers = BuildIPGeoProviders(policy.ProviderNames) + } + attempts := make([]providerAttempt, 0, len(providers)) + for index, provider := range providers { + started := time.Now() + providerCtx, cancel := context.WithTimeout(ctx, policy.ProviderTimeout) + result, err := provider.ResolveCountry(providerCtx, job) + cancel() + attempt := providerAttempt{ + Provider: provider.Name(), + Order: index + 1, + Country: normalizeCountryCode(result.CountryCode), + DurationMs: time.Since(started).Milliseconds(), + } + if err != nil { + attempt.Status = "failed" + attempt.ErrorCode = trimRiskFailure(err.Error()) + attempts = append(attempts, attempt) + logx.Warn(ctx, "login_ip_geo_provider_attempt", slog.String("component", "user_auth_risk"), slog.String("job_id", job.JobID), slog.String("provider", provider.Name()), slog.Int("order", index+1), slog.String("status", attempt.Status), slog.String("error_code", attempt.ErrorCode), slog.Int64("duration_ms", attempt.DurationMs)) + continue + } + if attempt.Country == "" { + attempt.Status = "failed" + attempt.ErrorCode = "empty_country" + attempts = append(attempts, attempt) + continue + } + attempt.Status = "ok" + attempts = append(attempts, attempt) + logx.Info(ctx, "login_ip_geo_provider_attempt", slog.String("component", "user_auth_risk"), slog.String("job_id", job.JobID), slog.String("provider", provider.Name()), slog.Int("order", index+1), slog.String("country", attempt.Country), slog.String("status", attempt.Status), slog.Int64("duration_ms", attempt.DurationMs)) + return IPGeoResult{CountryCode: attempt.Country, Confidence: result.Confidence}, attempts + } + return IPGeoResult{}, attempts +} + +func (s *Service) decisionForCountry(countryCode string) string { + countryCode = normalizeCountryCode(countryCode) + if countryCode == "" { + return authdomain.LoginIPRiskDecisionUnknown + } + for _, country := range s.loginRiskPolicy.UnsupportedCountries { + if normalizeCountryCode(country) == countryCode { + return authdomain.LoginIPRiskDecisionBlocked + } + } + return authdomain.LoginIPRiskDecisionAllowed +} + +func (s *Service) ttlForDecision(decision string) time.Duration { + switch decision { + case authdomain.LoginIPRiskDecisionBlocked: + return s.loginRiskPolicy.BlockedTTL + case authdomain.LoginIPRiskDecisionAllowed: + return s.loginRiskPolicy.AllowedTTL + default: + return s.loginRiskPolicy.UnknownTTL + } +} + +func (s *Service) applyLoginIPRiskDecision(ctx context.Context, job authdomain.LoginIPRiskJob, decision IPDecision, attempts []providerAttempt, status string) error { + nowMs := s.now().UnixMilli() + job.Status = status + job.Decision = decision.Decision + job.CountryCode = normalizeCountryCode(decision.Country) + job.UpdatedAtMs = nowMs + if decision.Decision == authdomain.LoginIPRiskDecisionUnknown { + job.FailureReason = "provider_unavailable" + } + if err := s.authRepository.CompleteLoginIPRiskJob(ctx, job); err != nil { + return err + } + logx.Info(ctx, "login_ip_risk_decision", slog.String("component", "user_auth_risk"), slog.String("job_id", job.JobID), slog.String("session_id", job.SessionID), slog.Int64("user_id", job.UserID), slog.String("decision", job.Decision), slog.String("country", job.CountryCode)) + if decision.Decision != authdomain.LoginIPRiskDecisionBlocked { + return nil + } + revoked, err := s.authRepository.RevokeSessionWithReason(ctx, job.SessionID, nowMs, revokeReasonGeoPolicyBlocked, job.RequestID, "risk_worker") + if err != nil { + return err + } + if s.ipDecisionCache != nil { + if err := s.ipDecisionCache.SetRevokedSession(ctx, job.AppCode, job.SessionID, revokeReasonGeoPolicyBlocked, s.loginRiskPolicy.DenylistTTL); err != nil { + logx.Warn(ctx, "revoked_session_denylist_write_failed", slog.String("component", "user_auth_risk"), slog.String("job_id", job.JobID), slog.String("session_id", job.SessionID), slog.String("error", err.Error())) + } + } + if err := s.authRepository.MarkLoginAuditBlocked(ctx, job.RequestID, job.UserID, job.CountryCode, riskBlockReasonIPAsync); err != nil { + logx.Warn(ctx, "login_audit_mark_blocked_failed", slog.String("component", "user_auth_risk"), slog.String("job_id", job.JobID), slog.String("session_id", job.SessionID), slog.String("error", err.Error())) + } + logx.Info(ctx, "login_session_revoked_by_risk", slog.String("component", "user_auth_risk"), slog.String("job_id", job.JobID), slog.String("session_id", job.SessionID), slog.Int64("user_id", job.UserID), slog.String("reason", revokeReasonGeoPolicyBlocked), slog.String("country", job.CountryCode), slog.Bool("revoked", revoked)) + _ = attempts + return nil +} + +func trimRiskFailure(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + if len(value) > 128 { + return value[:128] + } + return value +} + +func (r IPGeoResult) String() string { + return fmt.Sprintf("%s/%d", r.CountryCode, r.Confidence) +} diff --git a/services/user-service/internal/service/auth/service.go b/services/user-service/internal/service/auth/service.go index 961e6b25..57f20d0e 100644 --- a/services/user-service/internal/service/auth/service.go +++ b/services/user-service/internal/service/auth/service.go @@ -52,12 +52,24 @@ type AuthRepository interface { ReplaceSession(ctx context.Context, oldSessionID string, newSession authdomain.Session, revokedAtMs int64) error // RevokeSession 按 session_id 或 refresh token hash 吊销 session。 RevokeSession(ctx context.Context, sessionID string, refreshTokenHash string, revokedAtMs int64) (bool, error) + // RevokeSessionWithReason 按 session_id 幂等吊销 session,并持久化撤销来源。 + RevokeSessionWithReason(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error) // FindThirdPartyIdentity 查找 provider + subject 的绑定。 FindThirdPartyIdentity(ctx context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error) // CreateThirdPartyUser 原子创建用户、默认短号、三方身份和首个 session。 CreateThirdPartyUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session, invite invitedomain.BindCommand) error // RecordLoginAudit 记录登录审计,主链路不会因为审计失败而失败。 RecordLoginAudit(ctx context.Context, audit authdomain.LoginAudit) error + // CreateLoginIPRiskJob 创建登录后异步 IP 风控任务,失败只影响复核能力。 + CreateLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error + // ClaimLoginIPRiskJobs 按 DB 租约认领待处理风控任务。 + ClaimLoginIPRiskJobs(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) ([]authdomain.LoginIPRiskJob, error) + // CompleteLoginIPRiskJob 写入任务终态。 + CompleteLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error + // CreateLoginIPRiskDecision 持久化 IP 风控决策审计。 + CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error + // MarkLoginAuditBlocked 把登录成功后被异步风控撤销的审计行更新成阻断态。 + MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error } // UserRepository 为 auth service 提供登录签发前的用户快照读取。 @@ -95,6 +107,46 @@ type Config struct { SigningSecret string } +// LoginRiskPolicy 保存 IP 风控 worker 的判定和缓存策略。 +type LoginRiskPolicy struct { + Enabled bool + UnsupportedCountries []string + BlockedTTL time.Duration + AllowedTTL time.Duration + UnknownTTL time.Duration + ProviderTimeout time.Duration + ProviderNames []string + DenylistTTL time.Duration +} + +// IPDecisionCache 是 Redis 风控快查缓存和 session denylist 的最小接口。 +type IPDecisionCache interface { + GetIPDecision(ctx context.Context, appCode string, clientIPHash string) (IPDecision, bool, error) + SetIPDecision(ctx context.Context, appCode string, clientIPHash string, decision IPDecision, ttl time.Duration) error + SetRevokedSession(ctx context.Context, appCode string, sessionID string, reason string, ttl time.Duration) error +} + +// IPDecision 是 Redis 中 IP 决策 JSON 的领域投影。 +type IPDecision struct { + Decision string `json:"decision"` + Country string `json:"country"` + Source string `json:"source"` + CheckedAtMs int64 `json:"checked_at_ms"` + ExpiresAtMs int64 `json:"expires_at_ms"` +} + +// IPGeoProvider 表达一个按优先级调用的 IP Geo 来源。 +type IPGeoProvider interface { + Name() string + ResolveCountry(ctx context.Context, job authdomain.LoginIPRiskJob) (IPGeoResult, error) +} + +// IPGeoResult 是 provider 返回的标准化结果。 +type IPGeoResult struct { + CountryCode string + Confidence int +} + // Service 承载登录注册用例。 type Service struct { // cfg 保存 token 签发配置。 @@ -117,6 +169,12 @@ type Service struct { now func() time.Time // allocateMaxAttempts 是默认短号冲突重试上限。 allocateMaxAttempts int + // loginRiskPolicy 控制登录 IP 风控 worker 的判定和 TTL。 + loginRiskPolicy LoginRiskPolicy + // ipDecisionCache 读写 Redis IP 决策和 session denylist,nil 时按 fail-open 处理。 + ipDecisionCache IPDecisionCache + // ipGeoProviders 按顺序提供 IP 国家解析能力。 + ipGeoProviders []IPGeoProvider } // Option 调整 auth service 的依赖和策略。 @@ -132,6 +190,7 @@ func New(cfg Config, options ...Option) *Service { thirdPartyVerifier: NewStaticThirdPartyVerifier([]string{"wechat"}), now: time.Now, allocateMaxAttempts: 8, + loginRiskPolicy: DefaultLoginRiskPolicy(), } for _, option := range options { @@ -142,6 +201,52 @@ func New(cfg Config, options ...Option) *Service { return svc } +func DefaultLoginRiskPolicy() LoginRiskPolicy { + return LoginRiskPolicy{ + Enabled: true, + UnsupportedCountries: []string{"CN"}, + BlockedTTL: 30 * 24 * time.Hour, + AllowedTTL: 7 * 24 * time.Hour, + UnknownTTL: 30 * time.Minute, + ProviderTimeout: 800 * time.Millisecond, + DenylistTTL: 31 * time.Minute, + } +} + +func WithLoginRiskPolicy(policy LoginRiskPolicy) Option { + return func(s *Service) { + defaults := DefaultLoginRiskPolicy() + if policy.BlockedTTL <= 0 { + policy.BlockedTTL = defaults.BlockedTTL + } + if policy.AllowedTTL <= 0 { + policy.AllowedTTL = defaults.AllowedTTL + } + if policy.UnknownTTL <= 0 { + policy.UnknownTTL = defaults.UnknownTTL + } + if policy.ProviderTimeout <= 0 { + policy.ProviderTimeout = defaults.ProviderTimeout + } + if policy.DenylistTTL <= 0 { + policy.DenylistTTL = time.Duration(s.cfg.AccessTokenTTLSec+60) * time.Second + } + s.loginRiskPolicy = policy + } +} + +func WithIPDecisionCache(cache IPDecisionCache) Option { + return func(s *Service) { + s.ipDecisionCache = cache + } +} + +func WithIPGeoProviders(providers []IPGeoProvider) Option { + return func(s *Service) { + s.ipGeoProviders = providers + } +} + // WithAuthRepository 注入纯认证持久化实现。 func WithAuthRepository(repository AuthRepository) Option { return func(s *Service) { diff --git a/services/user-service/internal/service/auth/service_test.go b/services/user-service/internal/service/auth/service_test.go index b86229ce..abcb5484 100644 --- a/services/user-service/internal/service/auth/service_test.go +++ b/services/user-service/internal/service/auth/service_test.go @@ -4,6 +4,7 @@ package auth_test import ( "context" "strings" + "sync" "testing" "time" @@ -15,6 +16,37 @@ import ( "hyapp/services/user-service/internal/testutil/mysqltest" ) +type memoryDecisionCache struct { + mu sync.Mutex + ip map[string]authservice.IPDecision + revoked map[string]string +} + +func newMemoryDecisionCache() *memoryDecisionCache { + return &memoryDecisionCache{ip: make(map[string]authservice.IPDecision), revoked: make(map[string]string)} +} + +func (c *memoryDecisionCache) GetIPDecision(_ context.Context, appCode string, clientIPHash string) (authservice.IPDecision, bool, error) { + c.mu.Lock() + defer c.mu.Unlock() + decision, ok := c.ip[appCode+":"+clientIPHash] + return decision, ok, nil +} + +func (c *memoryDecisionCache) SetIPDecision(_ context.Context, appCode string, clientIPHash string, decision authservice.IPDecision, _ time.Duration) error { + c.mu.Lock() + defer c.mu.Unlock() + c.ip[appCode+":"+clientIPHash] = decision + return nil +} + +func (c *memoryDecisionCache) SetRevokedSession(_ context.Context, appCode string, sessionID string, reason string, _ time.Duration) error { + c.mu.Lock() + defer c.mu.Unlock() + c.revoked[appCode+":"+sessionID] = reason + return nil +} + type sequenceIDGenerator struct { // values 是测试预设 user_id 序列。 values []int64 @@ -58,15 +90,9 @@ func newUserService(repository *mysqltest.Repository, options ...userservice.Opt return userservice.New(repository, append(base, options...)...) } -func newAuthService(repository *mysqltest.Repository, now *time.Time, ids []int64, displayIDs []string) *authservice.Service { +func newAuthService(repository *mysqltest.Repository, now *time.Time, ids []int64, displayIDs []string, extra ...authservice.Option) *authservice.Service { // 测试服务固定 JWT、发号器、短号候选和时钟,保证 token/session 语义可断言。 - return authservice.New(authservice.Config{ - Issuer: "hyapp-test", - AccessTokenTTLSec: 1800, - RefreshTokenTTLSec: 2592000, - SigningAlg: "HS256", - SigningSecret: "test-secret", - }, + options := []authservice.Option{ authservice.WithAuthRepository(repository), authservice.WithUserRepository(repository), authservice.WithIdentityRepository(repository), @@ -75,7 +101,15 @@ func newAuthService(repository *mysqltest.Repository, now *time.Time, ids []int6 authservice.WithDisplayUserIDAllocator(sequenceDisplayUserIDAllocator{values: displayIDs}), authservice.WithClock(func() time.Time { return *now }), authservice.WithThirdPartyVerifier(authservice.NewStaticThirdPartyVerifier([]string{"wechat"})), - ) + } + options = append(options, extra...) + return authservice.New(authservice.Config{ + Issuer: "hyapp-test", + AccessTokenTTLSec: 1800, + RefreshTokenTTLSec: 2592000, + SigningAlg: "HS256", + SigningSecret: "test-secret", + }, options...) } func thirdPartyRegistration(platform string) authdomain.ThirdPartyRegistration { @@ -222,6 +256,76 @@ func TestRefreshTokenCarriesCompletedOnboardingStatus(t *testing.T) { } } +func TestLoginIPRiskWorkerRevokesBlockedCountrySession(t *testing.T) { + ctx := context.Background() + repository := mysqltest.NewRepository(t) + seedCountry(t, repository, "CN") + now := time.UnixMilli(1000) + cache := newMemoryDecisionCache() + authSvc := newAuthService(repository, &now, []int64{900020}, []string{"100020"}, + authservice.WithLoginRiskPolicy(authservice.LoginRiskPolicy{ + Enabled: true, + UnsupportedCountries: []string{"CN"}, + BlockedTTL: time.Hour, + AllowedTTL: time.Hour, + UnknownTTL: time.Minute, + ProviderTimeout: 50 * time.Millisecond, + ProviderNames: []string{"edge_country"}, + DenylistTTL: time.Hour, + }), + authservice.WithIPDecisionCache(cache), + authservice.WithIPGeoProviders(authservice.BuildIPGeoProviders([]string{"edge_country"})), + ) + + token, _, err := authSvc.LoginThirdParty(ctx, "wechat", "openid-risk-blocked", thirdPartyRegistration("ios"), authservice.Meta{ + AppCode: "lalu", + RequestID: "req-risk-blocked", + ClientIP: "203.0.113.77", + CountryByIP: "CN", + Platform: "ios", + Language: "en-US", + Timezone: "America/Los_Angeles", + }) + if err != nil { + t.Fatalf("LoginThirdParty failed: %v", err) + } + + now = time.UnixMilli(2000) + processed, err := authSvc.ProcessLoginIPRiskBatch(ctx, authservice.LoginIPRiskWorkerOptions{ + WorkerID: "risk-test", + LockTTL: time.Second, + BatchSize: 10, + }) + if err != nil || processed != 1 { + t.Fatalf("risk worker mismatch: processed=%d err=%v", processed, err) + } + if _, err := authSvc.RefreshToken(ctx, token.RefreshToken, "dev-ios", authservice.Meta{AppCode: "lalu", RequestID: "req-refresh-revoked"}); !xerr.IsCode(err, xerr.SessionRevoked) { + t.Fatalf("expected revoked refresh session, got %v", err) + } + if got := cache.revoked["lalu:"+token.SessionID]; got != "GEO_POLICY_BLOCKED" { + t.Fatalf("revoked denylist mismatch: %q", got) + } + decision, ok := cache.ip["lalu:"+authservice.HashLoginRiskIP("203.0.113.77")] + if !ok || decision.Decision != "blocked" || decision.Country != "CN" { + t.Fatalf("blocked IP cache mismatch: ok=%v decision=%+v", ok, decision) + } + + var result string + var blocked bool + var blockReason string + err = repository.RawDB().QueryRowContext(ctx, ` + SELECT result, blocked, block_reason + FROM login_audit + WHERE app_code = 'lalu' AND request_id = 'req-risk-blocked' AND user_id = ? + `, token.UserID).Scan(&result, &blocked, &blockReason) + if err != nil { + t.Fatalf("query login audit failed: %v", err) + } + if result != "revoked" || !blocked || blockReason != "ip_async" { + t.Fatalf("login audit risk mark mismatch: result=%s blocked=%v reason=%s", result, blocked, blockReason) + } +} + func TestIssueAccessTokenForSessionCarriesCompletedOnboardingStatusWithoutRotatingRefresh(t *testing.T) { // CompleteOnboarding 后只需要替换 access token;refresh token 原文不可从数据库反查,也不应被静默轮换。 ctx := context.Background() diff --git a/services/user-service/internal/service/auth/third_party.go b/services/user-service/internal/service/auth/third_party.go index aba1b8c4..5229cbd4 100644 --- a/services/user-service/internal/service/auth/third_party.go +++ b/services/user-service/internal/service/auth/third_party.go @@ -46,6 +46,9 @@ func (s *Service) LoginThirdParty(ctx context.Context, provider string, credenti provider = strings.ToLower(strings.TrimSpace(provider)) credential = strings.TrimSpace(credential) registration = normalizeThirdPartyRegistration(registration, meta) + meta.Platform = registration.Platform + meta.Language = registration.Language + meta.Timezone = registration.Timezone if provider == "" || credential == "" { // provider/credential 是认证材料,缺失时属于请求参数错误,不进入 provider verifier。 return authdomain.Token{}, false, xerr.New(xerr.InvalidArgument, "provider and credential are required") @@ -71,7 +74,7 @@ func (s *Service) LoginThirdParty(ctx context.Context, provider string, credenti // 已存在 provider + subject 绑定时走登录路径,否则走注册事务。 identity, err := s.authRepository.FindThirdPartyIdentity(ctx, provider, profile.ProviderSubject) if err == nil { - return s.loginExistingThirdParty(ctx, identity, registration.DeviceID, meta) + return s.loginExistingThirdParty(ctx, identity, registration, meta) } if !xerr.IsCode(err, xerr.NotFound) { // 非 not found 的存储错误不能退化为注册。 @@ -87,7 +90,7 @@ func (s *Service) LoginThirdParty(ctx context.Context, provider string, credenti return s.createThirdPartyUser(ctx, provider, profile, registration, meta) } -func (s *Service) loginExistingThirdParty(ctx context.Context, identity authdomain.ThirdPartyIdentity, deviceID string, meta Meta) (authdomain.Token, bool, error) { +func (s *Service) loginExistingThirdParty(ctx context.Context, identity authdomain.ThirdPartyIdentity, registration authdomain.ThirdPartyRegistration, meta Meta) (authdomain.Token, bool, error) { // 已绑定三方用户登录时仍要重新读取用户快照,确保禁用状态和靓号过期生效。 user, err := s.freshUser(ctx, identity.UserID, s.now().UnixMilli(), meta.RequestID) if err != nil { @@ -101,7 +104,7 @@ func (s *Service) loginExistingThirdParty(ctx context.Context, identity authdoma } // 每次三方登录创建新的 refresh session。 - session, refreshToken, err := s.newSession(meta.AppCode, user.UserID, deviceID) + session, refreshToken, err := s.newSession(meta.AppCode, user.UserID, registration.DeviceID) if err != nil { return authdomain.Token{}, false, err } @@ -112,7 +115,11 @@ func (s *Service) loginExistingThirdParty(ctx context.Context, identity authdoma s.audit(ctx, meta, user.UserID, loginThird, identity.Provider, resultSuccess, "") // isNewUser=false 表示本次只创建 session,没有创建用户。 token, err := s.issueToken(user, session.SessionID, refreshToken) - return token, false, err + if err != nil { + return authdomain.Token{}, false, err + } + s.enqueueLoginIPRiskJob(ctx, meta, token, normalizeThirdPartyChannel(identity.Provider), registration.Platform, registration.Language, registration.Timezone) + return token, false, nil } func (s *Service) createThirdPartyUser(ctx context.Context, provider string, profile authdomain.ThirdPartyProfile, registration authdomain.ThirdPartyRegistration, meta Meta) (authdomain.Token, bool, error) { @@ -163,7 +170,11 @@ func (s *Service) createThirdPartyUser(ctx context.Context, provider string, pro // repository 事务成功后,用户、默认短号、三方绑定和首个 session 同时存在。 s.audit(ctx, meta, user.UserID, loginThird, provider, resultSuccess, "") token, tokenErr := s.issueToken(user, session.SessionID, refreshToken) - return token, true, tokenErr + if tokenErr != nil { + return authdomain.Token{}, false, tokenErr + } + s.enqueueLoginIPRiskJob(ctx, meta, token, normalizeThirdPartyChannel(provider), registration.Platform, registration.Language, registration.Timezone) + return token, true, nil } if xerr.IsCode(err, xerr.DisplayUserIDExists) { // 默认短号冲突可重试,user_id 和 display_user_id 都会重新生成。 @@ -174,7 +185,7 @@ func (s *Service) createThirdPartyUser(ctx context.Context, provider string, pro // 并发注册同一 provider subject 时,尝试转为已有三方登录。 identity, findErr := s.authRepository.FindThirdPartyIdentity(ctx, provider, profile.ProviderSubject) if findErr == nil { - return s.loginExistingThirdParty(ctx, identity, registration.DeviceID, meta) + return s.loginExistingThirdParty(ctx, identity, registration, meta) } } diff --git a/services/user-service/internal/service/mictime/service.go b/services/user-service/internal/service/mictime/service.go index d3a5c888..88b11ed8 100644 --- a/services/user-service/internal/service/mictime/service.go +++ b/services/user-service/internal/service/mictime/service.go @@ -39,7 +39,6 @@ type WorkerOptions struct { // CompensationWorkerOptions controls abnormal open session closure. type CompensationWorkerOptions struct { WorkerID string - PollInterval time.Duration BatchSize int PendingPublishMaxAge time.Duration PublishingSessionMaxAge time.Duration @@ -92,14 +91,10 @@ 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) { +// ProcessOpenSessionCompensationBatch exposes one bounded compensation scan for cron-service. +func (s *Service) ProcessOpenSessionCompensationBatch(ctx context.Context, options CompensationWorkerOptions) (mictimedomain.CompensationResult, error) { 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 + return mictimedomain.CompensationResult{}, xerr.New(xerr.Unavailable, "mic time repository is not configured") } if options.BatchSize <= 0 { options.BatchSize = 100 @@ -110,25 +105,12 @@ func (s *Service) RunOpenSessionCompensationWorker(ctx context.Context, options 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): - } - } + return s.repository.CompensateOpenSessions(ctx, mictimedomain.CompensationOptions{ + NowMs: time.Now().UnixMilli(), + BatchSize: options.BatchSize, + PendingPublishMaxAgeMs: options.PendingPublishMaxAge.Milliseconds(), + PublishingSessionMaxAgeMs: options.PublishingSessionMaxAge.Milliseconds(), + }) } func (s *Service) processBatch(ctx context.Context, cursor *mictimedomain.RoomOutboxCursor, batchSize int) (int, error) { @@ -145,16 +127,3 @@ 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 10e2aee2..5724e4d7 100644 --- a/services/user-service/internal/service/user/region_rebuild_worker.go +++ b/services/user-service/internal/service/user/region_rebuild_worker.go @@ -11,17 +11,14 @@ import ( ) const ( - defaultRegionRebuildPollInterval = 5 * time.Second - defaultRegionRebuildLockTTL = 30 * time.Second - defaultRegionRebuildBatchSize = 500 + defaultRegionRebuildLockTTL = 30 * time.Second + defaultRegionRebuildBatchSize = 500 ) -// RegionRebuildWorkerOptions 控制 user_region_rebuild_tasks 的后台消费节奏。 +// RegionRebuildWorkerOptions 控制 cron-service 触发的单批 user_region_rebuild_tasks 处理边界。 type RegionRebuildWorkerOptions struct { // WorkerID 写入 task locked_by,便于排查某个节点是否卡住任务。 WorkerID string - // PollInterval 是没有任务或单轮处理后下一次扫描的间隔。 - PollInterval time.Duration // LockTTL 是单个 task claim 后允许本节点持有的最长时间,过期可被其他节点接管。 LockTTL time.Duration // BatchSize 是单次最多更新的 users 数量,避免大国家全量更新长事务。 @@ -33,9 +30,6 @@ func normalizeRegionRebuildWorkerOptions(options RegionRebuildWorkerOptions) Reg if options.WorkerID == "" { options.WorkerID = "user-region-rebuild" } - if options.PollInterval <= 0 { - options.PollInterval = defaultRegionRebuildPollInterval - } if options.LockTTL <= 0 { options.LockTTL = defaultRegionRebuildLockTTL } @@ -46,29 +40,7 @@ func normalizeRegionRebuildWorkerOptions(options RegionRebuildWorkerOptions) Reg return options } -// RunRegionRebuildWorker 持续消费 user_region_rebuild_tasks,保证管理端映射变更最终刷新历史用户快照。 -func (s *Service) RunRegionRebuildWorker(ctx context.Context, options RegionRebuildWorkerOptions) { - options = normalizeRegionRebuildWorkerOptions(options) - if s.regionRebuildRepository == nil { - logx.Warn(ctx, "worker_disabled", slog.String("worker", "user_region_rebuild"), slog.String("reason", "repository is not configured")) - return - } - - _, _ = s.ProcessNextRegionRebuildTask(ctx, options) - ticker := time.NewTicker(options.PollInterval) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - _, _ = s.ProcessNextRegionRebuildTask(ctx, options) - } - } -} - -// ProcessNextRegionRebuildTask 处理一个待重算任务;错误只影响本轮,worker 下一轮继续扫描。 +// ProcessNextRegionRebuildTask 处理一个待重算任务;错误只影响本批,cron-service 下一轮继续触发。 func (s *Service) ProcessNextRegionRebuildTask(ctx context.Context, options RegionRebuildWorkerOptions) (userdomain.RegionRebuildProcessResult, error) { options = normalizeRegionRebuildWorkerOptions(options) if s.regionRebuildRepository == nil { diff --git a/services/user-service/internal/storage/mysql/auth/audit.go b/services/user-service/internal/storage/mysql/auth/audit.go index d49ef40e..bdb9ec73 100644 --- a/services/user-service/internal/storage/mysql/auth/audit.go +++ b/services/user-service/internal/storage/mysql/auth/audit.go @@ -4,6 +4,7 @@ import ( "context" "hyapp/pkg/appcode" + "hyapp/pkg/xerr" authdomain "hyapp/services/user-service/internal/domain/auth" "hyapp/services/user-service/internal/storage/mysql/shared" ) @@ -11,9 +12,22 @@ import ( func (r *Repository) RecordLoginAudit(ctx context.Context, audit authdomain.LoginAudit) error { // 审计表不能包含密码、三方 credential 或 refresh token 原文。 _, err := r.db.ExecContext(ctx, ` - INSERT INTO login_audit (app_code, request_id, user_id, login_type, provider, result, failure_code, client_ip, user_agent, created_at_ms) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, appcode.Normalize(audit.AppCode), audit.RequestID, nullableUserID(audit.UserID), audit.LoginType, shared.NullableString(audit.Provider), audit.Result, shared.NullableString(audit.FailureCode), shared.NullableString(audit.ClientIP), shared.NullableString(audit.UserAgent), audit.CreatedAtMs) + INSERT INTO login_audit (app_code, request_id, user_id, login_type, provider, channel, platform, result, failure_code, client_ip, ip_country_code, blocked, block_reason, user_agent, created_at_ms) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, appcode.Normalize(audit.AppCode), audit.RequestID, nullableUserID(audit.UserID), audit.LoginType, shared.NullableString(audit.Provider), audit.Channel, audit.Platform, audit.Result, shared.NullableString(audit.FailureCode), shared.NullableString(audit.ClientIP), audit.IPCountryCode, audit.Blocked, audit.BlockReason, shared.NullableString(audit.UserAgent), audit.CreatedAtMs) + return err +} + +func (r *Repository) MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error { + _, err := r.db.ExecContext(ctx, ` + UPDATE login_audit + SET result = 'revoked', + failure_code = ?, + ip_country_code = ?, + blocked = 1, + block_reason = ? + WHERE app_code = ? AND request_id = ? AND user_id = ? + `, string(xerr.AuthLoginBlocked), countryCode, blockReason, appcode.FromContext(ctx), requestID, userID) return err } diff --git a/services/user-service/internal/storage/mysql/auth/risk.go b/services/user-service/internal/storage/mysql/auth/risk.go new file mode 100644 index 00000000..779a6a0b --- /dev/null +++ b/services/user-service/internal/storage/mysql/auth/risk.go @@ -0,0 +1,139 @@ +package auth + +import ( + "context" + "database/sql" + "time" + + "hyapp/pkg/appcode" + authdomain "hyapp/services/user-service/internal/domain/auth" +) + +func (r *Repository) CreateLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error { + _, err := r.db.ExecContext(ctx, ` + INSERT INTO login_ip_risk_jobs ( + app_code, job_id, session_id, user_id, client_ip, client_ip_hash, channel, platform, language, timezone, + request_id, edge_country_code, status, decision, country_code, failure_reason, locked_by, locked_until_ms, + attempt_count, created_at_ms, updated_at_ms + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '', '', NULL, NULL, 0, ?, ?) + `, appcode.Normalize(job.AppCode), job.JobID, job.SessionID, job.UserID, job.ClientIP, job.ClientIPHash, job.Channel, job.Platform, job.Language, job.Timezone, job.RequestID, job.EdgeCountryCode, job.Status, job.CreatedAtMs, job.UpdatedAtMs) + return err +} + +func (r *Repository) ClaimLoginIPRiskJobs(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) ([]authdomain.LoginIPRiskJob, error) { + if batchSize <= 0 { + batchSize = 100 + } + rows, err := r.db.QueryContext(ctx, ` + SELECT app_code, job_id, session_id, user_id, client_ip, client_ip_hash, channel, platform, language, timezone, + request_id, edge_country_code, status, decision, country_code, failure_reason, + COALESCE(locked_by, ''), COALESCE(locked_until_ms, 0), attempt_count, created_at_ms, updated_at_ms + FROM login_ip_risk_jobs + WHERE app_code = ? + AND status IN (?, ?) + AND (locked_until_ms IS NULL OR locked_until_ms <= ?) + ORDER BY updated_at_ms ASC, job_id ASC + LIMIT ? + `, appcode.FromContext(ctx), authdomain.LoginIPRiskStatusPending, authdomain.LoginIPRiskStatusFailed, nowMs, batchSize) + if err != nil { + return nil, err + } + defer rows.Close() + + candidates := make([]authdomain.LoginIPRiskJob, 0, batchSize) + for rows.Next() { + job, err := scanLoginIPRiskJob(rows) + if err != nil { + return nil, err + } + candidates = append(candidates, job) + } + if err := rows.Err(); err != nil { + return nil, err + } + + claimed := make([]authdomain.LoginIPRiskJob, 0, len(candidates)) + lockUntilMs := nowMs + lockTTL.Milliseconds() + for _, job := range candidates { + result, err := r.db.ExecContext(ctx, ` + UPDATE login_ip_risk_jobs + SET status = ?, locked_by = ?, locked_until_ms = ?, attempt_count = attempt_count + 1, updated_at_ms = ? + WHERE app_code = ? AND job_id = ? + AND status IN (?, ?) + AND (locked_until_ms IS NULL OR locked_until_ms <= ?) + `, authdomain.LoginIPRiskStatusRunning, workerID, lockUntilMs, nowMs, job.AppCode, job.JobID, authdomain.LoginIPRiskStatusPending, authdomain.LoginIPRiskStatusFailed, nowMs) + if err != nil { + return nil, err + } + affected, err := result.RowsAffected() + if err != nil { + return nil, err + } + if affected == 1 { + job.Status = authdomain.LoginIPRiskStatusRunning + job.LockedBy = workerID + job.LockedUntilMs = lockUntilMs + job.AttemptCount++ + job.UpdatedAtMs = nowMs + claimed = append(claimed, job) + } + } + + return claimed, nil +} + +func (r *Repository) CompleteLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error { + _, err := r.db.ExecContext(ctx, ` + UPDATE login_ip_risk_jobs + SET status = ?, decision = ?, country_code = ?, failure_reason = ?, locked_by = NULL, locked_until_ms = NULL, updated_at_ms = ? + WHERE app_code = ? AND job_id = ? + `, job.Status, job.Decision, job.CountryCode, job.FailureReason, job.UpdatedAtMs, appcode.Normalize(job.AppCode), job.JobID) + return err +} + +func (r *Repository) CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error { + _, err := r.db.ExecContext(ctx, ` + INSERT INTO login_ip_risk_decisions ( + app_code, decision_id, client_ip, client_ip_hash, decision, country_code, confidence, + provider_attempts_json, decided_at_ms, expires_at_ms, created_at_ms + ) + VALUES (?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?) + `, appcode.Normalize(decision.AppCode), decision.DecisionID, decision.ClientIP, decision.ClientIPHash, decision.Decision, decision.CountryCode, decision.Confidence, decision.ProviderAttemptsJSON, decision.DecidedAtMs, decision.ExpiresAtMs, decision.CreatedAtMs) + return err +} + +type loginIPRiskJobScanner interface { + Scan(dest ...any) error +} + +func scanLoginIPRiskJob(scanner loginIPRiskJobScanner) (authdomain.LoginIPRiskJob, error) { + var job authdomain.LoginIPRiskJob + err := scanner.Scan( + &job.AppCode, + &job.JobID, + &job.SessionID, + &job.UserID, + &job.ClientIP, + &job.ClientIPHash, + &job.Channel, + &job.Platform, + &job.Language, + &job.Timezone, + &job.RequestID, + &job.EdgeCountryCode, + &job.Status, + &job.Decision, + &job.CountryCode, + &job.FailureReason, + &job.LockedBy, + &job.LockedUntilMs, + &job.AttemptCount, + &job.CreatedAtMs, + &job.UpdatedAtMs, + ) + if err == sql.ErrNoRows { + return authdomain.LoginIPRiskJob{}, err + } + return job, err +} diff --git a/services/user-service/internal/storage/mysql/auth/sessions.go b/services/user-service/internal/storage/mysql/auth/sessions.go index 2028eebd..ea5eaa57 100644 --- a/services/user-service/internal/storage/mysql/auth/sessions.go +++ b/services/user-service/internal/storage/mysql/auth/sessions.go @@ -12,8 +12,8 @@ import ( func (r *Repository) CreateSession(ctx context.Context, session authdomain.Session) error { // refresh token 只保存 hash,原文不会写入数据库。 _, err := r.db.ExecContext(ctx, ` - INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, created_at_ms, updated_at_ms) - VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?) + INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms) + VALUES (?, ?, ?, ?, ?, ?, NULL, '', '', '', ?, ?) `, appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.RefreshTokenHash, session.DeviceID, session.ExpiresAtMs, session.CreatedAtMs, session.UpdatedAtMs) if err != nil { // session_id 或 refresh hash 冲突统一映射成 auth 领域冲突。 @@ -90,9 +90,9 @@ func (r *Repository) ReplaceSession(ctx context.Context, oldSessionID string, ne _, err = tx.ExecContext(ctx, ` UPDATE auth_sessions - SET revoked_at_ms = ?, updated_at_ms = ? + SET revoked_at_ms = ?, revoked_reason = ?, revoked_request_id = ?, revoked_by = ?, updated_at_ms = ? WHERE app_code = ? AND session_id = ? - `, revokedAtMs, revokedAtMs, appcode.FromContext(ctx), oldSessionID) + `, revokedAtMs, "REFRESH_ROTATED", "", "refresh_token", revokedAtMs, appcode.FromContext(ctx), oldSessionID) if err != nil { return err } @@ -117,11 +117,19 @@ func (r *Repository) RevokeSession(ctx context.Context, sessionID string, refres sessionID = session.SessionID } + return r.revokeSessionByID(ctx, sessionID, revokedAtMs, "USER_LOGOUT", "", "user_logout") +} + +func (r *Repository) RevokeSessionWithReason(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error) { + return r.revokeSessionByID(ctx, sessionID, revokedAtMs, reason, requestID, revokedBy) +} + +func (r *Repository) revokeSessionByID(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error) { result, err := r.db.ExecContext(ctx, ` UPDATE auth_sessions - SET revoked_at_ms = ?, updated_at_ms = ? + SET revoked_at_ms = ?, revoked_reason = ?, revoked_request_id = ?, revoked_by = ?, updated_at_ms = ? WHERE app_code = ? AND session_id = ? AND revoked_at_ms IS NULL - `, revokedAtMs, revokedAtMs, appcode.FromContext(ctx), sessionID) + `, revokedAtMs, reason, requestID, revokedBy, revokedAtMs, appcode.FromContext(ctx), sessionID) if err != nil { return false, err } @@ -142,10 +150,10 @@ func (r *Repository) findSessionByRefreshHash(ctx context.Context, refreshTokenH var session authdomain.Session var revokedAt sql.NullInt64 err := r.db.QueryRowContext(ctx, ` - SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, created_at_ms, updated_at_ms + SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms FROM auth_sessions WHERE app_code = ? AND refresh_token_hash = ? - `, appcode.FromContext(ctx), refreshTokenHash).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &revokedAt, &session.CreatedAtMs, &session.UpdatedAtMs) + `, appcode.FromContext(ctx), refreshTokenHash).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &revokedAt, &session.RevokedReason, &session.RevokedRequestID, &session.RevokedBy, &session.CreatedAtMs, &session.UpdatedAtMs) if err == sql.ErrNoRows { // 找不到 hash 按 revoked 返回,避免暴露 token 是否存在。 return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked") @@ -166,10 +174,10 @@ func (r *Repository) findSessionByID(ctx context.Context, sessionID string) (aut var session authdomain.Session var revokedAt sql.NullInt64 err := r.db.QueryRowContext(ctx, ` - SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, created_at_ms, updated_at_ms + SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms FROM auth_sessions WHERE app_code = ? AND session_id = ? - `, appcode.FromContext(ctx), sessionID).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &revokedAt, &session.CreatedAtMs, &session.UpdatedAtMs) + `, appcode.FromContext(ctx), sessionID).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &revokedAt, &session.RevokedReason, &session.RevokedRequestID, &session.RevokedBy, &session.CreatedAtMs, &session.UpdatedAtMs) if err == sql.ErrNoRows { // 不暴露 session_id 是否存在,统一按 revoked 处理。 return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked") @@ -187,8 +195,8 @@ func (r *Repository) findSessionByID(ctx context.Context, sessionID string) (aut func insertSession(ctx context.Context, tx *sql.Tx, session authdomain.Session) error { // insertSession 只在已有事务中使用,保证与 session 替换或三方注册同提交。 _, err := tx.ExecContext(ctx, ` - INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, created_at_ms, updated_at_ms) - VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?) + INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms) + VALUES (?, ?, ?, ?, ?, ?, NULL, '', '', '', ?, ?) `, appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.RefreshTokenHash, session.DeviceID, session.ExpiresAtMs, session.CreatedAtMs, session.UpdatedAtMs) if err != nil { return mapAuthDuplicateError(err) diff --git a/services/user-service/internal/storage/mysql/mictime/repository.go b/services/user-service/internal/storage/mysql/mictime/repository.go index 82d204b7..d3298ffa 100644 --- a/services/user-service/internal/storage/mysql/mictime/repository.go +++ b/services/user-service/internal/storage/mysql/mictime/repository.go @@ -145,19 +145,20 @@ func (r *Repository) CompensateOpenSessions(ctx context.Context, options mictime pendingCutoff := options.NowMs - options.PendingPublishMaxAgeMs publishingCutoff := options.NowMs - options.PublishingSessionMaxAgeMs + app := appcode.FromContext(ctx) 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 ( + WHERE app_code = ? AND (( 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) + `, app, mictimedomain.SessionStatusPendingPublish, pendingCutoff, mictimedomain.SessionStatusPublishing, publishingCutoff, options.BatchSize) if err != nil { return mictimedomain.CompensationResult{}, err } diff --git a/services/user-service/internal/storage/mysql/region/rebuild_tasks.go b/services/user-service/internal/storage/mysql/region/rebuild_tasks.go index a7c4fdb3..3855ead7 100644 --- a/services/user-service/internal/storage/mysql/region/rebuild_tasks.go +++ b/services/user-service/internal/storage/mysql/region/rebuild_tasks.go @@ -21,7 +21,7 @@ func (r *Repository) ProcessNextRegionRebuildTask(ctx context.Context, workerID } // claim 必须独立提交:running/locked_until/attempt_count 要对其他 worker 和运维可见。 - task, ok, err := claimRegionRebuildTask(ctx, r.db, workerID, nowMs, lockTTL) + task, ok, err := claimRegionRebuildTask(ctx, r.db, appcode.FromContext(ctx), workerID, nowMs, lockTTL) if err != nil { return userdomain.RegionRebuildProcessResult{}, err } @@ -116,14 +116,14 @@ func insertRebuildTask(ctx context.Context, tx *sql.Tx, appCode string, regionID return err } -func claimRegionRebuildTask(ctx context.Context, db *sql.DB, workerID string, nowMs int64, lockTTL time.Duration) (userdomain.UserRegionRebuildTask, bool, error) { +func claimRegionRebuildTask(ctx context.Context, db *sql.DB, appCode string, workerID string, nowMs int64, lockTTL time.Duration) (userdomain.UserRegionRebuildTask, bool, error) { tx, err := db.BeginTx(ctx, nil) if err != nil { return userdomain.UserRegionRebuildTask{}, false, err } defer tx.Rollback() - task, ok, err := selectClaimableRegionRebuildTask(ctx, tx, nowMs) + task, ok, err := selectClaimableRegionRebuildTask(ctx, tx, appCode, nowMs) if err != nil { return userdomain.UserRegionRebuildTask{}, false, err } @@ -157,17 +157,18 @@ func claimRegionRebuildTask(ctx context.Context, db *sql.DB, workerID string, no return task, true, nil } -func selectClaimableRegionRebuildTask(ctx context.Context, tx *sql.Tx, nowMs int64) (userdomain.UserRegionRebuildTask, bool, error) { +func selectClaimableRegionRebuildTask(ctx context.Context, tx *sql.Tx, appCode string, nowMs int64) (userdomain.UserRegionRebuildTask, bool, error) { row := tx.QueryRowContext(ctx, ` SELECT app_code, task_id, region_id, country_code, target_region_id, mapping_revision, status, cursor_user_id, locked_by, locked_until_ms, attempt_count, error_message, created_at_ms, updated_at_ms FROM user_region_rebuild_tasks - WHERE status = ? - OR (status = ? AND COALESCE(locked_until_ms, 0) <= ?) + WHERE app_code = ? + AND (status = ? + OR (status = ? AND COALESCE(locked_until_ms, 0) <= ?)) ORDER BY updated_at_ms ASC, task_id ASC LIMIT 1 FOR UPDATE SKIP LOCKED - `, userdomain.RegionRebuildTaskStatusPending, userdomain.RegionRebuildTaskStatusRunning, nowMs) + `, appcode.Normalize(appCode), userdomain.RegionRebuildTaskStatusPending, userdomain.RegionRebuildTaskStatusRunning, nowMs) task, err := scanRegionRebuildTask(row) if err == sql.ErrNoRows { return userdomain.UserRegionRebuildTask{}, false, nil diff --git a/services/user-service/internal/testutil/mysqltest/mysqltest.go b/services/user-service/internal/testutil/mysqltest/mysqltest.go index 2a15b27c..a8255934 100644 --- a/services/user-service/internal/testutil/mysqltest/mysqltest.go +++ b/services/user-service/internal/testutil/mysqltest/mysqltest.go @@ -221,6 +221,11 @@ func (r *Repository) RevokeSession(ctx context.Context, sessionID string, refres return r.Repository.AuthRepository().RevokeSession(ctx, sessionID, refreshTokenHash, revokedAtMs) } +// RevokeSessionWithReason 让测试 wrapper 继续满足认证接口。 +func (r *Repository) RevokeSessionWithReason(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error) { + return r.Repository.AuthRepository().RevokeSessionWithReason(ctx, sessionID, revokedAtMs, reason, requestID, revokedBy) +} + // FindThirdPartyIdentity 让测试 wrapper 继续满足认证接口。 func (r *Repository) FindThirdPartyIdentity(ctx context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error) { return r.Repository.AuthRepository().FindThirdPartyIdentity(ctx, provider, providerSubject) @@ -236,6 +241,31 @@ func (r *Repository) RecordLoginAudit(ctx context.Context, audit authdomain.Logi return r.Repository.AuthRepository().RecordLoginAudit(ctx, audit) } +// CreateLoginIPRiskJob 让测试 wrapper 继续满足认证接口。 +func (r *Repository) CreateLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error { + return r.Repository.AuthRepository().CreateLoginIPRiskJob(ctx, job) +} + +// ClaimLoginIPRiskJobs 让测试 wrapper 继续满足认证接口。 +func (r *Repository) ClaimLoginIPRiskJobs(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) ([]authdomain.LoginIPRiskJob, error) { + return r.Repository.AuthRepository().ClaimLoginIPRiskJobs(ctx, workerID, nowMs, lockTTL, batchSize) +} + +// CompleteLoginIPRiskJob 让测试 wrapper 继续满足认证接口。 +func (r *Repository) CompleteLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error { + return r.Repository.AuthRepository().CompleteLoginIPRiskJob(ctx, job) +} + +// CreateLoginIPRiskDecision 让测试 wrapper 继续满足认证接口。 +func (r *Repository) CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error { + return r.Repository.AuthRepository().CreateLoginIPRiskDecision(ctx, decision) +} + +// MarkLoginAuditBlocked 让测试 wrapper 继续满足认证接口。 +func (r *Repository) MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error { + return r.Repository.AuthRepository().MarkLoginAuditBlocked(ctx, requestID, userID, countryCode, blockReason) +} + // PutUser seeds or overwrites a user row plus its active default display_user_id. func (r *Repository) PutUser(user userdomain.User) { r.t.Helper() diff --git a/services/user-service/internal/transport/grpc/convert.go b/services/user-service/internal/transport/grpc/convert.go index 4142f6e5..3807903f 100644 --- a/services/user-service/internal/transport/grpc/convert.go +++ b/services/user-service/internal/transport/grpc/convert.go @@ -194,6 +194,9 @@ func authMeta(meta *userv1.RequestMeta) authservice.Meta { ClientIP: meta.GetClientIp(), UserAgent: meta.GetUserAgent(), CountryByIP: meta.GetCountryByIp(), + Platform: meta.GetPlatform(), + Language: meta.GetLanguage(), + Timezone: meta.GetTimezone(), } } diff --git a/services/user-service/internal/transport/grpc/cron.go b/services/user-service/internal/transport/grpc/cron.go new file mode 100644 index 00000000..8185f092 --- /dev/null +++ b/services/user-service/internal/transport/grpc/cron.go @@ -0,0 +1,97 @@ +package grpc + +import ( + "context" + "time" + + userv1 "hyapp.local/api/proto/user/v1" + "hyapp/pkg/xerr" + userdomain "hyapp/services/user-service/internal/domain/user" + authservice "hyapp/services/user-service/internal/service/auth" + mictimeservice "hyapp/services/user-service/internal/service/mictime" + userservice "hyapp/services/user-service/internal/service/user" +) + +// ProcessLoginIPRiskBatch 处理一批登录后 IP 风控任务,任务 claim 和 session revoke 仍在 user-service 内完成。 +func (s *Server) ProcessLoginIPRiskBatch(ctx context.Context, req *userv1.CronBatchRequest) (*userv1.CronBatchResponse, error) { + ctx = contextWithApp(ctx, req.GetMeta()) + if s.authSvc == nil { + return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "auth service is not configured")) + } + processed, err := s.authSvc.ProcessLoginIPRiskBatch(ctx, authservice.LoginIPRiskWorkerOptions{ + WorkerID: req.GetWorkerId(), + LockTTL: durationFromMillis(req.GetLockTtlMs()), + BatchSize: int(req.GetBatchSize()), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + return &userv1.CronBatchResponse{ + ClaimedCount: int32(processed), + ProcessedCount: int32(processed), + SuccessCount: int32(processed), + HasMore: processed >= normalizedBatchSize(req.GetBatchSize(), 100), + }, nil +} + +// ProcessRegionRebuildBatch 处理一个区域重算分页任务,按 app_code 作用域 claim。 +func (s *Server) ProcessRegionRebuildBatch(ctx context.Context, req *userv1.CronBatchRequest) (*userv1.CronBatchResponse, error) { + ctx = contextWithApp(ctx, req.GetMeta()) + if s.userSvc == nil { + return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "user service is not configured")) + } + result, err := s.userSvc.ProcessNextRegionRebuildTask(ctx, userservice.RegionRebuildWorkerOptions{ + WorkerID: req.GetWorkerId(), + LockTTL: durationFromMillis(req.GetLockTtlMs()), + BatchSize: int(req.GetBatchSize()), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + if result.NoTask { + return &userv1.CronBatchResponse{}, nil + } + return &userv1.CronBatchResponse{ + ClaimedCount: 1, + ProcessedCount: int32(result.ProcessedUsers), + SuccessCount: 1, + HasMore: result.Status == userdomain.RegionRebuildTaskStatusPending, + }, nil +} + +// CompensateMicOpenSessions 关闭异常长时间未收到 down 事件的用户麦位统计 session,不反写 Room Cell。 +func (s *Server) CompensateMicOpenSessions(ctx context.Context, req *userv1.CronBatchRequest) (*userv1.CronBatchResponse, error) { + ctx = contextWithApp(ctx, req.GetMeta()) + if s.micTimeSvc == nil { + return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "mic time service is not configured")) + } + result, err := s.micTimeSvc.ProcessOpenSessionCompensationBatch(ctx, mictimeservice.CompensationWorkerOptions{ + WorkerID: req.GetWorkerId(), + BatchSize: int(req.GetBatchSize()), + PendingPublishMaxAge: durationFromMillis(req.GetPendingPublishMaxAgeMs()), + PublishingSessionMaxAge: durationFromMillis(req.GetPublishingSessionMaxAgeMs()), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + return &userv1.CronBatchResponse{ + ClaimedCount: int32(result.ClosedCount), + ProcessedCount: int32(result.ClosedCount), + SuccessCount: int32(result.ClosedCount), + HasMore: result.ClosedCount >= normalizedBatchSize(req.GetBatchSize(), 100), + }, nil +} + +func durationFromMillis(value int64) time.Duration { + if value <= 0 { + return 0 + } + return time.Duration(value) * time.Millisecond +} + +func normalizedBatchSize(value int32, fallback int) int { + if value <= 0 { + return fallback + } + return int(value) +} diff --git a/services/user-service/internal/transport/grpc/server.go b/services/user-service/internal/transport/grpc/server.go index 959ada0f..9492bfd5 100644 --- a/services/user-service/internal/transport/grpc/server.go +++ b/services/user-service/internal/transport/grpc/server.go @@ -37,6 +37,8 @@ type Server struct { userv1.UnimplementedUserHostServiceServer // UnimplementedUserHostAdminServiceServer 提供 host 后台关系管理 RPC 的默认返回。 userv1.UnimplementedUserHostAdminServiceServer + // UnimplementedUserCronServiceServer 提供 cron-service 批处理 RPC 的默认返回。 + userv1.UnimplementedUserCronServiceServer // authSvc 承载登录、设置密码、refresh 和 logout。 authSvc *authservice.Service @@ -155,6 +157,13 @@ func (s *Server) Logout(ctx context.Context, req *userv1.LogoutRequest) (*userv1 return &userv1.LogoutResponse{Revoked: revoked}, nil } +// RecordLoginBlocked 记录 gateway 登录入口快拦审计,不参与认证主链路。 +func (s *Server) RecordLoginBlocked(ctx context.Context, req *userv1.RecordLoginBlockedRequest) (*userv1.RecordLoginBlockedResponse, error) { + ctx = contextWithApp(ctx, req.GetMeta()) + recorded := s.authSvc.RecordLoginBlocked(ctx, authMeta(req.GetMeta()), req.GetChannel(), req.GetPlatform(), req.GetLanguage(), req.GetTimezone(), req.GetBlockReason()) + return &userv1.RecordLoginBlockedResponse{Recorded: recorded}, nil +} + // GetUser 返回用户主状态。 func (s *Server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) diff --git a/services/wallet-service/internal/domain/ledger/ledger.go b/services/wallet-service/internal/domain/ledger/ledger.go index 9cd14552..69987dbc 100644 --- a/services/wallet-service/internal/domain/ledger/ledger.go +++ b/services/wallet-service/internal/domain/ledger/ledger.go @@ -162,6 +162,26 @@ type AdminCreditAssetCommand struct { EvidenceRef string } +// TaskRewardCommand 是 activity-service 任务领奖的 COIN 入账命令。 +type TaskRewardCommand struct { + AppCode string + CommandID string + TargetUserID int64 + Amount int64 + TaskType string + TaskID string + CycleKey string + Reason string +} + +// TaskRewardReceipt 是任务奖励入账后的稳定回执。 +type TaskRewardReceipt struct { + TransactionID string + Balance AssetBalance + Amount int64 + GrantedAtMS int64 +} + // ValidAssetType 限定账本允许落账的资产类型,避免写入任意字符串资产。 func ValidAssetType(assetType string) bool { switch assetType { diff --git a/services/wallet-service/internal/service/wallet/service.go b/services/wallet-service/internal/service/wallet/service.go index 070469d3..e25d649c 100644 --- a/services/wallet-service/internal/service/wallet/service.go +++ b/services/wallet-service/internal/service/wallet/service.go @@ -17,6 +17,7 @@ type Repository interface { 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) + CreditTaskReward(ctx context.Context, command ledger.TaskRewardCommand) (ledger.TaskRewardReceipt, error) ListRechargeBills(ctx context.Context, query ledger.ListRechargeBillsQuery) ([]ledger.RechargeBill, int64, error) ListResources(ctx context.Context, query resourcedomain.ListResourcesQuery) ([]resourcedomain.Resource, int64, error) GetResource(ctx context.Context, resourceID int64) (resourcedomain.Resource, error) @@ -187,6 +188,28 @@ func (s *Service) TransferCoinFromSeller(ctx context.Context, command ledger.Coi return s.repository.TransferCoinFromSeller(ctx, command) } +// CreditTaskReward 发放任务奖励金币;任务完成判断属于 activity-service,这里只负责账本幂等入账。 +func (s *Service) CreditTaskReward(ctx context.Context, command ledger.TaskRewardCommand) (ledger.TaskRewardReceipt, error) { + if command.CommandID == "" || command.TargetUserID <= 0 || command.Amount <= 0 || command.TaskID == "" || command.CycleKey == "" { + return ledger.TaskRewardReceipt{}, xerr.New(xerr.InvalidArgument, "task reward command is incomplete") + } + command.TaskType = strings.ToLower(strings.TrimSpace(command.TaskType)) + if command.TaskType != "daily" && command.TaskType != "exclusive" { + return ledger.TaskRewardReceipt{}, xerr.New(xerr.InvalidArgument, "task_type is invalid") + } + command.Reason = strings.TrimSpace(command.Reason) + if command.Reason == "" { + return ledger.TaskRewardReceipt{}, xerr.New(xerr.InvalidArgument, "reason is required") + } + if s.repository == nil { + return ledger.TaskRewardReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") + } + command.AppCode = appcode.Normalize(command.AppCode) + ctx = appcode.WithContext(ctx, command.AppCode) + + return s.repository.CreditTaskReward(ctx, command) +} + // ListRechargeBills 返回充值账单只读列表;所有充值来源必须先在 wallet-service 落充值记录。 func (s *Service) ListRechargeBills(ctx context.Context, query ledger.ListRechargeBillsQuery) ([]ledger.RechargeBill, int64, error) { if s.repository == nil { diff --git a/services/wallet-service/internal/service/wallet/service_test.go b/services/wallet-service/internal/service/wallet/service_test.go index 93ee8b29..daac2c15 100644 --- a/services/wallet-service/internal/service/wallet/service_test.go +++ b/services/wallet-service/internal/service/wallet/service_test.go @@ -171,6 +171,49 @@ func TestAdminCreditAssetIsIdempotent(t *testing.T) { } } +// TestCreditTaskRewardIsIdempotent 验证任务奖励走独立账务类型且重复领奖命令不会重复发金币。 +func TestCreditTaskRewardIsIdempotent(t *testing.T) { + repository := mysqltest.NewRepository(t) + svc := walletservice.New(repository) + command := ledger.TaskRewardCommand{ + CommandID: "cmd-task-reward", + TargetUserID: 21001, + Amount: 88, + TaskType: "daily", + TaskID: "task-1", + CycleKey: "2026-05-09", + Reason: "daily_task_reward", + } + + first, err := svc.CreditTaskReward(context.Background(), command) + if err != nil { + t.Fatalf("CreditTaskReward failed: %v", err) + } + second, err := svc.CreditTaskReward(context.Background(), command) + if err != nil { + t.Fatalf("CreditTaskReward retry failed: %v", err) + } + if first.TransactionID == "" || first.TransactionID != second.TransactionID || first.Amount != 88 || second.Balance.AvailableAmount != 88 { + t.Fatalf("task reward receipt mismatch: first=%+v second=%+v", first, second) + } + balances, err := svc.GetBalances(context.Background(), 21001, []string{ledger.AssetCoin}) + if err != nil { + t.Fatalf("GetBalances failed: %v", err) + } + if balanceAmount(balances, ledger.AssetCoin) != 88 { + t.Fatalf("task reward balance mismatch: %+v", balances) + } + if got := repository.CountRows("wallet_transactions", "command_id = ? AND biz_type = ?", command.CommandID, "task_reward"); got != 1 { + t.Fatalf("task reward should write one transaction, got %d", got) + } + if got := repository.CountRows("wallet_entries", "transaction_id = ?", first.TransactionID); got != 1 { + t.Fatalf("task reward should write one entry, got %d", got) + } + if got := repository.CountRows("wallet_outbox", "transaction_id = ?", first.TransactionID); got != 2 { + t.Fatalf("task reward should write balance and task outbox events, got %d", got) + } +} + // TestTransferCoinFromSellerMovesDedicatedAssetToPlayerCoin 验证币商专用金币和玩家普通金币不会混账。 func TestTransferCoinFromSellerMovesDedicatedAssetToPlayerCoin(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 c353a1df..df89d189 100644 --- a/services/wallet-service/internal/storage/mysql/repository.go +++ b/services/wallet-service/internal/storage/mysql/repository.go @@ -24,6 +24,7 @@ const ( transactionStatusSucceeded = "succeeded" bizTypeGiftDebit = "gift_debit" bizTypeManualCredit = "manual_credit" + bizTypeTaskReward = "task_reward" bizTypeCoinSellerTransfer = "coin_seller_transfer" bizTypeCoinSellerStockPurchase = "coin_seller_stock_purchase" bizTypeCoinSellerCoinCompensation = "coin_seller_coin_compensation" @@ -347,6 +348,78 @@ func (r *Repository) AdminCreditAsset(ctx context.Context, command ledger.AdminC return balance, transactionID, nil } +// CreditTaskReward 在一个事务内完成任务奖励 COIN 入账、交易分录和 outbox 事件。 +func (r *Repository) CreditTaskReward(ctx context.Context, command ledger.TaskRewardCommand) (ledger.TaskRewardReceipt, error) { + if r == nil || r.db == nil { + return ledger.TaskRewardReceipt{}, 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.TaskRewardReceipt{}, err + } + defer func() { _ = tx.Rollback() }() + + requestHash := taskRewardRequestHash(command) + if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeTaskReward); err != nil || exists { + if err != nil || !exists { + return ledger.TaskRewardReceipt{}, err + } + return r.receiptForTaskRewardTransaction(ctx, tx, txRow.TransactionID) + } + + nowMs := time.Now().UnixMilli() + account, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs) + if err != nil { + return ledger.TaskRewardReceipt{}, err + } + transactionID := transactionID(command.AppCode, command.CommandID) + balanceAfter := account.AvailableAmount + command.Amount + metadata := taskRewardMetadata{ + AppCode: command.AppCode, + TargetUserID: command.TargetUserID, + AssetType: ledger.AssetCoin, + Amount: command.Amount, + TaskType: command.TaskType, + TaskID: command.TaskID, + CycleKey: command.CycleKey, + Reason: command.Reason, + BalanceAfter: balanceAfter, + GrantedAtMS: nowMs, + } + if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeTaskReward, requestHash, fmt.Sprintf("%s:%s", command.TaskID, command.CycleKey), metadata, nowMs); err != nil { + return ledger.TaskRewardReceipt{}, err + } + if err := r.applyAccountDelta(ctx, tx, account, command.Amount, 0, nowMs); err != nil { + return ledger.TaskRewardReceipt{}, err + } + if err := r.insertEntry(ctx, tx, walletEntry{ + TransactionID: transactionID, + UserID: command.TargetUserID, + AssetType: ledger.AssetCoin, + AvailableDelta: command.Amount, + FrozenDelta: 0, + AvailableAfter: balanceAfter, + FrozenAfter: account.FrozenAmount, + CreatedAtMS: nowMs, + }); err != nil { + return ledger.TaskRewardReceipt{}, err + } + if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ + balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, balanceAfter, account.FrozenAmount, metadata, nowMs), + taskRewardCreditedEvent(transactionID, command.CommandID, metadata, nowMs), + }); err != nil { + return ledger.TaskRewardReceipt{}, err + } + if err := tx.Commit(); err != nil { + return ledger.TaskRewardReceipt{}, err + } + + return receiptFromTaskRewardMetadata(transactionID, metadata), nil +} + // AdminCreditCoinSellerStock 在同一事务里给币商 COIN_SELLER_COIN 库存入账,并记录专用进货明细。 func (r *Repository) AdminCreditCoinSellerStock(ctx context.Context, command ledger.CoinSellerStockCreditCommand) (ledger.CoinSellerStockCreditReceipt, error) { if r == nil || r.db == nil { @@ -1040,6 +1113,19 @@ type adminCreditMetadata struct { EvidenceRef string `json:"evidence_ref"` } +type taskRewardMetadata struct { + AppCode string `json:"app_code"` + TargetUserID int64 `json:"target_user_id"` + AssetType string `json:"asset_type"` + Amount int64 `json:"amount"` + TaskType string `json:"task_type"` + TaskID string `json:"task_id"` + CycleKey string `json:"cycle_key"` + Reason string `json:"reason"` + BalanceAfter int64 `json:"balance_after"` + GrantedAtMS int64 `json:"granted_at_ms"` +} + type coinSellerTransferMetadata struct { AppCode string `json:"app_code"` SellerUserID int64 `json:"seller_user_id"` @@ -1096,6 +1182,38 @@ func receiptFromGiftMetadata(transactionID string, metadata giftMetadata) ledger } } +func (r *Repository) receiptForTaskRewardTransaction(ctx context.Context, tx *sql.Tx, transactionID string) (ledger.TaskRewardReceipt, error) { + var metadataJSON string + if err := tx.QueryRowContext(ctx, + `SELECT COALESCE(CAST(metadata_json AS CHAR), '{}') + FROM wallet_transactions + WHERE app_code = ? AND transaction_id = ?`, + appcode.FromContext(ctx), + transactionID, + ).Scan(&metadataJSON); err != nil { + return ledger.TaskRewardReceipt{}, err + } + var metadata taskRewardMetadata + if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil { + return ledger.TaskRewardReceipt{}, err + } + return receiptFromTaskRewardMetadata(transactionID, metadata), nil +} + +func receiptFromTaskRewardMetadata(transactionID string, metadata taskRewardMetadata) ledger.TaskRewardReceipt { + return ledger.TaskRewardReceipt{ + TransactionID: transactionID, + Balance: ledger.AssetBalance{ + AppCode: metadata.AppCode, + UserID: metadata.TargetUserID, + AssetType: metadata.AssetType, + AvailableAmount: metadata.BalanceAfter, + }, + Amount: metadata.Amount, + GrantedAtMS: metadata.GrantedAtMS, + } +} + 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, @@ -1225,6 +1343,31 @@ func rechargeRecordedEvent(transactionID string, commandID string, targetUserID } } +func taskRewardCreditedEvent(transactionID string, commandID string, metadata taskRewardMetadata, nowMs int64) walletOutboxEvent { + return walletOutboxEvent{ + EventID: eventID(transactionID, "WalletTaskRewardCredited", metadata.TargetUserID, ledger.AssetCoin), + EventType: "WalletTaskRewardCredited", + TransactionID: transactionID, + CommandID: commandID, + UserID: metadata.TargetUserID, + AssetType: ledger.AssetCoin, + AvailableDelta: metadata.Amount, + FrozenDelta: 0, + Payload: map[string]any{ + "transaction_id": transactionID, + "command_id": commandID, + "user_id": metadata.TargetUserID, + "task_type": metadata.TaskType, + "task_id": metadata.TaskID, + "cycle_key": metadata.CycleKey, + "amount": metadata.Amount, + "reason": metadata.Reason, + "created_at_ms": nowMs, + }, + CreatedAtMS: nowMs, + } +} + func coinSellerStockCreditedEvent(transactionID string, commandID string, metadata coinSellerStockMetadata, nowMs int64) walletOutboxEvent { eventType := "WalletCoinSellerCoinCompensated" if metadata.StockType == ledger.StockTypeUSDTPurchase { @@ -1342,6 +1485,18 @@ func adminCreditRequestHash(command ledger.AdminCreditAssetCommand) string { )) } +func taskRewardRequestHash(command ledger.TaskRewardCommand) string { + return stableHash(fmt.Sprintf("task_reward|%s|%d|%d|%s|%s|%s|%s", + appcode.Normalize(command.AppCode), + command.TargetUserID, + command.Amount, + command.TaskType, + command.TaskID, + command.CycleKey, + strings.TrimSpace(command.Reason), + )) +} + 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), diff --git a/services/wallet-service/internal/transport/grpc/server.go b/services/wallet-service/internal/transport/grpc/server.go index 15fa6275..7a10e11e 100644 --- a/services/wallet-service/internal/transport/grpc/server.go +++ b/services/wallet-service/internal/transport/grpc/server.go @@ -104,6 +104,37 @@ func (s *Server) AdminCreditAsset(ctx context.Context, req *walletv1.AdminCredit }, nil } +// CreditTaskReward 处理 activity-service 任务奖励金币入账。 +func (s *Server) CreditTaskReward(ctx context.Context, req *walletv1.CreditTaskRewardRequest) (*walletv1.CreditTaskRewardResponse, error) { + ctx = appcode.WithContext(ctx, req.GetAppCode()) + receipt, err := s.svc.CreditTaskReward(ctx, ledger.TaskRewardCommand{ + AppCode: req.GetAppCode(), + CommandID: req.GetCommandId(), + TargetUserID: req.GetTargetUserId(), + Amount: req.GetAmount(), + TaskType: req.GetTaskType(), + TaskID: req.GetTaskId(), + CycleKey: req.GetCycleKey(), + Reason: req.GetReason(), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + + return &walletv1.CreditTaskRewardResponse{ + TransactionId: receipt.TransactionID, + Amount: receipt.Amount, + GrantedAtMs: receipt.GrantedAtMS, + Balance: &walletv1.AssetBalance{ + AppCode: receipt.Balance.AppCode, + AssetType: receipt.Balance.AssetType, + AvailableAmount: receipt.Balance.AvailableAmount, + FrozenAmount: receipt.Balance.FrozenAmount, + Version: receipt.Balance.Version, + }, + }, nil +} + // AdminCreditCoinSellerStock 处理后台币商进货或金币补偿,币商身份由 admin-server 先行校验。 func (s *Server) AdminCreditCoinSellerStock(ctx context.Context, req *walletv1.AdminCreditCoinSellerStockRequest) (*walletv1.AdminCreditCoinSellerStockResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode())