Split lucky gift service and ops center
This commit is contained in:
parent
779d864e80
commit
8affe63f6a
88
Makefile
88
Makefile
@ -1,11 +1,44 @@
|
||||
COMPOSE_SERVICES := mysql redis rocketmq-namesrv rocketmq-broker gateway-service room-service wallet-service user-service activity-service cron-service robot-service game-service notice-service statistics-service
|
||||
GO_RUN_SERVICES := gateway-service room-service wallet-service user-service activity-service cron-service robot-service game-service notice-service statistics-service
|
||||
SERVICE_ALIASES := gs gateway rs room ws wallet us user as activity cs cron robot robots rb game games ns notice stats statistics mq namesrv broker mysql redis rocketmq-namesrv rocketmq-broker gateway-service room-service wallet-service user-service activity-service cron-service robot-service game-service notice-service statistics-service
|
||||
COMPOSE_SERVICES := mysql redis rocketmq-namesrv rocketmq-broker gateway-service room-service wallet-service user-service activity-service lucky-gift-service luck-gateway cron-service robot-service game-service notice-service statistics-service
|
||||
GO_RUN_SERVICES := gateway-service room-service wallet-service user-service activity-service lucky-gift-service luck-gateway cron-service robot-service game-service notice-service statistics-service
|
||||
SERVICE_ALIASES := gs gateway rs room ws wallet us user as activity lucky lucky-gift lgs luckygift lg cs cron robot robots rb game games ns notice stats statistics mq namesrv broker mysql redis rocketmq-namesrv rocketmq-broker gateway-service room-service wallet-service user-service activity-service lucky-gift-service luck-gateway cron-service robot-service game-service notice-service statistics-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,$(if $(filter cs cron,$(SERVICE_TOKEN)),cron-service,$(if $(filter robot robots rb,$(SERVICE_TOKEN)),robot-service,$(if $(filter game games,$(SERVICE_TOKEN)),game-service,$(if $(filter ns notice,$(SERVICE_TOKEN)),notice-service,$(if $(filter stats statistics,$(SERVICE_TOKEN)),statistics-service,$(if $(filter namesrv,$(SERVICE_TOKEN)),rocketmq-namesrv,$(if $(filter mq broker,$(SERVICE_TOKEN)),rocketmq-broker,$(SERVICE_TOKEN)))))))))))))))
|
||||
SERVICE_ID := $(strip $(or $(SERVICE),$(SERVICE_TOKEN)))
|
||||
SERVICE_ID := $(patsubst gs,gateway-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst gateway,gateway-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst rs,room-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst room,room-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst ws,wallet-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst wallet,wallet-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst us,user-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst user,user-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst as,activity-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst activity,activity-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst lucky,lucky-gift-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst lucky-gift,lucky-gift-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst lgs,lucky-gift-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst luckygift,lucky-gift-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst lg,luck-gateway,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst cs,cron-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst cron,cron-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst robot,robot-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst robots,robot-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst rb,robot-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst game,game-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst games,game-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst ns,notice-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst notice,notice-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst stats,statistics-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst statistics,statistics-service,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst namesrv,rocketmq-namesrv,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst mq,rocketmq-broker,$(SERVICE_ID))
|
||||
SERVICE_ID := $(patsubst broker,rocketmq-broker,$(SERVICE_ID))
|
||||
ADMIN_CONFIG ?= configs/config.yaml
|
||||
DEV_RUN_SERVICES ?= all
|
||||
DEV_RUN_SERVICE_LIST := $(strip $(if $(filter mysql redis,$(SERVICE_ID)),,$(if $(SERVICE_ID),$(SERVICE_ID),$(DEV_RUN_SERVICES))))
|
||||
DEV_RUN_MQ ?= 0
|
||||
DEV_RUN_COMPOSE_ONLY := mysql redis rocketmq-namesrv rocketmq-broker
|
||||
DEV_RUN_SERVICE_LIST := $(strip $(if $(filter $(DEV_RUN_COMPOSE_ONLY),$(SERVICE_ID)),,$(if $(SERVICE_ID),$(SERVICE_ID),$(DEV_RUN_SERVICES))))
|
||||
ROCKETMQ_BROKER_CONFIG ?= ./tmp/rocketmq/broker.local.conf
|
||||
export ROCKETMQ_BROKER_CONFIG
|
||||
|
||||
.PHONY: proto vet test build run admin admin-up admin-deps admin-bootstrap admin-test admin-build up db-init databi-real-flow-check rebuild restart stop logs ps addr down up-service check-service resolve-compose-conflicts $(SERVICE_ALIASES)
|
||||
|
||||
@ -21,6 +54,7 @@ proto:
|
||||
proto/user/v1/auth.proto \
|
||||
proto/user/v1/host.proto \
|
||||
proto/activity/v1/activity.proto \
|
||||
proto/luckygift/v1/luckygift.proto \
|
||||
proto/room/v1/room.proto \
|
||||
proto/game/v1/game.proto \
|
||||
proto/robot/v1/robot.proto \
|
||||
@ -32,30 +66,46 @@ vet:
|
||||
go vet ./...
|
||||
go vet ./api/...
|
||||
go vet ./server/admin/...
|
||||
go vet ./server/luck-gateway/...
|
||||
|
||||
# `test` 统一跑 workspace 内的 app 后端、api 契约和后台管理服务测试。
|
||||
test: vet
|
||||
go test ./...
|
||||
go test ./api/...
|
||||
go test ./server/admin/...
|
||||
go test ./server/luck-gateway/...
|
||||
|
||||
# `build` 验证 app 后端和后台管理服务命令入口都能编译。
|
||||
build:
|
||||
go build ./...
|
||||
go build ./server/admin/...
|
||||
go build ./server/luck-gateway/...
|
||||
|
||||
# `run` 拉起 MySQL/Redis 后在本机用 go run 启动所有 Go 服务,不做源码监听或热重启。
|
||||
# `run` 默认只拉 MySQL/Redis 并让 go run 服务读取内置默认配置,避免本地开发被 MQ topic/consumer 初始化拖慢。
|
||||
# 需要验证 MQ fanout 时使用 `DEV_RUN_MQ=1 make run`,该模式会恢复 RocketMQ 配置生成和 topic 初始化。
|
||||
# 可用 `make run rs`、`make run SERVICE=room-service` 或 `make run DEV_RUN_SERVICES=user-service,gateway-service` 只跑子集。
|
||||
run:
|
||||
./scripts/resolve-compose-container-conflicts.sh
|
||||
./scripts/prepare-local-mysql-initdb.sh
|
||||
./scripts/prepare-local-rocketmq-config.sh
|
||||
ROCKETMQ_BROKER_CONFIG=./tmp/rocketmq/broker.local.conf docker compose up -d mysql redis rocketmq-namesrv rocketmq-broker
|
||||
@if [ "$(DEV_RUN_MQ)" = "1" ]; then \
|
||||
./scripts/prepare-local-rocketmq-config.sh; \
|
||||
docker compose up -d mysql redis rocketmq-namesrv rocketmq-broker; \
|
||||
else \
|
||||
docker compose up -d mysql redis; \
|
||||
fi
|
||||
./scripts/apply-local-mysql-initdb.sh
|
||||
./scripts/apply-local-rocketmq-topics.sh
|
||||
@if [ "$(DEV_RUN_MQ)" = "1" ]; then \
|
||||
./scripts/apply-local-rocketmq-topics.sh; \
|
||||
else \
|
||||
echo "DEV_RUN_MQ=0: skip RocketMQ startup and topic initialization"; \
|
||||
fi
|
||||
docker compose stop $(GO_RUN_SERVICES) >/dev/null 2>&1 || true
|
||||
./scripts/print-compose-addresses.sh
|
||||
TZ=UTC go run ./cmd/dev-run -services "$(DEV_RUN_SERVICE_LIST)"
|
||||
DEV_RUN_MQ="$(DEV_RUN_MQ)" ./scripts/print-compose-addresses.sh
|
||||
@if [ -n "$(DEV_RUN_SERVICE_LIST)" ]; then \
|
||||
DEV_RUN_MQ="$(DEV_RUN_MQ)" TZ=UTC go run ./cmd/dev-run -services "$(DEV_RUN_SERVICE_LIST)"; \
|
||||
else \
|
||||
echo "compose dependencies are running; no Go service selected"; \
|
||||
fi
|
||||
|
||||
# `admin` 在本机直接启动后台管理后端;依赖默认从 server/admin/configs/config.yaml 读取。
|
||||
# 启动前自动杀掉占用 13100 端口的旧进程,避免 bind: address already in use。
|
||||
@ -73,9 +123,15 @@ admin-deps:
|
||||
./scripts/prepare-local-mysql-initdb.sh
|
||||
docker compose up -d mysql redis
|
||||
./scripts/apply-local-mysql-initdb.sh
|
||||
./scripts/apply-local-rocketmq-topics.sh
|
||||
@if [ "$(DEV_RUN_MQ)" = "1" ]; then \
|
||||
./scripts/prepare-local-rocketmq-config.sh; \
|
||||
docker compose up -d rocketmq-namesrv rocketmq-broker; \
|
||||
./scripts/apply-local-rocketmq-topics.sh; \
|
||||
else \
|
||||
echo "DEV_RUN_MQ=0: skip RocketMQ startup and topic initialization"; \
|
||||
fi
|
||||
docker compose up --build -d user-service
|
||||
./scripts/print-compose-addresses.sh
|
||||
DEV_RUN_MQ="$(DEV_RUN_MQ)" ./scripts/print-compose-addresses.sh
|
||||
|
||||
# `admin-bootstrap` 显式执行后台初始化种子后启动服务。
|
||||
admin-bootstrap:
|
||||
@ -99,7 +155,7 @@ up:
|
||||
docker compose up -d mysql redis; \
|
||||
./scripts/apply-local-mysql-initdb.sh; \
|
||||
./scripts/apply-local-rocketmq-topics.sh; \
|
||||
docker compose up --build -d gateway-service room-service wallet-service user-service activity-service cron-service robot-service game-service notice-service statistics-service; \
|
||||
docker compose up --build -d gateway-service room-service wallet-service user-service activity-service lucky-gift-service luck-gateway cron-service robot-service game-service notice-service statistics-service; \
|
||||
elif [ "$(SERVICE_ID)" = "mysql" ] || [ "$(SERVICE_ID)" = "redis" ]; then \
|
||||
docker compose up -d $(SERVICE_ID); \
|
||||
else \
|
||||
@ -168,13 +224,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 cs/cron game/games ns/notice mysql redis"; \
|
||||
echo "Aliases: gs/gateway rs/room ws/wallet us/user as/activity lucky/lgs lg/luck-gateway cs/cron game/games ns/notice 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 cs/cron game/games ns/notice mysql redis"; \
|
||||
echo "Aliases: gs/gateway rs/room ws/wallet us/user as/activity lucky/lgs lg/luck-gateway cs/cron game/games ns/notice mysql redis"; \
|
||||
echo "Services: $(COMPOSE_SERVICES)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -283,6 +283,7 @@ message TaskItem {
|
||||
string action_param = 22;
|
||||
string action_payload_json = 23;
|
||||
string dimension_filter_json = 24;
|
||||
string reward_asset_type = 25;
|
||||
}
|
||||
|
||||
// TaskSection 固定表达 App 任务页的 daily/exclusive 两个分区。
|
||||
@ -327,6 +328,7 @@ message ClaimTaskRewardResponse {
|
||||
string wallet_transaction_id = 7;
|
||||
int64 granted_at_ms = 8;
|
||||
bool claimed = 9;
|
||||
string reward_asset_type = 10;
|
||||
}
|
||||
|
||||
// ConsumeTaskEventRequest 是异步 outbox consumer 投递给任务系统的事实事件。
|
||||
@ -445,6 +447,7 @@ message TaskDefinition {
|
||||
string action_param = 23;
|
||||
string action_payload_json = 24;
|
||||
string dimension_filter_json = 25;
|
||||
string reward_asset_type = 26;
|
||||
}
|
||||
|
||||
// ListTaskDefinitionsRequest 是后台任务配置列表筛选条件。
|
||||
@ -487,6 +490,7 @@ message UpsertTaskDefinitionRequest {
|
||||
string action_param = 20;
|
||||
string action_payload_json = 21;
|
||||
string dimension_filter_json = 22;
|
||||
string reward_asset_type = 23;
|
||||
}
|
||||
|
||||
message UpsertTaskDefinitionResponse {
|
||||
@ -506,6 +510,27 @@ message SetTaskDefinitionStatusResponse {
|
||||
TaskDefinition task = 1;
|
||||
}
|
||||
|
||||
// PublishTaskRewardPolicyRequest 是后台策略发布写入 activity 运行侧任务奖励默认资产的命令。
|
||||
message PublishTaskRewardPolicyRequest {
|
||||
RequestMeta meta = 1;
|
||||
string instance_code = 2;
|
||||
string template_code = 3;
|
||||
string template_version = 4;
|
||||
string status = 5;
|
||||
string reward_asset_type = 6;
|
||||
string rule_json = 7;
|
||||
int64 operator_admin_id = 8;
|
||||
int64 published_at_ms = 9;
|
||||
}
|
||||
|
||||
message PublishTaskRewardPolicyResponse {
|
||||
string app_code = 1;
|
||||
string instance_code = 2;
|
||||
string status = 3;
|
||||
string reward_asset_type = 4;
|
||||
int64 published_at_ms = 5;
|
||||
}
|
||||
|
||||
// RegistrationRewardConfig 是注册奖励的当前配置;enabled=false 时保留配置但不自动发放。
|
||||
message RegistrationRewardConfig {
|
||||
string app_code = 1;
|
||||
@ -1721,252 +1746,6 @@ message DeleteAchievementDefinitionResponse {
|
||||
AchievementDefinition achievement = 1;
|
||||
}
|
||||
|
||||
message LuckyGiftMeta {
|
||||
RequestMeta meta = 1;
|
||||
string command_id = 2;
|
||||
int64 user_id = 3;
|
||||
string device_id = 4;
|
||||
string room_id = 5;
|
||||
string anchor_id = 6;
|
||||
string gift_id = 7;
|
||||
int64 coin_spent = 8;
|
||||
int64 paid_at_ms = 9;
|
||||
string pool_id = 10;
|
||||
int64 target_user_id = 11;
|
||||
int32 gift_count = 12;
|
||||
// visible_region_id 来自 room-service 的房间可见区域,用于中奖后的区域 IM 飘屏。
|
||||
int64 visible_region_id = 13;
|
||||
// country_id 来自送礼用户当前国家,用于统计服务按真实国家聚合返奖,不用区域桶反推。
|
||||
int64 country_id = 14;
|
||||
}
|
||||
|
||||
message LuckyGiftTier {
|
||||
string pool = 1;
|
||||
string tier_id = 2;
|
||||
int64 reward_coins = 3;
|
||||
int64 weight = 4;
|
||||
bool high_water_only = 5;
|
||||
bool enabled = 6;
|
||||
int64 multiplier_ppm = 7;
|
||||
}
|
||||
|
||||
message LuckyGiftConfig {
|
||||
string app_code = 1;
|
||||
string gift_id = 2;
|
||||
bool enabled = 3;
|
||||
int64 rule_version = 4;
|
||||
int64 gift_price = 5;
|
||||
int64 target_rtp_ppm = 6;
|
||||
int64 pool_rate_ppm = 7;
|
||||
int64 global_window_draws = 8;
|
||||
int64 gift_window_draws = 9;
|
||||
int64 novice_draw_limit = 10;
|
||||
int64 intermediate_draw_limit = 11;
|
||||
int64 high_multiplier = 12;
|
||||
int64 high_water_pool_multiple = 13;
|
||||
int64 platform_pool_weight_ppm = 14;
|
||||
int64 room_pool_weight_ppm = 15;
|
||||
int64 gift_pool_weight_ppm = 16;
|
||||
int64 initial_platform_pool = 17;
|
||||
int64 initial_gift_pool = 18;
|
||||
int64 initial_room_pool = 19;
|
||||
int64 platform_reserve = 20;
|
||||
int64 gift_reserve = 21;
|
||||
int64 room_reserve = 22;
|
||||
int64 max_single_payout = 23;
|
||||
int64 user_hourly_payout_cap = 24;
|
||||
int64 user_daily_payout_cap = 25;
|
||||
int64 device_daily_payout_cap = 26;
|
||||
int64 room_hourly_payout_cap = 27;
|
||||
int64 anchor_daily_payout_cap = 28;
|
||||
int64 room_atmosphere_rate_ppm = 29;
|
||||
int64 room_atmosphere_initial = 30;
|
||||
int64 room_atmosphere_reserve = 31;
|
||||
int64 activity_budget = 32;
|
||||
int64 activity_daily_limit = 33;
|
||||
bool large_tier_enabled = 34;
|
||||
repeated LuckyGiftTier tiers = 35;
|
||||
int64 updated_by_admin_id = 36;
|
||||
int64 created_at_ms = 37;
|
||||
int64 updated_at_ms = 38;
|
||||
string pool_id = 39;
|
||||
repeated int64 multiplier_ppms = 40;
|
||||
}
|
||||
|
||||
message LuckyGiftRuleTier {
|
||||
string stage = 1;
|
||||
string tier_id = 2;
|
||||
int64 multiplier_ppm = 3;
|
||||
int64 base_weight_ppm = 4;
|
||||
string reward_source = 5;
|
||||
bool high_water_only = 6;
|
||||
string broadcast_level = 7;
|
||||
bool enabled = 8;
|
||||
}
|
||||
|
||||
message LuckyGiftRuleStage {
|
||||
string stage = 1;
|
||||
repeated LuckyGiftRuleTier tiers = 2;
|
||||
}
|
||||
|
||||
message LuckyGiftRuleConfig {
|
||||
string app_code = 1;
|
||||
string pool_id = 2;
|
||||
int64 rule_version = 3;
|
||||
bool enabled = 4;
|
||||
int64 target_rtp_ppm = 5;
|
||||
int64 pool_rate_ppm = 6;
|
||||
int64 settlement_window_wager = 7;
|
||||
int64 control_band_ppm = 8;
|
||||
int64 gift_price_reference = 9;
|
||||
int64 novice_max_equivalent_draws = 10;
|
||||
int64 normal_max_equivalent_draws = 11;
|
||||
int64 effective_from_ms = 12;
|
||||
int64 created_by_admin_id = 13;
|
||||
int64 created_at_ms = 14;
|
||||
int64 max_single_payout = 15;
|
||||
int64 user_hourly_payout_cap = 16;
|
||||
int64 user_daily_payout_cap = 17;
|
||||
int64 device_daily_payout_cap = 18;
|
||||
int64 room_hourly_payout_cap = 19;
|
||||
int64 anchor_daily_payout_cap = 20;
|
||||
repeated LuckyGiftRuleStage stages = 21;
|
||||
}
|
||||
|
||||
message CheckLuckyGiftRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
string room_id = 3;
|
||||
string gift_id = 4;
|
||||
string pool_id = 5;
|
||||
}
|
||||
|
||||
message CheckLuckyGiftResponse {
|
||||
bool enabled = 1;
|
||||
string reason = 2;
|
||||
string gift_id = 3;
|
||||
int64 gift_price = 4;
|
||||
int64 rule_version = 5;
|
||||
int64 target_rtp_ppm = 6;
|
||||
string experience_pool = 7;
|
||||
string pool_id = 8;
|
||||
}
|
||||
|
||||
message LuckyGiftDrawResult {
|
||||
string draw_id = 1;
|
||||
string command_id = 2;
|
||||
string gift_id = 3;
|
||||
int64 rule_version = 4;
|
||||
string experience_pool = 5;
|
||||
string selected_tier_id = 6;
|
||||
int64 base_reward_coins = 7;
|
||||
int64 room_atmosphere_reward_coins = 8;
|
||||
int64 activity_subsidy_coins = 9;
|
||||
int64 effective_reward_coins = 10;
|
||||
string budget_sources_json = 11;
|
||||
string reward_status = 12;
|
||||
int64 rtp_window_index = 13;
|
||||
int64 gift_rtp_window_index = 14;
|
||||
int64 global_base_rtp_ppm = 15;
|
||||
int64 gift_base_rtp_ppm = 16;
|
||||
bool stage_feedback = 17;
|
||||
bool high_multiplier = 18;
|
||||
int64 created_at_ms = 19;
|
||||
string pool_id = 20;
|
||||
int64 multiplier_ppm = 21;
|
||||
string wallet_transaction_id = 22;
|
||||
int64 coin_balance_after = 23;
|
||||
}
|
||||
|
||||
message ExecuteLuckyGiftDrawRequest {
|
||||
LuckyGiftMeta lucky_gift = 1;
|
||||
}
|
||||
|
||||
message ExecuteLuckyGiftDrawResponse {
|
||||
LuckyGiftDrawResult result = 1;
|
||||
}
|
||||
|
||||
message BatchExecuteLuckyGiftDrawRequest {
|
||||
repeated LuckyGiftMeta lucky_gifts = 1;
|
||||
}
|
||||
|
||||
message BatchExecuteLuckyGiftDrawResponse {
|
||||
repeated LuckyGiftDrawResult results = 1;
|
||||
}
|
||||
|
||||
message GetLuckyGiftConfigRequest {
|
||||
RequestMeta meta = 1;
|
||||
string gift_id = 2;
|
||||
string pool_id = 3;
|
||||
}
|
||||
|
||||
message GetLuckyGiftConfigResponse {
|
||||
LuckyGiftRuleConfig config = 1;
|
||||
}
|
||||
|
||||
message UpsertLuckyGiftConfigRequest {
|
||||
RequestMeta meta = 1;
|
||||
LuckyGiftRuleConfig config = 2;
|
||||
int64 operator_admin_id = 3;
|
||||
}
|
||||
|
||||
message UpsertLuckyGiftConfigResponse {
|
||||
LuckyGiftRuleConfig config = 1;
|
||||
}
|
||||
|
||||
message ListLuckyGiftConfigsRequest {
|
||||
RequestMeta meta = 1;
|
||||
}
|
||||
|
||||
message ListLuckyGiftConfigsResponse {
|
||||
repeated LuckyGiftRuleConfig configs = 1;
|
||||
}
|
||||
|
||||
message ListLuckyGiftDrawsRequest {
|
||||
RequestMeta meta = 1;
|
||||
string gift_id = 2;
|
||||
int64 user_id = 3;
|
||||
string room_id = 4;
|
||||
string status = 5;
|
||||
int32 page = 6;
|
||||
int32 page_size = 7;
|
||||
string pool_id = 8;
|
||||
}
|
||||
|
||||
message ListLuckyGiftDrawsResponse {
|
||||
repeated LuckyGiftDrawResult draws = 1;
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
message LuckyGiftDrawSummary {
|
||||
string pool_id = 1;
|
||||
int64 total_draws = 2;
|
||||
int64 unique_users = 3;
|
||||
int64 unique_rooms = 4;
|
||||
int64 total_spent_coins = 5;
|
||||
int64 total_reward_coins = 6;
|
||||
int64 base_reward_coins = 7;
|
||||
int64 room_atmosphere_reward_coins = 8;
|
||||
int64 activity_subsidy_coins = 9;
|
||||
int64 actual_rtp_ppm = 10;
|
||||
int64 pending_draws = 11;
|
||||
int64 granted_draws = 12;
|
||||
int64 failed_draws = 13;
|
||||
}
|
||||
|
||||
message GetLuckyGiftDrawSummaryRequest {
|
||||
RequestMeta meta = 1;
|
||||
string gift_id = 2;
|
||||
int64 user_id = 3;
|
||||
string room_id = 4;
|
||||
string status = 5;
|
||||
string pool_id = 6;
|
||||
}
|
||||
|
||||
message GetLuckyGiftDrawSummaryResponse {
|
||||
LuckyGiftDrawSummary summary = 1;
|
||||
}
|
||||
|
||||
message WheelPrizeTier {
|
||||
string tier_id = 1;
|
||||
string display_name = 2;
|
||||
@ -2589,13 +2368,6 @@ service AchievementService {
|
||||
rpc SetBadgeDisplay(SetBadgeDisplayRequest) returns (SetBadgeDisplayResponse);
|
||||
}
|
||||
|
||||
// LuckyGiftService owns lucky gift rule checks and draw decisions after wallet debit succeeds.
|
||||
service LuckyGiftService {
|
||||
rpc CheckLuckyGift(CheckLuckyGiftRequest) returns (CheckLuckyGiftResponse);
|
||||
rpc ExecuteLuckyGiftDraw(ExecuteLuckyGiftDrawRequest) returns (ExecuteLuckyGiftDrawResponse);
|
||||
rpc BatchExecuteLuckyGiftDraw(BatchExecuteLuckyGiftDrawRequest) returns (BatchExecuteLuckyGiftDrawResponse);
|
||||
}
|
||||
|
||||
// WheelService owns wheel draw decisions after wallet debit succeeds.
|
||||
service WheelService {
|
||||
rpc ExecuteWheelDraw(ExecuteWheelDrawRequest) returns (ExecuteWheelDrawResponse);
|
||||
@ -2644,6 +2416,7 @@ service AdminTaskService {
|
||||
rpc ListTaskDefinitions(ListTaskDefinitionsRequest) returns (ListTaskDefinitionsResponse);
|
||||
rpc UpsertTaskDefinition(UpsertTaskDefinitionRequest) returns (UpsertTaskDefinitionResponse);
|
||||
rpc SetTaskDefinitionStatus(SetTaskDefinitionStatusRequest) returns (SetTaskDefinitionStatusResponse);
|
||||
rpc PublishTaskRewardPolicy(PublishTaskRewardPolicyRequest) returns (PublishTaskRewardPolicyResponse);
|
||||
}
|
||||
|
||||
// RegistrationRewardService 拥有 App 查询和注册完成发放入口。
|
||||
@ -2765,15 +2538,6 @@ service AdminAchievementService {
|
||||
rpc DeleteAchievementDefinition(DeleteAchievementDefinitionRequest) returns (DeleteAchievementDefinitionResponse);
|
||||
}
|
||||
|
||||
// AdminLuckyGiftService is the admin entry for lucky gift rule versions and draw audit.
|
||||
service AdminLuckyGiftService {
|
||||
rpc GetLuckyGiftConfig(GetLuckyGiftConfigRequest) returns (GetLuckyGiftConfigResponse);
|
||||
rpc UpsertLuckyGiftConfig(UpsertLuckyGiftConfigRequest) returns (UpsertLuckyGiftConfigResponse);
|
||||
rpc ListLuckyGiftConfigs(ListLuckyGiftConfigsRequest) returns (ListLuckyGiftConfigsResponse);
|
||||
rpc ListLuckyGiftDraws(ListLuckyGiftDrawsRequest) returns (ListLuckyGiftDrawsResponse);
|
||||
rpc GetLuckyGiftDrawSummary(GetLuckyGiftDrawSummaryRequest) returns (GetLuckyGiftDrawSummaryResponse);
|
||||
}
|
||||
|
||||
// AdminWheelService is the admin entry for wheel rule versions, draw records, and RTP statistics.
|
||||
service AdminWheelService {
|
||||
rpc GetWheelConfig(GetWheelConfigRequest) returns (GetWheelConfigResponse);
|
||||
|
||||
@ -1787,188 +1787,6 @@ var AchievementService_ServiceDesc = grpc.ServiceDesc{
|
||||
Metadata: "proto/activity/v1/activity.proto",
|
||||
}
|
||||
|
||||
const (
|
||||
LuckyGiftService_CheckLuckyGift_FullMethodName = "/hyapp.activity.v1.LuckyGiftService/CheckLuckyGift"
|
||||
LuckyGiftService_ExecuteLuckyGiftDraw_FullMethodName = "/hyapp.activity.v1.LuckyGiftService/ExecuteLuckyGiftDraw"
|
||||
LuckyGiftService_BatchExecuteLuckyGiftDraw_FullMethodName = "/hyapp.activity.v1.LuckyGiftService/BatchExecuteLuckyGiftDraw"
|
||||
)
|
||||
|
||||
// LuckyGiftServiceClient is the client API for LuckyGiftService 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.
|
||||
//
|
||||
// LuckyGiftService owns lucky gift rule checks and draw decisions after wallet debit succeeds.
|
||||
type LuckyGiftServiceClient interface {
|
||||
CheckLuckyGift(ctx context.Context, in *CheckLuckyGiftRequest, opts ...grpc.CallOption) (*CheckLuckyGiftResponse, error)
|
||||
ExecuteLuckyGiftDraw(ctx context.Context, in *ExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*ExecuteLuckyGiftDrawResponse, error)
|
||||
BatchExecuteLuckyGiftDraw(ctx context.Context, in *BatchExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*BatchExecuteLuckyGiftDrawResponse, error)
|
||||
}
|
||||
|
||||
type luckyGiftServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewLuckyGiftServiceClient(cc grpc.ClientConnInterface) LuckyGiftServiceClient {
|
||||
return &luckyGiftServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *luckyGiftServiceClient) CheckLuckyGift(ctx context.Context, in *CheckLuckyGiftRequest, opts ...grpc.CallOption) (*CheckLuckyGiftResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CheckLuckyGiftResponse)
|
||||
err := c.cc.Invoke(ctx, LuckyGiftService_CheckLuckyGift_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *luckyGiftServiceClient) ExecuteLuckyGiftDraw(ctx context.Context, in *ExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*ExecuteLuckyGiftDrawResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ExecuteLuckyGiftDrawResponse)
|
||||
err := c.cc.Invoke(ctx, LuckyGiftService_ExecuteLuckyGiftDraw_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *luckyGiftServiceClient) BatchExecuteLuckyGiftDraw(ctx context.Context, in *BatchExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*BatchExecuteLuckyGiftDrawResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(BatchExecuteLuckyGiftDrawResponse)
|
||||
err := c.cc.Invoke(ctx, LuckyGiftService_BatchExecuteLuckyGiftDraw_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// LuckyGiftServiceServer is the server API for LuckyGiftService service.
|
||||
// All implementations must embed UnimplementedLuckyGiftServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// LuckyGiftService owns lucky gift rule checks and draw decisions after wallet debit succeeds.
|
||||
type LuckyGiftServiceServer interface {
|
||||
CheckLuckyGift(context.Context, *CheckLuckyGiftRequest) (*CheckLuckyGiftResponse, error)
|
||||
ExecuteLuckyGiftDraw(context.Context, *ExecuteLuckyGiftDrawRequest) (*ExecuteLuckyGiftDrawResponse, error)
|
||||
BatchExecuteLuckyGiftDraw(context.Context, *BatchExecuteLuckyGiftDrawRequest) (*BatchExecuteLuckyGiftDrawResponse, error)
|
||||
mustEmbedUnimplementedLuckyGiftServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedLuckyGiftServiceServer 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 UnimplementedLuckyGiftServiceServer struct{}
|
||||
|
||||
func (UnimplementedLuckyGiftServiceServer) CheckLuckyGift(context.Context, *CheckLuckyGiftRequest) (*CheckLuckyGiftResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckLuckyGift not implemented")
|
||||
}
|
||||
func (UnimplementedLuckyGiftServiceServer) ExecuteLuckyGiftDraw(context.Context, *ExecuteLuckyGiftDrawRequest) (*ExecuteLuckyGiftDrawResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExecuteLuckyGiftDraw not implemented")
|
||||
}
|
||||
func (UnimplementedLuckyGiftServiceServer) BatchExecuteLuckyGiftDraw(context.Context, *BatchExecuteLuckyGiftDrawRequest) (*BatchExecuteLuckyGiftDrawResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchExecuteLuckyGiftDraw not implemented")
|
||||
}
|
||||
func (UnimplementedLuckyGiftServiceServer) mustEmbedUnimplementedLuckyGiftServiceServer() {}
|
||||
func (UnimplementedLuckyGiftServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeLuckyGiftServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to LuckyGiftServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeLuckyGiftServiceServer interface {
|
||||
mustEmbedUnimplementedLuckyGiftServiceServer()
|
||||
}
|
||||
|
||||
func RegisterLuckyGiftServiceServer(s grpc.ServiceRegistrar, srv LuckyGiftServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedLuckyGiftServiceServer 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(&LuckyGiftService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _LuckyGiftService_CheckLuckyGift_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CheckLuckyGiftRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LuckyGiftServiceServer).CheckLuckyGift(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: LuckyGiftService_CheckLuckyGift_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LuckyGiftServiceServer).CheckLuckyGift(ctx, req.(*CheckLuckyGiftRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _LuckyGiftService_ExecuteLuckyGiftDraw_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ExecuteLuckyGiftDrawRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LuckyGiftServiceServer).ExecuteLuckyGiftDraw(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: LuckyGiftService_ExecuteLuckyGiftDraw_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LuckyGiftServiceServer).ExecuteLuckyGiftDraw(ctx, req.(*ExecuteLuckyGiftDrawRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _LuckyGiftService_BatchExecuteLuckyGiftDraw_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BatchExecuteLuckyGiftDrawRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LuckyGiftServiceServer).BatchExecuteLuckyGiftDraw(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: LuckyGiftService_BatchExecuteLuckyGiftDraw_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LuckyGiftServiceServer).BatchExecuteLuckyGiftDraw(ctx, req.(*BatchExecuteLuckyGiftDrawRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// LuckyGiftService_ServiceDesc is the grpc.ServiceDesc for LuckyGiftService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var LuckyGiftService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "hyapp.activity.v1.LuckyGiftService",
|
||||
HandlerType: (*LuckyGiftServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "CheckLuckyGift",
|
||||
Handler: _LuckyGiftService_CheckLuckyGift_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ExecuteLuckyGiftDraw",
|
||||
Handler: _LuckyGiftService_ExecuteLuckyGiftDraw_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BatchExecuteLuckyGiftDraw",
|
||||
Handler: _LuckyGiftService_BatchExecuteLuckyGiftDraw_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/activity/v1/activity.proto",
|
||||
}
|
||||
|
||||
const (
|
||||
WheelService_ExecuteWheelDraw_FullMethodName = "/hyapp.activity.v1.WheelService/ExecuteWheelDraw"
|
||||
)
|
||||
@ -3021,6 +2839,7 @@ 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"
|
||||
AdminTaskService_PublishTaskRewardPolicy_FullMethodName = "/hyapp.activity.v1.AdminTaskService/PublishTaskRewardPolicy"
|
||||
)
|
||||
|
||||
// AdminTaskServiceClient is the client API for AdminTaskService service.
|
||||
@ -3032,6 +2851,7 @@ 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)
|
||||
PublishTaskRewardPolicy(ctx context.Context, in *PublishTaskRewardPolicyRequest, opts ...grpc.CallOption) (*PublishTaskRewardPolicyResponse, error)
|
||||
}
|
||||
|
||||
type adminTaskServiceClient struct {
|
||||
@ -3072,6 +2892,16 @@ func (c *adminTaskServiceClient) SetTaskDefinitionStatus(ctx context.Context, in
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminTaskServiceClient) PublishTaskRewardPolicy(ctx context.Context, in *PublishTaskRewardPolicyRequest, opts ...grpc.CallOption) (*PublishTaskRewardPolicyResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PublishTaskRewardPolicyResponse)
|
||||
err := c.cc.Invoke(ctx, AdminTaskService_PublishTaskRewardPolicy_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.
|
||||
@ -3081,6 +2911,7 @@ type AdminTaskServiceServer interface {
|
||||
ListTaskDefinitions(context.Context, *ListTaskDefinitionsRequest) (*ListTaskDefinitionsResponse, error)
|
||||
UpsertTaskDefinition(context.Context, *UpsertTaskDefinitionRequest) (*UpsertTaskDefinitionResponse, error)
|
||||
SetTaskDefinitionStatus(context.Context, *SetTaskDefinitionStatusRequest) (*SetTaskDefinitionStatusResponse, error)
|
||||
PublishTaskRewardPolicy(context.Context, *PublishTaskRewardPolicyRequest) (*PublishTaskRewardPolicyResponse, error)
|
||||
mustEmbedUnimplementedAdminTaskServiceServer()
|
||||
}
|
||||
|
||||
@ -3100,6 +2931,9 @@ func (UnimplementedAdminTaskServiceServer) UpsertTaskDefinition(context.Context,
|
||||
func (UnimplementedAdminTaskServiceServer) SetTaskDefinitionStatus(context.Context, *SetTaskDefinitionStatusRequest) (*SetTaskDefinitionStatusResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetTaskDefinitionStatus not implemented")
|
||||
}
|
||||
func (UnimplementedAdminTaskServiceServer) PublishTaskRewardPolicy(context.Context, *PublishTaskRewardPolicyRequest) (*PublishTaskRewardPolicyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PublishTaskRewardPolicy not implemented")
|
||||
}
|
||||
func (UnimplementedAdminTaskServiceServer) mustEmbedUnimplementedAdminTaskServiceServer() {}
|
||||
func (UnimplementedAdminTaskServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@ -3175,6 +3009,24 @@ func _AdminTaskService_SetTaskDefinitionStatus_Handler(srv interface{}, ctx cont
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminTaskService_PublishTaskRewardPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PublishTaskRewardPolicyRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminTaskServiceServer).PublishTaskRewardPolicy(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminTaskService_PublishTaskRewardPolicy_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminTaskServiceServer).PublishTaskRewardPolicy(ctx, req.(*PublishTaskRewardPolicyRequest))
|
||||
}
|
||||
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)
|
||||
@ -3194,6 +3046,10 @@ var AdminTaskService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "SetTaskDefinitionStatus",
|
||||
Handler: _AdminTaskService_SetTaskDefinitionStatus_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "PublishTaskRewardPolicy",
|
||||
Handler: _AdminTaskService_PublishTaskRewardPolicy_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/activity/v1/activity.proto",
|
||||
@ -6393,264 +6249,6 @@ var AdminAchievementService_ServiceDesc = grpc.ServiceDesc{
|
||||
Metadata: "proto/activity/v1/activity.proto",
|
||||
}
|
||||
|
||||
const (
|
||||
AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/GetLuckyGiftConfig"
|
||||
AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/UpsertLuckyGiftConfig"
|
||||
AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/ListLuckyGiftConfigs"
|
||||
AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/ListLuckyGiftDraws"
|
||||
AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/GetLuckyGiftDrawSummary"
|
||||
)
|
||||
|
||||
// AdminLuckyGiftServiceClient is the client API for AdminLuckyGiftService 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.
|
||||
//
|
||||
// AdminLuckyGiftService is the admin entry for lucky gift rule versions and draw audit.
|
||||
type AdminLuckyGiftServiceClient interface {
|
||||
GetLuckyGiftConfig(ctx context.Context, in *GetLuckyGiftConfigRequest, opts ...grpc.CallOption) (*GetLuckyGiftConfigResponse, error)
|
||||
UpsertLuckyGiftConfig(ctx context.Context, in *UpsertLuckyGiftConfigRequest, opts ...grpc.CallOption) (*UpsertLuckyGiftConfigResponse, error)
|
||||
ListLuckyGiftConfigs(ctx context.Context, in *ListLuckyGiftConfigsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigsResponse, error)
|
||||
ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error)
|
||||
GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error)
|
||||
}
|
||||
|
||||
type adminLuckyGiftServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewAdminLuckyGiftServiceClient(cc grpc.ClientConnInterface) AdminLuckyGiftServiceClient {
|
||||
return &adminLuckyGiftServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) GetLuckyGiftConfig(ctx context.Context, in *GetLuckyGiftConfigRequest, opts ...grpc.CallOption) (*GetLuckyGiftConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetLuckyGiftConfigResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) UpsertLuckyGiftConfig(ctx context.Context, in *UpsertLuckyGiftConfigRequest, opts ...grpc.CallOption) (*UpsertLuckyGiftConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(UpsertLuckyGiftConfigResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) ListLuckyGiftConfigs(ctx context.Context, in *ListLuckyGiftConfigsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListLuckyGiftConfigsResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListLuckyGiftDrawsResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetLuckyGiftDrawSummaryResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AdminLuckyGiftServiceServer is the server API for AdminLuckyGiftService service.
|
||||
// All implementations must embed UnimplementedAdminLuckyGiftServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// AdminLuckyGiftService is the admin entry for lucky gift rule versions and draw audit.
|
||||
type AdminLuckyGiftServiceServer interface {
|
||||
GetLuckyGiftConfig(context.Context, *GetLuckyGiftConfigRequest) (*GetLuckyGiftConfigResponse, error)
|
||||
UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error)
|
||||
ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error)
|
||||
ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error)
|
||||
GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error)
|
||||
mustEmbedUnimplementedAdminLuckyGiftServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedAdminLuckyGiftServiceServer 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 UnimplementedAdminLuckyGiftServiceServer struct{}
|
||||
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftConfig(context.Context, *GetLuckyGiftConfigRequest) (*GetLuckyGiftConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetLuckyGiftConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpsertLuckyGiftConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListLuckyGiftConfigs not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListLuckyGiftDraws not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetLuckyGiftDrawSummary not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) mustEmbedUnimplementedAdminLuckyGiftServiceServer() {}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeAdminLuckyGiftServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to AdminLuckyGiftServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeAdminLuckyGiftServiceServer interface {
|
||||
mustEmbedUnimplementedAdminLuckyGiftServiceServer()
|
||||
}
|
||||
|
||||
func RegisterAdminLuckyGiftServiceServer(s grpc.ServiceRegistrar, srv AdminLuckyGiftServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAdminLuckyGiftServiceServer 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(&AdminLuckyGiftService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_GetLuckyGiftConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetLuckyGiftConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftConfig(ctx, req.(*GetLuckyGiftConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_UpsertLuckyGiftConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UpsertLuckyGiftConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).UpsertLuckyGiftConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).UpsertLuckyGiftConfig(ctx, req.(*UpsertLuckyGiftConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_ListLuckyGiftConfigs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListLuckyGiftConfigsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftConfigs(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftConfigs(ctx, req.(*ListLuckyGiftConfigsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_ListLuckyGiftDraws_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListLuckyGiftDrawsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftDraws(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftDraws(ctx, req.(*ListLuckyGiftDrawsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_GetLuckyGiftDrawSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetLuckyGiftDrawSummaryRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftDrawSummary(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftDrawSummary(ctx, req.(*GetLuckyGiftDrawSummaryRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AdminLuckyGiftService_ServiceDesc is the grpc.ServiceDesc for AdminLuckyGiftService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var AdminLuckyGiftService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "hyapp.activity.v1.AdminLuckyGiftService",
|
||||
HandlerType: (*AdminLuckyGiftServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetLuckyGiftConfig",
|
||||
Handler: _AdminLuckyGiftService_GetLuckyGiftConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpsertLuckyGiftConfig",
|
||||
Handler: _AdminLuckyGiftService_UpsertLuckyGiftConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListLuckyGiftConfigs",
|
||||
Handler: _AdminLuckyGiftService_ListLuckyGiftConfigs_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListLuckyGiftDraws",
|
||||
Handler: _AdminLuckyGiftService_ListLuckyGiftDraws_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetLuckyGiftDrawSummary",
|
||||
Handler: _AdminLuckyGiftService_GetLuckyGiftDrawSummary_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/activity/v1/activity.proto",
|
||||
}
|
||||
|
||||
const (
|
||||
AdminWheelService_GetWheelConfig_FullMethodName = "/hyapp.activity.v1.AdminWheelService/GetWheelConfig"
|
||||
AdminWheelService_UpsertWheelConfig_FullMethodName = "/hyapp.activity.v1.AdminWheelService/UpsertWheelConfig"
|
||||
|
||||
@ -1576,24 +1576,22 @@ type RoomGiftBatchLuckyResult struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
||||
DrawId string `protobuf:"bytes,2,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
|
||||
CommandId string `protobuf:"bytes,3,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
|
||||
PoolId string `protobuf:"bytes,4,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
||||
GiftId string `protobuf:"bytes,5,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
|
||||
RuleVersion int64 `protobuf:"varint,6,opt,name=rule_version,json=ruleVersion,proto3" json:"rule_version,omitempty"`
|
||||
ExperiencePool string `protobuf:"bytes,7,opt,name=experience_pool,json=experiencePool,proto3" json:"experience_pool,omitempty"`
|
||||
SelectedTierId string `protobuf:"bytes,8,opt,name=selected_tier_id,json=selectedTierId,proto3" json:"selected_tier_id,omitempty"`
|
||||
MultiplierPpm int64 `protobuf:"varint,9,opt,name=multiplier_ppm,json=multiplierPpm,proto3" json:"multiplier_ppm,omitempty"`
|
||||
BaseRewardCoins int64 `protobuf:"varint,10,opt,name=base_reward_coins,json=baseRewardCoins,proto3" json:"base_reward_coins,omitempty"`
|
||||
RoomAtmosphereRewardCoins int64 `protobuf:"varint,11,opt,name=room_atmosphere_reward_coins,json=roomAtmosphereRewardCoins,proto3" json:"room_atmosphere_reward_coins,omitempty"`
|
||||
ActivitySubsidyCoins int64 `protobuf:"varint,12,opt,name=activity_subsidy_coins,json=activitySubsidyCoins,proto3" json:"activity_subsidy_coins,omitempty"`
|
||||
EffectiveRewardCoins int64 `protobuf:"varint,13,opt,name=effective_reward_coins,json=effectiveRewardCoins,proto3" json:"effective_reward_coins,omitempty"`
|
||||
RewardStatus string `protobuf:"bytes,14,opt,name=reward_status,json=rewardStatus,proto3" json:"reward_status,omitempty"`
|
||||
StageFeedback bool `protobuf:"varint,15,opt,name=stage_feedback,json=stageFeedback,proto3" json:"stage_feedback,omitempty"`
|
||||
HighMultiplier bool `protobuf:"varint,16,opt,name=high_multiplier,json=highMultiplier,proto3" json:"high_multiplier,omitempty"`
|
||||
CreatedAtMs int64 `protobuf:"varint,17,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||
TargetUserId int64 `protobuf:"varint,18,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
||||
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
||||
DrawId string `protobuf:"bytes,2,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
|
||||
CommandId string `protobuf:"bytes,3,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
|
||||
PoolId string `protobuf:"bytes,4,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
||||
GiftId string `protobuf:"bytes,5,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
|
||||
RuleVersion int64 `protobuf:"varint,6,opt,name=rule_version,json=ruleVersion,proto3" json:"rule_version,omitempty"`
|
||||
ExperiencePool string `protobuf:"bytes,7,opt,name=experience_pool,json=experiencePool,proto3" json:"experience_pool,omitempty"`
|
||||
SelectedTierId string `protobuf:"bytes,8,opt,name=selected_tier_id,json=selectedTierId,proto3" json:"selected_tier_id,omitempty"`
|
||||
MultiplierPpm int64 `protobuf:"varint,9,opt,name=multiplier_ppm,json=multiplierPpm,proto3" json:"multiplier_ppm,omitempty"`
|
||||
BaseRewardCoins int64 `protobuf:"varint,10,opt,name=base_reward_coins,json=baseRewardCoins,proto3" json:"base_reward_coins,omitempty"`
|
||||
EffectiveRewardCoins int64 `protobuf:"varint,11,opt,name=effective_reward_coins,json=effectiveRewardCoins,proto3" json:"effective_reward_coins,omitempty"`
|
||||
RewardStatus string `protobuf:"bytes,12,opt,name=reward_status,json=rewardStatus,proto3" json:"reward_status,omitempty"`
|
||||
StageFeedback bool `protobuf:"varint,13,opt,name=stage_feedback,json=stageFeedback,proto3" json:"stage_feedback,omitempty"`
|
||||
HighMultiplier bool `protobuf:"varint,14,opt,name=high_multiplier,json=highMultiplier,proto3" json:"high_multiplier,omitempty"`
|
||||
CreatedAtMs int64 `protobuf:"varint,15,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||
TargetUserId int64 `protobuf:"varint,16,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
||||
}
|
||||
|
||||
func (x *RoomGiftBatchLuckyResult) Reset() {
|
||||
@ -1696,20 +1694,6 @@ func (x *RoomGiftBatchLuckyResult) GetBaseRewardCoins() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomGiftBatchLuckyResult) GetRoomAtmosphereRewardCoins() int64 {
|
||||
if x != nil {
|
||||
return x.RoomAtmosphereRewardCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomGiftBatchLuckyResult) GetActivitySubsidyCoins() int64 {
|
||||
if x != nil {
|
||||
return x.ActivitySubsidyCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomGiftBatchLuckyResult) GetEffectiveRewardCoins() int64 {
|
||||
if x != nil {
|
||||
return x.EffectiveRewardCoins
|
||||
@ -2101,22 +2085,20 @@ type RoomRobotLuckyGiftDrawn struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
DrawId string `protobuf:"bytes,1,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
|
||||
CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
|
||||
SenderUserId int64 `protobuf:"varint,3,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"`
|
||||
TargetUserId int64 `protobuf:"varint,4,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
||||
GiftId string `protobuf:"bytes,5,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
|
||||
GiftCount int32 `protobuf:"varint,6,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
|
||||
PoolId string `protobuf:"bytes,7,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
||||
MultiplierPpm int64 `protobuf:"varint,8,opt,name=multiplier_ppm,json=multiplierPpm,proto3" json:"multiplier_ppm,omitempty"`
|
||||
BaseRewardCoins int64 `protobuf:"varint,9,opt,name=base_reward_coins,json=baseRewardCoins,proto3" json:"base_reward_coins,omitempty"`
|
||||
RoomAtmosphereRewardCoins int64 `protobuf:"varint,10,opt,name=room_atmosphere_reward_coins,json=roomAtmosphereRewardCoins,proto3" json:"room_atmosphere_reward_coins,omitempty"`
|
||||
ActivitySubsidyCoins int64 `protobuf:"varint,11,opt,name=activity_subsidy_coins,json=activitySubsidyCoins,proto3" json:"activity_subsidy_coins,omitempty"`
|
||||
EffectiveRewardCoins int64 `protobuf:"varint,12,opt,name=effective_reward_coins,json=effectiveRewardCoins,proto3" json:"effective_reward_coins,omitempty"`
|
||||
CreatedAtMs int64 `protobuf:"varint,13,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||
VisibleRegionId int64 `protobuf:"varint,14,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
|
||||
IsRobot bool `protobuf:"varint,15,opt,name=is_robot,json=isRobot,proto3" json:"is_robot,omitempty"`
|
||||
Synthetic bool `protobuf:"varint,16,opt,name=synthetic,proto3" json:"synthetic,omitempty"`
|
||||
DrawId string `protobuf:"bytes,1,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
|
||||
CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
|
||||
SenderUserId int64 `protobuf:"varint,3,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"`
|
||||
TargetUserId int64 `protobuf:"varint,4,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
||||
GiftId string `protobuf:"bytes,5,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
|
||||
GiftCount int32 `protobuf:"varint,6,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
|
||||
PoolId string `protobuf:"bytes,7,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
||||
MultiplierPpm int64 `protobuf:"varint,8,opt,name=multiplier_ppm,json=multiplierPpm,proto3" json:"multiplier_ppm,omitempty"`
|
||||
BaseRewardCoins int64 `protobuf:"varint,9,opt,name=base_reward_coins,json=baseRewardCoins,proto3" json:"base_reward_coins,omitempty"`
|
||||
EffectiveRewardCoins int64 `protobuf:"varint,10,opt,name=effective_reward_coins,json=effectiveRewardCoins,proto3" json:"effective_reward_coins,omitempty"`
|
||||
CreatedAtMs int64 `protobuf:"varint,11,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||
VisibleRegionId int64 `protobuf:"varint,12,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
|
||||
IsRobot bool `protobuf:"varint,13,opt,name=is_robot,json=isRobot,proto3" json:"is_robot,omitempty"`
|
||||
Synthetic bool `protobuf:"varint,14,opt,name=synthetic,proto3" json:"synthetic,omitempty"`
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) Reset() {
|
||||
@ -2212,20 +2194,6 @@ func (x *RoomRobotLuckyGiftDrawn) GetBaseRewardCoins() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetRoomAtmosphereRewardCoins() int64 {
|
||||
if x != nil {
|
||||
return x.RoomAtmosphereRewardCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetActivitySubsidyCoins() int64 {
|
||||
if x != nil {
|
||||
return x.ActivitySubsidyCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetEffectiveRewardCoins() int64 {
|
||||
if x != nil {
|
||||
return x.EffectiveRewardCoins
|
||||
@ -3229,7 +3197,7 @@ var file_proto_events_room_v1_events_proto_rawDesc = []byte{
|
||||
0x69, 0x76, 0x65, 0x72, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61,
|
||||
0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c,
|
||||
0x61, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64,
|
||||
0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0xd3, 0x05, 0x0a, 0x18, 0x52,
|
||||
0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x22, 0xdc, 0x04, 0x0a, 0x18, 0x52,
|
||||
0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x75, 0x63, 0x6b,
|
||||
0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65,
|
||||
@ -3252,287 +3220,272 @@ var file_proto_events_room_v1_events_proto_rawDesc = []byte{
|
||||
0x70, 0x6c, 0x69, 0x65, 0x72, 0x50, 0x70, 0x6d, 0x12, 0x2a, 0x0a, 0x11, 0x62, 0x61, 0x73, 0x65,
|
||||
0x5f, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0a, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x0f, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43,
|
||||
0x6f, 0x69, 0x6e, 0x73, 0x12, 0x3f, 0x0a, 0x1c, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x61, 0x74, 0x6d,
|
||||
0x6f, 0x73, 0x70, 0x68, 0x65, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63,
|
||||
0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x19, 0x72, 0x6f, 0x6f, 0x6d,
|
||||
0x41, 0x74, 0x6d, 0x6f, 0x73, 0x70, 0x68, 0x65, 0x72, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64,
|
||||
0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74,
|
||||
0x79, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x69, 0x64, 0x79, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18,
|
||||
0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x53,
|
||||
0x75, 0x62, 0x73, 0x69, 0x64, 0x79, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x65,
|
||||
0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f,
|
||||
0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x65, 0x66, 0x66,
|
||||
0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e,
|
||||
0x73, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74,
|
||||
0x75, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64,
|
||||
0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x67, 0x65, 0x5f,
|
||||
0x66, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d,
|
||||
0x73, 0x74, 0x61, 0x67, 0x65, 0x46, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x27, 0x0a,
|
||||
0x0f, 0x68, 0x69, 0x67, 0x68, 0x5f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72,
|
||||
0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x68, 0x69, 0x67, 0x68, 0x4d, 0x75, 0x6c, 0x74,
|
||||
0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 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, 0x24, 0x0a, 0x0e, 0x74, 0x61,
|
||||
0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x12, 0x20, 0x01,
|
||||
0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64,
|
||||
0x22, 0x96, 0x04, 0x0a, 0x13, 0x52, 0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x42, 0x61, 0x74,
|
||||
0x63, 0x68, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
|
||||
0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2b,
|
||||
0x0a, 0x11, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x69, 0x63, 0x6b, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x72, 0x65, 0x63, 0x65, 0x69,
|
||||
0x76, 0x65, 0x72, 0x4e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x72,
|
||||
0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x41, 0x76,
|
||||
0x61, 0x74, 0x61, 0x72, 0x12, 0x37, 0x0a, 0x18, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72,
|
||||
0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64,
|
||||
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72,
|
||||
0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x44, 0x0a,
|
||||
0x1f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79,
|
||||
0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64,
|
||||
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1b, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72,
|
||||
0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65,
|
||||
0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x67, 0x69, 0x66,
|
||||
0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x74,
|
||||
0x61, 0x72, 0x67, 0x65, 0x74, 0x47, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d,
|
||||
0x0a, 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01,
|
||||
0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x69, 0x6e, 0x53, 0x70, 0x65, 0x6e, 0x74, 0x12, 0x2c, 0x0a,
|
||||
0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74,
|
||||
0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x62, 0x69, 0x6c, 0x6c, 0x69,
|
||||
0x6e, 0x67, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63,
|
||||
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x4d, 0x0a, 0x0a, 0x6c, 0x75,
|
||||
0x63, 0x6b, 0x79, 0x5f, 0x67, 0x69, 0x66, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e,
|
||||
0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f,
|
||||
0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x42, 0x61,
|
||||
0x74, 0x63, 0x68, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x09,
|
||||
0x6c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x22, 0x89, 0x07, 0x0a, 0x11, 0x52, 0x6f,
|
||||
0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x53, 0x65, 0x6e, 0x74, 0x12,
|
||||
0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x55,
|
||||
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f,
|
||||
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x6e, 0x64,
|
||||
0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72,
|
||||
0x5f, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73,
|
||||
0x65, 0x6e, 0x64, 0x65, 0x72, 0x41, 0x76, 0x61, 0x74, 0x61, 0x72, 0x12, 0x33, 0x0a, 0x16, 0x73,
|
||||
0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73,
|
||||
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x73, 0x65, 0x6e,
|
||||
0x64, 0x65, 0x72, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64,
|
||||
0x12, 0x40, 0x0a, 0x1d, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x72, 0x65, 0x74, 0x74,
|
||||
0x79, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x50,
|
||||
0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72,
|
||||
0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67,
|
||||
0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52,
|
||||
0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f,
|
||||
0x74, 0x61, 0x6c, 0x5f, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x08,
|
||||
0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74, 0x56,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f,
|
||||
0x69, 0x6e, 0x5f, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e,
|
||||
0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x69, 0x6e, 0x53, 0x70, 0x65, 0x6e, 0x74, 0x12, 0x2c,
|
||||
0x0a, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70,
|
||||
0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x62, 0x69, 0x6c, 0x6c,
|
||||
0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09,
|
||||
0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x68, 0x65, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x08, 0x72, 0x6f, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f,
|
||||
0x6c, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c,
|
||||
0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f,
|
||||
0x63, 0x6f, 0x64, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x67, 0x69, 0x66, 0x74,
|
||||
0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x63, 0x70, 0x5f, 0x72,
|
||||
0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0e, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x0e, 0x63, 0x70, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79,
|
||||
0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18,
|
||||
0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x67, 0x69, 0x66, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12,
|
||||
0x22, 0x0a, 0x0d, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c,
|
||||
0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x67, 0x69, 0x66, 0x74, 0x49, 0x63, 0x6f, 0x6e,
|
||||
0x55, 0x72, 0x6c, 0x12, 0x2c, 0x0a, 0x12, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x61, 0x6e, 0x69, 0x6d,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x10, 0x67, 0x69, 0x66, 0x74, 0x41, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72,
|
||||
0x6c, 0x12, 0x2a, 0x0a, 0x11, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74,
|
||||
0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x12, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x67, 0x69,
|
||||
0x66, 0x74, 0x45, 0x66, 0x66, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x26, 0x0a,
|
||||
0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73,
|
||||
0x18, 0x13, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73,
|
||||
0x65, 0x72, 0x49, 0x64, 0x73, 0x12, 0x43, 0x0a, 0x07, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73,
|
||||
0x18, 0x14, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x65,
|
||||
0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f,
|
||||
0x6f, 0x69, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76,
|
||||
0x65, 0x5f, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0b,
|
||||
0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52,
|
||||
0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65,
|
||||
0x77, 0x61, 0x72, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0c, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
|
||||
0x25, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x67, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x64, 0x62, 0x61, 0x63,
|
||||
0x6b, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x73, 0x74, 0x61, 0x67, 0x65, 0x46, 0x65,
|
||||
0x65, 0x64, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x27, 0x0a, 0x0f, 0x68, 0x69, 0x67, 0x68, 0x5f, 0x6d,
|
||||
0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||
0x0e, 0x68, 0x69, 0x67, 0x68, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x12,
|
||||
0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73,
|
||||
0x18, 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41,
|
||||
0x74, 0x4d, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73,
|
||||
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72,
|
||||
0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x96, 0x04, 0x0a, 0x13, 0x52, 0x6f,
|
||||
0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x54, 0x61, 0x72, 0x67, 0x65,
|
||||
0x74, 0x52, 0x07, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f,
|
||||
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
|
||||
0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73,
|
||||
0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x16,
|
||||
0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67,
|
||||
0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xf7, 0x04, 0x0a, 0x17, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f,
|
||||
0x62, 0x6f, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77,
|
||||
0x6e, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x06, 0x64, 0x72, 0x61, 0x77, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f,
|
||||
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
|
||||
0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e,
|
||||
0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12,
|
||||
0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55,
|
||||
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64,
|
||||
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d,
|
||||
0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01,
|
||||
0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x17, 0x0a,
|
||||
0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
|
||||
0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70,
|
||||
0x6c, 0x69, 0x65, 0x72, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d,
|
||||
0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x50, 0x70, 0x6d, 0x12, 0x2a, 0x0a,
|
||||
0x11, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69,
|
||||
0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65,
|
||||
0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x3f, 0x0a, 0x1c, 0x72, 0x6f, 0x6f,
|
||||
0x6d, 0x5f, 0x61, 0x74, 0x6d, 0x6f, 0x73, 0x70, 0x68, 0x65, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x77,
|
||||
0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x19, 0x72, 0x6f, 0x6f, 0x6d, 0x41, 0x74, 0x6d, 0x6f, 0x73, 0x70, 0x68, 0x65, 0x72, 0x65, 0x52,
|
||||
0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x61, 0x63,
|
||||
0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x69, 0x64, 0x79, 0x5f, 0x63,
|
||||
0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x61, 0x63, 0x74, 0x69,
|
||||
0x76, 0x69, 0x74, 0x79, 0x53, 0x75, 0x62, 0x73, 0x69, 0x64, 0x79, 0x43, 0x6f, 0x69, 0x6e, 0x73,
|
||||
0x12, 0x34, 0x0a, 0x16, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x72, 0x65,
|
||||
0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x14, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72,
|
||||
0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
|
||||
0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63,
|
||||
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69,
|
||||
0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18,
|
||||
0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65,
|
||||
0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x72, 0x6f, 0x62,
|
||||
0x6f, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x52, 0x6f, 0x62, 0x6f,
|
||||
0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x18, 0x10,
|
||||
0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x22,
|
||||
0x4a, 0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67,
|
||||
0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x03, 0x52, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72,
|
||||
0x65, 0x6e, 0x74, 0x5f, 0x68, 0x65, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
|
||||
0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x65, 0x61, 0x74, 0x22, 0x5f, 0x0a, 0x0f, 0x52,
|
||||
0x6f, 0x6f, 0x6d, 0x52, 0x61, 0x6e, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x17,
|
||||
0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x0a,
|
||||
0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x03, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x94, 0x02, 0x0a,
|
||||
0x15, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72,
|
||||
0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64,
|
||||
0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x77,
|
||||
0x61, 0x72, 0x64, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64,
|
||||
0x12, 0x24, 0x0a, 0x0e, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64,
|
||||
0x49, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
|
||||
0x63, 0x65, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||
0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70,
|
||||
0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61,
|
||||
0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61,
|
||||
0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72,
|
||||
0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x69, 0x63, 0x6f, 0x6e, 0x55, 0x72, 0x6c,
|
||||
0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73,
|
||||
0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61,
|
||||
0x74, 0x75, 0x73, 0x22, 0xeb, 0x03, 0x0a, 0x15, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b,
|
||||
0x65, 0x74, 0x46, 0x75, 0x65, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x1b, 0x0a,
|
||||
0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65,
|
||||
0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c,
|
||||
0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x61, 0x64, 0x64, 0x65, 0x64, 0x46, 0x75, 0x65, 0x6c, 0x12,
|
||||
0x30, 0x0a, 0x14, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61, 0x64, 0x64,
|
||||
0x65, 0x64, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x65,
|
||||
0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x65, 0x64, 0x46, 0x75, 0x65,
|
||||
0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x66, 0x75,
|
||||
0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c,
|
||||
0x6f, 0x77, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e,
|
||||
0x74, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75,
|
||||
0x72, 0x72, 0x65, 0x6e, 0x74, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x75, 0x65,
|
||||
0x6c, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28,
|
||||
0x03, 0x52, 0x0d, 0x66, 0x75, 0x65, 0x6c, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64,
|
||||
0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65,
|
||||
0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72,
|
||||
0x65, 0x73, 0x65, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64,
|
||||
0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17,
|
||||
0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x74, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72,
|
||||
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65,
|
||||
0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x72, 0x65, 0x63, 0x65, 0x69,
|
||||
0x76, 0x65, 0x72, 0x5f, 0x6e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x10, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x4e, 0x69, 0x63, 0x6b,
|
||||
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72,
|
||||
0x5f, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72,
|
||||
0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x41, 0x76, 0x61, 0x74, 0x61, 0x72, 0x12, 0x37, 0x0a,
|
||||
0x18, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61,
|
||||
0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x15, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79,
|
||||
0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x44, 0x0a, 0x1f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76,
|
||||
0x65, 0x72, 0x5f, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61,
|
||||
0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x1b, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44,
|
||||
0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a,
|
||||
0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x74,
|
||||
0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x47, 0x69,
|
||||
0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x69, 0x6e, 0x5f,
|
||||
0x73, 0x70, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x6f, 0x69,
|
||||
0x6e, 0x53, 0x70, 0x65, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e,
|
||||
0x67, 0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x10, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x65, 0x69,
|
||||
0x70, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
|
||||
0x64, 0x49, 0x64, 0x12, 0x4d, 0x0a, 0x0a, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x5f, 0x67, 0x69, 0x66,
|
||||
0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e,
|
||||
0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52,
|
||||
0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x75, 0x63, 0x6b,
|
||||
0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x09, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69,
|
||||
0x66, 0x74, 0x22, 0x89, 0x07, 0x0a, 0x11, 0x52, 0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x42,
|
||||
0x61, 0x74, 0x63, 0x68, 0x53, 0x65, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64,
|
||||
0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1f,
|
||||
0x0a, 0x0b, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12,
|
||||
0x23, 0x0a, 0x0d, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x41, 0x76,
|
||||
0x61, 0x74, 0x61, 0x72, 0x12, 0x33, 0x0a, 0x16, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x64,
|
||||
0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x44, 0x69, 0x73, 0x70,
|
||||
0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x40, 0x0a, 0x1d, 0x73, 0x65, 0x6e,
|
||||
0x64, 0x65, 0x72, 0x5f, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c,
|
||||
0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x19, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x50, 0x72, 0x65, 0x74, 0x74, 0x79, 0x44, 0x69,
|
||||
0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67,
|
||||
0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69,
|
||||
0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75,
|
||||
0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f,
|
||||
0x75, 0x6e, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x67, 0x69, 0x66,
|
||||
0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74,
|
||||
0x6f, 0x74, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x28, 0x0a,
|
||||
0x10, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x5f, 0x73, 0x70, 0x65, 0x6e,
|
||||
0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f,
|
||||
0x69, 0x6e, 0x53, 0x70, 0x65, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69,
|
||||
0x6e, 0x67, 0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x10, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x65,
|
||||
0x69, 0x70, 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x68, 0x65,
|
||||
0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x6f, 0x6f, 0x6d, 0x48, 0x65,
|
||||
0x61, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x67,
|
||||
0x69, 0x66, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0d, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x0c, 0x67, 0x69, 0x66, 0x74, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x64,
|
||||
0x65, 0x12, 0x28, 0x0a, 0x10, 0x63, 0x70, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x70, 0x52,
|
||||
0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x67,
|
||||
0x69, 0x66, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
|
||||
0x67, 0x69, 0x66, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x67, 0x69, 0x66, 0x74,
|
||||
0x5f, 0x69, 0x63, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0b, 0x67, 0x69, 0x66, 0x74, 0x49, 0x63, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x2c, 0x0a, 0x12,
|
||||
0x67, 0x69, 0x66, 0x74, 0x5f, 0x61, 0x6e, 0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75,
|
||||
0x72, 0x6c, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x67, 0x69, 0x66, 0x74, 0x41, 0x6e,
|
||||
0x69, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x2a, 0x0a, 0x11, 0x67, 0x69,
|
||||
0x66, 0x74, 0x5f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18,
|
||||
0x12, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x67, 0x69, 0x66, 0x74, 0x45, 0x66, 0x66, 0x65, 0x63,
|
||||
0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74,
|
||||
0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x13, 0x20, 0x03, 0x28, 0x03, 0x52,
|
||||
0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, 0x12, 0x43,
|
||||
0x0a, 0x07, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
||||
0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72,
|
||||
0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x42,
|
||||
0x61, 0x74, 0x63, 0x68, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x07, 0x74, 0x61, 0x72, 0x67,
|
||||
0x65, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
|
||||
0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65,
|
||||
0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x16, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76,
|
||||
0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0x80,
|
||||
0x04, 0x0a, 0x17, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x62, 0x6f, 0x74, 0x4c, 0x75, 0x63, 0x6b,
|
||||
0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72,
|
||||
0x61, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x72, 0x61,
|
||||
0x77, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
|
||||
0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65,
|
||||
0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64,
|
||||
0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
|
||||
0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17,
|
||||
0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f,
|
||||
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66,
|
||||
0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
|
||||
0x64, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d,
|
||||
0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65,
|
||||
0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03,
|
||||
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66,
|
||||
0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12,
|
||||
0x25, 0x0a, 0x0e, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x70, 0x70,
|
||||
0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c,
|
||||
0x69, 0x65, 0x72, 0x50, 0x70, 0x6d, 0x12, 0x2a, 0x0a, 0x11, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x72,
|
||||
0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28,
|
||||
0x03, 0x52, 0x0f, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69,
|
||||
0x6e, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f,
|
||||
0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x0a, 0x20, 0x01,
|
||||
0x28, 0x03, 0x52, 0x14, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x77,
|
||||
0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x22, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61,
|
||||
0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x2a, 0x0a, 0x11,
|
||||
0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65,
|
||||
0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x72,
|
||||
0x6f, 0x62, 0x6f, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x52, 0x6f,
|
||||
0x62, 0x6f, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63,
|
||||
0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69,
|
||||
0x63, 0x22, 0x4a, 0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x74, 0x43, 0x68, 0x61,
|
||||
0x6e, 0x67, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75,
|
||||
0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x65, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x65, 0x61, 0x74, 0x22, 0x5f, 0x0a,
|
||||
0x0f, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x61, 0x6e, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64,
|
||||
0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f,
|
||||
0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12,
|
||||
0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x94,
|
||||
0x02, 0x0a, 0x15, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77,
|
||||
0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x77, 0x61,
|
||||
0x72, 0x64, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72,
|
||||
0x65, 0x77, 0x61, 0x72, 0x64, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65,
|
||||
0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72,
|
||||
0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x69, 0x74, 0x65,
|
||||
0x6d, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x77, 0x61,
|
||||
0x72, 0x64, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x47, 0x72, 0x6f,
|
||||
0x75, 0x70, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f,
|
||||
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70,
|
||||
0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x63, 0x6f, 0x6e, 0x5f,
|
||||
0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x69, 0x63, 0x6f, 0x6e, 0x55,
|
||||
0x72, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x07,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a,
|
||||
0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73,
|
||||
0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xeb, 0x03, 0x0a, 0x15, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f,
|
||||
0x63, 0x6b, 0x65, 0x74, 0x46, 0x75, 0x65, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12,
|
||||
0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76,
|
||||
0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x65, 0x6c,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x61, 0x64, 0x64, 0x65, 0x64, 0x46, 0x75, 0x65,
|
||||
0x6c, 0x12, 0x30, 0x0a, 0x14, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x61,
|
||||
0x64, 0x64, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x12, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x41, 0x64, 0x64, 0x65, 0x64, 0x46,
|
||||
0x75, 0x65, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x5f,
|
||||
0x66, 0x75, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x76, 0x65, 0x72,
|
||||
0x66, 0x6c, 0x6f, 0x77, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72,
|
||||
0x65, 0x6e, 0x74, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
|
||||
0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x0e, 0x66,
|
||||
0x75, 0x65, 0x6c, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18, 0x07, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x0d, 0x66, 0x75, 0x65, 0x6c, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f,
|
||||
0x6c, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65,
|
||||
0x73, 0x65, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52,
|
||||
0x09, 0x72, 0x65, 0x73, 0x65, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65,
|
||||
0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01,
|
||||
0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64,
|
||||
0x12, 0x17, 0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66,
|
||||
0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67,
|
||||
0x69, 0x66, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d,
|
||||
0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f,
|
||||
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62,
|
||||
0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01,
|
||||
0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f,
|
||||
0x6e, 0x49, 0x64, 0x22, 0x82, 0x04, 0x0a, 0x11, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b,
|
||||
0x65, 0x74, 0x49, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63,
|
||||
0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f,
|
||||
0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x21, 0x0a, 0x0c,
|
||||
0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01,
|
||||
0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x46, 0x75, 0x65, 0x6c, 0x12,
|
||||
0x25, 0x0a, 0x0e, 0x66, 0x75, 0x65, 0x6c, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c,
|
||||
0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x66, 0x75, 0x65, 0x6c, 0x54, 0x68, 0x72,
|
||||
0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65,
|
||||
0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69,
|
||||
0x67, 0x6e, 0x69, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x6c, 0x61,
|
||||
0x75, 0x6e, 0x63, 0x68, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0a, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x27, 0x0a, 0x0f,
|
||||
0x62, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18,
|
||||
0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x62, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74,
|
||||
0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65,
|
||||
0x5f, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0f, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49,
|
||||
0x64, 0x22, 0x82, 0x04, 0x0a, 0x11, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74,
|
||||
0x49, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65,
|
||||
0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b,
|
||||
0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75,
|
||||
0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x66, 0x75, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0b, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x46, 0x75, 0x65, 0x6c, 0x12, 0x25, 0x0a,
|
||||
0x0e, 0x66, 0x75, 0x65, 0x6c, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x66, 0x75, 0x65, 0x6c, 0x54, 0x68, 0x72, 0x65, 0x73,
|
||||
0x68, 0x6f, 0x6c, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x64, 0x5f,
|
||||
0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x69, 0x67, 0x6e,
|
||||
0x69, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x6c, 0x61, 0x75, 0x6e,
|
||||
0x63, 0x68, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a,
|
||||
0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x62, 0x72,
|
||||
0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x0e, 0x62, 0x72, 0x6f, 0x61, 0x64, 0x63, 0x61, 0x73, 0x74, 0x53, 0x63,
|
||||
0x6f, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72,
|
||||
0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f,
|
||||
0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12,
|
||||
0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x70, 0x31, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
|
||||
0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x70, 0x31, 0x55, 0x73, 0x65, 0x72, 0x49,
|
||||
0x64, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65,
|
||||
0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x67, 0x6e, 0x69,
|
||||
0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63,
|
||||
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x6f, 0x6f, 0x6d,
|
||||
0x5f, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0b, 0x72, 0x6f, 0x6f, 0x6d, 0x53, 0x68, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10,
|
||||
0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x75, 0x72, 0x6c,
|
||||
0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x43, 0x6f,
|
||||
0x76, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f,
|
||||
0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73,
|
||||
0x65, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xe6, 0x02, 0x0a, 0x12, 0x52, 0x6f, 0x6f, 0x6d, 0x52,
|
||||
0x6f, 0x63, 0x6b, 0x65, 0x74, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x12, 0x1b, 0x0a,
|
||||
0x64, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x70, 0x31, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
|
||||
0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x70, 0x31, 0x55, 0x73, 0x65,
|
||||
0x72, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x5f, 0x75,
|
||||
0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x67,
|
||||
0x6e, 0x69, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63,
|
||||
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x6f,
|
||||
0x6f, 0x6d, 0x5f, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0b, 0x72, 0x6f, 0x6f, 0x6d, 0x53, 0x68, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x12, 0x28,
|
||||
0x0a, 0x10, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x5f, 0x75,
|
||||
0x72, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74,
|
||||
0x43, 0x6f, 0x76, 0x65, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65,
|
||||
0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72,
|
||||
0x65, 0x73, 0x65, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x22, 0xe6, 0x02, 0x0a, 0x12, 0x52, 0x6f, 0x6f,
|
||||
0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x4c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x12,
|
||||
0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76,
|
||||
0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x6e, 0x65, 0x78, 0x74, 0x4c, 0x65, 0x76, 0x65,
|
||||
0x6c, 0x12, 0x24, 0x0a, 0x0e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74,
|
||||
0x5f, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6c, 0x61, 0x75, 0x6e, 0x63,
|
||||
0x68, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74,
|
||||
0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65,
|
||||
0x73, 0x65, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x70, 0x31, 0x5f,
|
||||
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74,
|
||||
0x6f, 0x70, 0x31, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x67, 0x6e,
|
||||
0x69, 0x74, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01,
|
||||
0x28, 0x03, 0x52, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49,
|
||||
0x64, 0x12, 0x27, 0x0a, 0x10, 0x69, 0x6e, 0x5f, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x75, 0x73, 0x65,
|
||||
0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x6e, 0x52,
|
||||
0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, 0x12, 0x45, 0x0a, 0x07, 0x72, 0x65,
|
||||
0x77, 0x61, 0x72, 0x64, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79,
|
||||
0x61, 0x70, 0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77,
|
||||
0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64,
|
||||
0x73, 0x22, 0x93, 0x01, 0x0a, 0x17, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74,
|
||||
0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x12, 0x1b, 0x0a,
|
||||
0x09, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x08, 0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65,
|
||||
0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c,
|
||||
0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x6e, 0x65, 0x78, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12,
|
||||
0x24, 0x0a, 0x0e, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d,
|
||||
0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6c, 0x61, 0x75, 0x6e, 0x63, 0x68, 0x65,
|
||||
0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x72, 0x65, 0x73, 0x65, 0x74, 0x5f, 0x61,
|
||||
0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x73, 0x65,
|
||||
0x74, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x70, 0x31, 0x5f, 0x75, 0x73,
|
||||
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x70,
|
||||
0x31, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x67, 0x6e, 0x69, 0x74,
|
||||
0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03,
|
||||
0x52, 0x0d, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12,
|
||||
0x27, 0x0a, 0x10, 0x69, 0x6e, 0x5f, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f,
|
||||
0x69, 0x64, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x6e, 0x52, 0x6f, 0x6f,
|
||||
0x6d, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x73, 0x12, 0x45, 0x0a, 0x07, 0x72, 0x65, 0x77, 0x61,
|
||||
0x72, 0x64, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70,
|
||||
0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72,
|
||||
0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x22,
|
||||
0x93, 0x01, 0x0a, 0x17, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65,
|
||||
0x77, 0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72,
|
||||
0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
|
||||
0x72, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65,
|
||||
0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x45,
|
||||
0x0a, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
||||
0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x72,
|
||||
0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63, 0x6b, 0x65,
|
||||
0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x07, 0x72, 0x65,
|
||||
0x77, 0x61, 0x72, 0x64, 0x73, 0x42, 0x33, 0x5a, 0x31, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c,
|
||||
0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65,
|
||||
0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x72, 0x6f, 0x6f, 0x6d, 0x2f, 0x76, 0x31, 0x3b, 0x72, 0x6f,
|
||||
0x6f, 0x6d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x33,
|
||||
0x12, 0x45, 0x0a, 0x07, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
|
||||
0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73,
|
||||
0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6f, 0x6d, 0x52, 0x6f, 0x63,
|
||||
0x6b, 0x65, 0x74, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x07,
|
||||
0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x73, 0x42, 0x33, 0x5a, 0x31, 0x68, 0x79, 0x61, 0x70, 0x70,
|
||||
0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x72, 0x6f, 0x6f, 0x6d, 0x2f, 0x76, 0x31, 0x3b,
|
||||
0x72, 0x6f, 0x6f, 0x6d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@ -211,14 +211,12 @@ message RoomGiftBatchLuckyResult {
|
||||
string selected_tier_id = 8;
|
||||
int64 multiplier_ppm = 9;
|
||||
int64 base_reward_coins = 10;
|
||||
int64 room_atmosphere_reward_coins = 11;
|
||||
int64 activity_subsidy_coins = 12;
|
||||
int64 effective_reward_coins = 13;
|
||||
string reward_status = 14;
|
||||
bool stage_feedback = 15;
|
||||
bool high_multiplier = 16;
|
||||
int64 created_at_ms = 17;
|
||||
int64 target_user_id = 18;
|
||||
int64 effective_reward_coins = 11;
|
||||
string reward_status = 12;
|
||||
bool stage_feedback = 13;
|
||||
bool high_multiplier = 14;
|
||||
int64 created_at_ms = 15;
|
||||
int64 target_user_id = 16;
|
||||
}
|
||||
|
||||
// RoomGiftBatchTarget 固化批量送礼中单个收礼目标的展示和结算快照。
|
||||
@ -276,13 +274,11 @@ message RoomRobotLuckyGiftDrawn {
|
||||
string pool_id = 7;
|
||||
int64 multiplier_ppm = 8;
|
||||
int64 base_reward_coins = 9;
|
||||
int64 room_atmosphere_reward_coins = 10;
|
||||
int64 activity_subsidy_coins = 11;
|
||||
int64 effective_reward_coins = 12;
|
||||
int64 created_at_ms = 13;
|
||||
int64 visible_region_id = 14;
|
||||
bool is_robot = 15;
|
||||
bool synthetic = 16;
|
||||
int64 effective_reward_coins = 10;
|
||||
int64 created_at_ms = 11;
|
||||
int64 visible_region_id = 12;
|
||||
bool is_robot = 13;
|
||||
bool synthetic = 14;
|
||||
}
|
||||
|
||||
// RoomHeatChanged 表达热度变化结果。
|
||||
|
||||
2958
api/proto/luckygift/v1/luckygift.pb.go
Normal file
2958
api/proto/luckygift/v1/luckygift.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
269
api/proto/luckygift/v1/luckygift.proto
Normal file
269
api/proto/luckygift/v1/luckygift.proto
Normal file
@ -0,0 +1,269 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package hyapp.luckygift.v1;
|
||||
|
||||
option go_package = "hyapp.local/api/proto/luckygift/v1;luckygiftv1";
|
||||
|
||||
// RequestMeta 是 lucky-gift-service 的内部追踪元信息。
|
||||
// request_id 只用于链路排查,不能被任何业务流程当成幂等键使用。
|
||||
message RequestMeta {
|
||||
string request_id = 1;
|
||||
string caller = 2;
|
||||
string gateway_node_id = 3;
|
||||
int64 sent_at_ms = 4;
|
||||
string app_code = 5;
|
||||
}
|
||||
|
||||
// LuckyGiftMeta 是 HyApp 内部房间送礼扣费成功后的抽奖事实输入。
|
||||
// 金额、目标用户和 command_id 必须来自 wallet-service 已落账的 receipt,不能从客户端 payload 推导。
|
||||
message LuckyGiftMeta {
|
||||
RequestMeta meta = 1;
|
||||
string command_id = 2;
|
||||
int64 user_id = 3;
|
||||
string device_id = 4;
|
||||
string room_id = 5;
|
||||
string anchor_id = 6;
|
||||
string gift_id = 7;
|
||||
int64 coin_spent = 8;
|
||||
int64 paid_at_ms = 9;
|
||||
string pool_id = 10;
|
||||
int64 target_user_id = 11;
|
||||
int32 gift_count = 12;
|
||||
int64 visible_region_id = 13;
|
||||
int64 country_id = 14;
|
||||
}
|
||||
|
||||
message LuckyGiftRuleTier {
|
||||
string stage = 1;
|
||||
string tier_id = 2;
|
||||
int64 multiplier_ppm = 3;
|
||||
int64 base_weight_ppm = 4;
|
||||
bool high_water_only = 5;
|
||||
bool enabled = 6;
|
||||
}
|
||||
|
||||
message LuckyGiftRuleStage {
|
||||
string stage = 1;
|
||||
repeated LuckyGiftRuleTier tiers = 2;
|
||||
}
|
||||
|
||||
message LuckyGiftRuleConfig {
|
||||
string app_code = 1;
|
||||
string pool_id = 2;
|
||||
int64 rule_version = 3;
|
||||
bool enabled = 4;
|
||||
int64 target_rtp_ppm = 5;
|
||||
int64 pool_rate_ppm = 6;
|
||||
int64 settlement_window_wager = 7;
|
||||
int64 control_band_ppm = 8;
|
||||
int64 gift_price_reference = 9;
|
||||
int64 novice_max_equivalent_draws = 10;
|
||||
int64 normal_max_equivalent_draws = 11;
|
||||
int64 effective_from_ms = 12;
|
||||
int64 created_by_admin_id = 13;
|
||||
int64 created_at_ms = 14;
|
||||
repeated LuckyGiftRuleStage stages = 15;
|
||||
}
|
||||
|
||||
message CheckLuckyGiftRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
string room_id = 3;
|
||||
string gift_id = 4;
|
||||
string pool_id = 5;
|
||||
}
|
||||
|
||||
message CheckLuckyGiftResponse {
|
||||
bool enabled = 1;
|
||||
string reason = 2;
|
||||
string gift_id = 3;
|
||||
int64 gift_price = 4;
|
||||
int64 rule_version = 5;
|
||||
int64 target_rtp_ppm = 6;
|
||||
string experience_pool = 7;
|
||||
string pool_id = 8;
|
||||
}
|
||||
|
||||
message LuckyGiftDrawResult {
|
||||
string draw_id = 1;
|
||||
string command_id = 2;
|
||||
string gift_id = 3;
|
||||
int64 rule_version = 4;
|
||||
string experience_pool = 5;
|
||||
string selected_tier_id = 6;
|
||||
int64 base_reward_coins = 7;
|
||||
int64 effective_reward_coins = 8;
|
||||
string reward_status = 9;
|
||||
int64 rtp_window_index = 10;
|
||||
int64 gift_rtp_window_index = 11;
|
||||
int64 global_base_rtp_ppm = 12;
|
||||
int64 gift_base_rtp_ppm = 13;
|
||||
bool stage_feedback = 14;
|
||||
bool high_multiplier = 15;
|
||||
int64 created_at_ms = 16;
|
||||
string pool_id = 17;
|
||||
int64 multiplier_ppm = 18;
|
||||
string wallet_transaction_id = 19;
|
||||
int64 coin_balance_after = 20;
|
||||
int64 user_id = 21;
|
||||
string external_user_id = 22;
|
||||
string app_code = 23;
|
||||
}
|
||||
|
||||
message ExecuteLuckyGiftDrawRequest {
|
||||
LuckyGiftMeta lucky_gift = 1;
|
||||
}
|
||||
|
||||
message ExecuteLuckyGiftDrawResponse {
|
||||
LuckyGiftDrawResult result = 1;
|
||||
}
|
||||
|
||||
message BatchExecuteLuckyGiftDrawRequest {
|
||||
repeated LuckyGiftMeta lucky_gifts = 1;
|
||||
}
|
||||
|
||||
message BatchExecuteLuckyGiftDrawResponse {
|
||||
repeated LuckyGiftDrawResult results = 1;
|
||||
}
|
||||
|
||||
// ExternalGiftDrawRequest 是外部 App 接入的抽奖输入。
|
||||
// App 调用前必须已经完成自己的扣费,lucky-gift-service 只校验金额事实并返回中奖结果。
|
||||
message ExternalGiftDrawRequest {
|
||||
RequestMeta meta = 1;
|
||||
string app_code = 3;
|
||||
string external_user_id = 4;
|
||||
string request_id = 5;
|
||||
int64 gift_count = 6;
|
||||
int64 unit_amount = 7;
|
||||
int64 total_amount = 8;
|
||||
string currency = 9;
|
||||
int64 paid_at_ms = 10;
|
||||
string metadata_json = 11;
|
||||
string pool_id = 12;
|
||||
}
|
||||
|
||||
message ExternalGiftDrawResponse {
|
||||
string draw_id = 1;
|
||||
string request_id = 2;
|
||||
string app_code = 4;
|
||||
string external_user_id = 5;
|
||||
int64 gift_count = 6;
|
||||
int64 unit_amount = 7;
|
||||
int64 total_amount = 8;
|
||||
int64 reward_amount = 9;
|
||||
int64 multiplier_ppm = 10;
|
||||
string reward_status = 11;
|
||||
int64 rule_version = 12;
|
||||
int64 created_at_ms = 13;
|
||||
}
|
||||
|
||||
message GetLuckyGiftConfigRequest {
|
||||
RequestMeta meta = 1;
|
||||
string pool_id = 2;
|
||||
}
|
||||
|
||||
message GetLuckyGiftConfigResponse {
|
||||
LuckyGiftRuleConfig config = 1;
|
||||
}
|
||||
|
||||
message UpsertLuckyGiftConfigRequest {
|
||||
RequestMeta meta = 1;
|
||||
LuckyGiftRuleConfig config = 2;
|
||||
int64 operator_admin_id = 3;
|
||||
}
|
||||
|
||||
message UpsertLuckyGiftConfigResponse {
|
||||
LuckyGiftRuleConfig config = 1;
|
||||
}
|
||||
|
||||
message ListLuckyGiftConfigsRequest {
|
||||
RequestMeta meta = 1;
|
||||
}
|
||||
|
||||
message ListLuckyGiftConfigsResponse {
|
||||
repeated LuckyGiftRuleConfig configs = 1;
|
||||
}
|
||||
|
||||
message ListLuckyGiftDrawsRequest {
|
||||
RequestMeta meta = 1;
|
||||
string gift_id = 2;
|
||||
int64 user_id = 3;
|
||||
string room_id = 4;
|
||||
string status = 5;
|
||||
int32 page = 6;
|
||||
int32 page_size = 7;
|
||||
string pool_id = 8;
|
||||
string external_user_id = 10;
|
||||
bool external_only = 11;
|
||||
}
|
||||
|
||||
message ListLuckyGiftDrawsResponse {
|
||||
repeated LuckyGiftDrawResult draws = 1;
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
message LuckyGiftDrawSummary {
|
||||
string pool_id = 1;
|
||||
int64 total_draws = 2;
|
||||
int64 unique_users = 3;
|
||||
int64 unique_rooms = 4;
|
||||
int64 total_spent_coins = 5;
|
||||
int64 total_reward_coins = 6;
|
||||
int64 base_reward_coins = 7;
|
||||
int64 actual_rtp_ppm = 8;
|
||||
int64 pending_draws = 9;
|
||||
int64 granted_draws = 10;
|
||||
int64 failed_draws = 11;
|
||||
}
|
||||
|
||||
message LuckyGiftPoolBalance {
|
||||
string app_code = 1;
|
||||
string pool_id = 2;
|
||||
int64 balance = 3;
|
||||
int64 reserve_floor = 4;
|
||||
int64 available_balance = 5;
|
||||
int64 total_in = 6;
|
||||
int64 total_out = 7;
|
||||
bool materialized = 8;
|
||||
int64 updated_at_ms = 9;
|
||||
}
|
||||
|
||||
message GetLuckyGiftDrawSummaryRequest {
|
||||
RequestMeta meta = 1;
|
||||
string gift_id = 2;
|
||||
int64 user_id = 3;
|
||||
string room_id = 4;
|
||||
string status = 5;
|
||||
string pool_id = 6;
|
||||
string external_user_id = 8;
|
||||
bool external_only = 9;
|
||||
}
|
||||
|
||||
message GetLuckyGiftDrawSummaryResponse {
|
||||
LuckyGiftDrawSummary summary = 1;
|
||||
}
|
||||
|
||||
message ListLuckyGiftPoolBalancesRequest {
|
||||
RequestMeta meta = 1;
|
||||
string pool_id = 2;
|
||||
}
|
||||
|
||||
message ListLuckyGiftPoolBalancesResponse {
|
||||
repeated LuckyGiftPoolBalance pools = 1;
|
||||
}
|
||||
|
||||
service LuckyGiftService {
|
||||
rpc CheckLuckyGift(CheckLuckyGiftRequest) returns (CheckLuckyGiftResponse);
|
||||
rpc ExecuteLuckyGiftDraw(ExecuteLuckyGiftDrawRequest) returns (ExecuteLuckyGiftDrawResponse);
|
||||
rpc BatchExecuteLuckyGiftDraw(BatchExecuteLuckyGiftDrawRequest) returns (BatchExecuteLuckyGiftDrawResponse);
|
||||
rpc ExecuteExternalGiftDraw(ExternalGiftDrawRequest) returns (ExternalGiftDrawResponse);
|
||||
}
|
||||
|
||||
service AdminLuckyGiftService {
|
||||
rpc GetLuckyGiftConfig(GetLuckyGiftConfigRequest) returns (GetLuckyGiftConfigResponse);
|
||||
rpc UpsertLuckyGiftConfig(UpsertLuckyGiftConfigRequest) returns (UpsertLuckyGiftConfigResponse);
|
||||
rpc ListLuckyGiftConfigs(ListLuckyGiftConfigsRequest) returns (ListLuckyGiftConfigsResponse);
|
||||
rpc ListLuckyGiftDraws(ListLuckyGiftDrawsRequest) returns (ListLuckyGiftDrawsResponse);
|
||||
rpc GetLuckyGiftDrawSummary(GetLuckyGiftDrawSummaryRequest) returns (GetLuckyGiftDrawSummaryResponse);
|
||||
rpc ListLuckyGiftPoolBalances(ListLuckyGiftPoolBalancesRequest) returns (ListLuckyGiftPoolBalancesResponse);
|
||||
}
|
||||
527
api/proto/luckygift/v1/luckygift_grpc.pb.go
Normal file
527
api/proto/luckygift/v1/luckygift_grpc.pb.go
Normal file
@ -0,0 +1,527 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v7.35.0
|
||||
// source: proto/luckygift/v1/luckygift.proto
|
||||
|
||||
package luckygiftv1
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
LuckyGiftService_CheckLuckyGift_FullMethodName = "/hyapp.luckygift.v1.LuckyGiftService/CheckLuckyGift"
|
||||
LuckyGiftService_ExecuteLuckyGiftDraw_FullMethodName = "/hyapp.luckygift.v1.LuckyGiftService/ExecuteLuckyGiftDraw"
|
||||
LuckyGiftService_BatchExecuteLuckyGiftDraw_FullMethodName = "/hyapp.luckygift.v1.LuckyGiftService/BatchExecuteLuckyGiftDraw"
|
||||
LuckyGiftService_ExecuteExternalGiftDraw_FullMethodName = "/hyapp.luckygift.v1.LuckyGiftService/ExecuteExternalGiftDraw"
|
||||
)
|
||||
|
||||
// LuckyGiftServiceClient is the client API for LuckyGiftService 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.
|
||||
type LuckyGiftServiceClient interface {
|
||||
CheckLuckyGift(ctx context.Context, in *CheckLuckyGiftRequest, opts ...grpc.CallOption) (*CheckLuckyGiftResponse, error)
|
||||
ExecuteLuckyGiftDraw(ctx context.Context, in *ExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*ExecuteLuckyGiftDrawResponse, error)
|
||||
BatchExecuteLuckyGiftDraw(ctx context.Context, in *BatchExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*BatchExecuteLuckyGiftDrawResponse, error)
|
||||
ExecuteExternalGiftDraw(ctx context.Context, in *ExternalGiftDrawRequest, opts ...grpc.CallOption) (*ExternalGiftDrawResponse, error)
|
||||
}
|
||||
|
||||
type luckyGiftServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewLuckyGiftServiceClient(cc grpc.ClientConnInterface) LuckyGiftServiceClient {
|
||||
return &luckyGiftServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *luckyGiftServiceClient) CheckLuckyGift(ctx context.Context, in *CheckLuckyGiftRequest, opts ...grpc.CallOption) (*CheckLuckyGiftResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CheckLuckyGiftResponse)
|
||||
err := c.cc.Invoke(ctx, LuckyGiftService_CheckLuckyGift_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *luckyGiftServiceClient) ExecuteLuckyGiftDraw(ctx context.Context, in *ExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*ExecuteLuckyGiftDrawResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ExecuteLuckyGiftDrawResponse)
|
||||
err := c.cc.Invoke(ctx, LuckyGiftService_ExecuteLuckyGiftDraw_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *luckyGiftServiceClient) BatchExecuteLuckyGiftDraw(ctx context.Context, in *BatchExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*BatchExecuteLuckyGiftDrawResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(BatchExecuteLuckyGiftDrawResponse)
|
||||
err := c.cc.Invoke(ctx, LuckyGiftService_BatchExecuteLuckyGiftDraw_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *luckyGiftServiceClient) ExecuteExternalGiftDraw(ctx context.Context, in *ExternalGiftDrawRequest, opts ...grpc.CallOption) (*ExternalGiftDrawResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ExternalGiftDrawResponse)
|
||||
err := c.cc.Invoke(ctx, LuckyGiftService_ExecuteExternalGiftDraw_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// LuckyGiftServiceServer is the server API for LuckyGiftService service.
|
||||
// All implementations must embed UnimplementedLuckyGiftServiceServer
|
||||
// for forward compatibility.
|
||||
type LuckyGiftServiceServer interface {
|
||||
CheckLuckyGift(context.Context, *CheckLuckyGiftRequest) (*CheckLuckyGiftResponse, error)
|
||||
ExecuteLuckyGiftDraw(context.Context, *ExecuteLuckyGiftDrawRequest) (*ExecuteLuckyGiftDrawResponse, error)
|
||||
BatchExecuteLuckyGiftDraw(context.Context, *BatchExecuteLuckyGiftDrawRequest) (*BatchExecuteLuckyGiftDrawResponse, error)
|
||||
ExecuteExternalGiftDraw(context.Context, *ExternalGiftDrawRequest) (*ExternalGiftDrawResponse, error)
|
||||
mustEmbedUnimplementedLuckyGiftServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedLuckyGiftServiceServer 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 UnimplementedLuckyGiftServiceServer struct{}
|
||||
|
||||
func (UnimplementedLuckyGiftServiceServer) CheckLuckyGift(context.Context, *CheckLuckyGiftRequest) (*CheckLuckyGiftResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckLuckyGift not implemented")
|
||||
}
|
||||
func (UnimplementedLuckyGiftServiceServer) ExecuteLuckyGiftDraw(context.Context, *ExecuteLuckyGiftDrawRequest) (*ExecuteLuckyGiftDrawResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExecuteLuckyGiftDraw not implemented")
|
||||
}
|
||||
func (UnimplementedLuckyGiftServiceServer) BatchExecuteLuckyGiftDraw(context.Context, *BatchExecuteLuckyGiftDrawRequest) (*BatchExecuteLuckyGiftDrawResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchExecuteLuckyGiftDraw not implemented")
|
||||
}
|
||||
func (UnimplementedLuckyGiftServiceServer) ExecuteExternalGiftDraw(context.Context, *ExternalGiftDrawRequest) (*ExternalGiftDrawResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExecuteExternalGiftDraw not implemented")
|
||||
}
|
||||
func (UnimplementedLuckyGiftServiceServer) mustEmbedUnimplementedLuckyGiftServiceServer() {}
|
||||
func (UnimplementedLuckyGiftServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeLuckyGiftServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to LuckyGiftServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeLuckyGiftServiceServer interface {
|
||||
mustEmbedUnimplementedLuckyGiftServiceServer()
|
||||
}
|
||||
|
||||
func RegisterLuckyGiftServiceServer(s grpc.ServiceRegistrar, srv LuckyGiftServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedLuckyGiftServiceServer 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(&LuckyGiftService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _LuckyGiftService_CheckLuckyGift_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CheckLuckyGiftRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LuckyGiftServiceServer).CheckLuckyGift(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: LuckyGiftService_CheckLuckyGift_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LuckyGiftServiceServer).CheckLuckyGift(ctx, req.(*CheckLuckyGiftRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _LuckyGiftService_ExecuteLuckyGiftDraw_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ExecuteLuckyGiftDrawRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LuckyGiftServiceServer).ExecuteLuckyGiftDraw(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: LuckyGiftService_ExecuteLuckyGiftDraw_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LuckyGiftServiceServer).ExecuteLuckyGiftDraw(ctx, req.(*ExecuteLuckyGiftDrawRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _LuckyGiftService_BatchExecuteLuckyGiftDraw_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BatchExecuteLuckyGiftDrawRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LuckyGiftServiceServer).BatchExecuteLuckyGiftDraw(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: LuckyGiftService_BatchExecuteLuckyGiftDraw_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LuckyGiftServiceServer).BatchExecuteLuckyGiftDraw(ctx, req.(*BatchExecuteLuckyGiftDrawRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _LuckyGiftService_ExecuteExternalGiftDraw_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ExternalGiftDrawRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LuckyGiftServiceServer).ExecuteExternalGiftDraw(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: LuckyGiftService_ExecuteExternalGiftDraw_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LuckyGiftServiceServer).ExecuteExternalGiftDraw(ctx, req.(*ExternalGiftDrawRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// LuckyGiftService_ServiceDesc is the grpc.ServiceDesc for LuckyGiftService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var LuckyGiftService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "hyapp.luckygift.v1.LuckyGiftService",
|
||||
HandlerType: (*LuckyGiftServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "CheckLuckyGift",
|
||||
Handler: _LuckyGiftService_CheckLuckyGift_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ExecuteLuckyGiftDraw",
|
||||
Handler: _LuckyGiftService_ExecuteLuckyGiftDraw_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BatchExecuteLuckyGiftDraw",
|
||||
Handler: _LuckyGiftService_BatchExecuteLuckyGiftDraw_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ExecuteExternalGiftDraw",
|
||||
Handler: _LuckyGiftService_ExecuteExternalGiftDraw_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/luckygift/v1/luckygift.proto",
|
||||
}
|
||||
|
||||
const (
|
||||
AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftConfig"
|
||||
AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/UpsertLuckyGiftConfig"
|
||||
AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftConfigs"
|
||||
AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftDraws"
|
||||
AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftDrawSummary"
|
||||
AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftPoolBalances"
|
||||
)
|
||||
|
||||
// AdminLuckyGiftServiceClient is the client API for AdminLuckyGiftService 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.
|
||||
type AdminLuckyGiftServiceClient interface {
|
||||
GetLuckyGiftConfig(ctx context.Context, in *GetLuckyGiftConfigRequest, opts ...grpc.CallOption) (*GetLuckyGiftConfigResponse, error)
|
||||
UpsertLuckyGiftConfig(ctx context.Context, in *UpsertLuckyGiftConfigRequest, opts ...grpc.CallOption) (*UpsertLuckyGiftConfigResponse, error)
|
||||
ListLuckyGiftConfigs(ctx context.Context, in *ListLuckyGiftConfigsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigsResponse, error)
|
||||
ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error)
|
||||
GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error)
|
||||
ListLuckyGiftPoolBalances(ctx context.Context, in *ListLuckyGiftPoolBalancesRequest, opts ...grpc.CallOption) (*ListLuckyGiftPoolBalancesResponse, error)
|
||||
}
|
||||
|
||||
type adminLuckyGiftServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewAdminLuckyGiftServiceClient(cc grpc.ClientConnInterface) AdminLuckyGiftServiceClient {
|
||||
return &adminLuckyGiftServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) GetLuckyGiftConfig(ctx context.Context, in *GetLuckyGiftConfigRequest, opts ...grpc.CallOption) (*GetLuckyGiftConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetLuckyGiftConfigResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) UpsertLuckyGiftConfig(ctx context.Context, in *UpsertLuckyGiftConfigRequest, opts ...grpc.CallOption) (*UpsertLuckyGiftConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(UpsertLuckyGiftConfigResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) ListLuckyGiftConfigs(ctx context.Context, in *ListLuckyGiftConfigsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListLuckyGiftConfigsResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListLuckyGiftDrawsResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetLuckyGiftDrawSummaryResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) ListLuckyGiftPoolBalances(ctx context.Context, in *ListLuckyGiftPoolBalancesRequest, opts ...grpc.CallOption) (*ListLuckyGiftPoolBalancesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListLuckyGiftPoolBalancesResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AdminLuckyGiftServiceServer is the server API for AdminLuckyGiftService service.
|
||||
// All implementations must embed UnimplementedAdminLuckyGiftServiceServer
|
||||
// for forward compatibility.
|
||||
type AdminLuckyGiftServiceServer interface {
|
||||
GetLuckyGiftConfig(context.Context, *GetLuckyGiftConfigRequest) (*GetLuckyGiftConfigResponse, error)
|
||||
UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error)
|
||||
ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error)
|
||||
ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error)
|
||||
GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error)
|
||||
ListLuckyGiftPoolBalances(context.Context, *ListLuckyGiftPoolBalancesRequest) (*ListLuckyGiftPoolBalancesResponse, error)
|
||||
mustEmbedUnimplementedAdminLuckyGiftServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedAdminLuckyGiftServiceServer 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 UnimplementedAdminLuckyGiftServiceServer struct{}
|
||||
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftConfig(context.Context, *GetLuckyGiftConfigRequest) (*GetLuckyGiftConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetLuckyGiftConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpsertLuckyGiftConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListLuckyGiftConfigs not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListLuckyGiftDraws not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetLuckyGiftDrawSummary not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftPoolBalances(context.Context, *ListLuckyGiftPoolBalancesRequest) (*ListLuckyGiftPoolBalancesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListLuckyGiftPoolBalances not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) mustEmbedUnimplementedAdminLuckyGiftServiceServer() {}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeAdminLuckyGiftServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to AdminLuckyGiftServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeAdminLuckyGiftServiceServer interface {
|
||||
mustEmbedUnimplementedAdminLuckyGiftServiceServer()
|
||||
}
|
||||
|
||||
func RegisterAdminLuckyGiftServiceServer(s grpc.ServiceRegistrar, srv AdminLuckyGiftServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAdminLuckyGiftServiceServer 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(&AdminLuckyGiftService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_GetLuckyGiftConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetLuckyGiftConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftConfig(ctx, req.(*GetLuckyGiftConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_UpsertLuckyGiftConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UpsertLuckyGiftConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).UpsertLuckyGiftConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).UpsertLuckyGiftConfig(ctx, req.(*UpsertLuckyGiftConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_ListLuckyGiftConfigs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListLuckyGiftConfigsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftConfigs(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftConfigs(ctx, req.(*ListLuckyGiftConfigsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_ListLuckyGiftDraws_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListLuckyGiftDrawsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftDraws(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftDraws(ctx, req.(*ListLuckyGiftDrawsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_GetLuckyGiftDrawSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetLuckyGiftDrawSummaryRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftDrawSummary(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftDrawSummary(ctx, req.(*GetLuckyGiftDrawSummaryRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_ListLuckyGiftPoolBalances_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListLuckyGiftPoolBalancesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftPoolBalances(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftPoolBalances(ctx, req.(*ListLuckyGiftPoolBalancesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AdminLuckyGiftService_ServiceDesc is the grpc.ServiceDesc for AdminLuckyGiftService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var AdminLuckyGiftService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "hyapp.luckygift.v1.AdminLuckyGiftService",
|
||||
HandlerType: (*AdminLuckyGiftServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetLuckyGiftConfig",
|
||||
Handler: _AdminLuckyGiftService_GetLuckyGiftConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "UpsertLuckyGiftConfig",
|
||||
Handler: _AdminLuckyGiftService_UpsertLuckyGiftConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListLuckyGiftConfigs",
|
||||
Handler: _AdminLuckyGiftService_ListLuckyGiftConfigs_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListLuckyGiftDraws",
|
||||
Handler: _AdminLuckyGiftService_ListLuckyGiftDraws_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetLuckyGiftDrawSummary",
|
||||
Handler: _AdminLuckyGiftService_GetLuckyGiftDrawSummary_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListLuckyGiftPoolBalances",
|
||||
Handler: _AdminLuckyGiftService_ListLuckyGiftPoolBalances_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/luckygift/v1/luckygift.proto",
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -101,17 +101,15 @@ message LuckyGiftDrawResult {
|
||||
string selected_tier_id = 8;
|
||||
int64 multiplier_ppm = 9;
|
||||
int64 base_reward_coins = 10;
|
||||
int64 room_atmosphere_reward_coins = 11;
|
||||
int64 activity_subsidy_coins = 12;
|
||||
int64 effective_reward_coins = 13;
|
||||
string reward_status = 14;
|
||||
bool stage_feedback = 15;
|
||||
bool high_multiplier = 16;
|
||||
int64 created_at_ms = 17;
|
||||
string wallet_transaction_id = 18;
|
||||
int64 coin_balance_after = 19;
|
||||
int64 effective_reward_coins = 11;
|
||||
string reward_status = 12;
|
||||
bool stage_feedback = 13;
|
||||
bool high_multiplier = 14;
|
||||
int64 created_at_ms = 15;
|
||||
string wallet_transaction_id = 16;
|
||||
int64 coin_balance_after = 17;
|
||||
// target_user_id 是本次幸运礼物抽奖对应的收礼用户;多目标送礼用它关联结果。
|
||||
int64 target_user_id = 20;
|
||||
int64 target_user_id = 18;
|
||||
}
|
||||
|
||||
// RoomRocketRewardItem 是后台配置给客户端展示的火箭奖励候选项。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -256,6 +256,21 @@ message ListBDLeaderAgenciesResponse {
|
||||
repeated Agency agencies = 1;
|
||||
}
|
||||
|
||||
message ListManagerTeamAgenciesRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 manager_user_id = 2;
|
||||
// page_size 是防御性截断上限;正常经理团队规模远小于该值,超限时 truncated=true 提示统计口径不完整。
|
||||
int32 page_size = 3;
|
||||
}
|
||||
|
||||
message ListManagerTeamAgenciesResponse {
|
||||
// agencies 覆盖经理整棵团队树:经理创建的 BD Leader 直挂 Agency + 这些 Leader 下属 BD 拓展的 Agency。
|
||||
repeated Agency agencies = 1;
|
||||
int32 total_bd_leaders = 2;
|
||||
int32 total_bds = 3;
|
||||
bool truncated = 4;
|
||||
}
|
||||
|
||||
message ListBDAgenciesRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 bd_user_id = 2;
|
||||
@ -573,6 +588,8 @@ service UserHostService {
|
||||
rpc ListBDLeaderBDs(ListBDLeaderBDsRequest) returns (ListBDLeaderBDsResponse);
|
||||
rpc ListBDLeaderAgencies(ListBDLeaderAgenciesRequest) returns (ListBDLeaderAgenciesResponse);
|
||||
rpc ListBDAgencies(ListBDAgenciesRequest) returns (ListBDAgenciesResponse);
|
||||
// ListManagerTeamAgencies 返回经理团队树内全部有效 Agency;经理中心团队工资统计据此圈定主播归属范围。
|
||||
rpc ListManagerTeamAgencies(ListManagerTeamAgenciesRequest) returns (ListManagerTeamAgenciesResponse);
|
||||
rpc ListRoleInvitations(ListRoleInvitationsRequest) returns (ListRoleInvitationsResponse);
|
||||
rpc GetRoleInvitation(GetRoleInvitationRequest) returns (GetRoleInvitationResponse);
|
||||
rpc ProcessRoleInvitation(ProcessRoleInvitationRequest) returns (ProcessRoleInvitationResponse);
|
||||
|
||||
@ -29,6 +29,7 @@ const (
|
||||
UserHostService_ListBDLeaderBDs_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDLeaderBDs"
|
||||
UserHostService_ListBDLeaderAgencies_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDLeaderAgencies"
|
||||
UserHostService_ListBDAgencies_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDAgencies"
|
||||
UserHostService_ListManagerTeamAgencies_FullMethodName = "/hyapp.user.v1.UserHostService/ListManagerTeamAgencies"
|
||||
UserHostService_ListRoleInvitations_FullMethodName = "/hyapp.user.v1.UserHostService/ListRoleInvitations"
|
||||
UserHostService_GetRoleInvitation_FullMethodName = "/hyapp.user.v1.UserHostService/GetRoleInvitation"
|
||||
UserHostService_ProcessRoleInvitation_FullMethodName = "/hyapp.user.v1.UserHostService/ProcessRoleInvitation"
|
||||
@ -61,6 +62,8 @@ type UserHostServiceClient interface {
|
||||
ListBDLeaderBDs(ctx context.Context, in *ListBDLeaderBDsRequest, opts ...grpc.CallOption) (*ListBDLeaderBDsResponse, error)
|
||||
ListBDLeaderAgencies(ctx context.Context, in *ListBDLeaderAgenciesRequest, opts ...grpc.CallOption) (*ListBDLeaderAgenciesResponse, error)
|
||||
ListBDAgencies(ctx context.Context, in *ListBDAgenciesRequest, opts ...grpc.CallOption) (*ListBDAgenciesResponse, error)
|
||||
// ListManagerTeamAgencies 返回经理团队树内全部有效 Agency;经理中心团队工资统计据此圈定主播归属范围。
|
||||
ListManagerTeamAgencies(ctx context.Context, in *ListManagerTeamAgenciesRequest, opts ...grpc.CallOption) (*ListManagerTeamAgenciesResponse, error)
|
||||
ListRoleInvitations(ctx context.Context, in *ListRoleInvitationsRequest, opts ...grpc.CallOption) (*ListRoleInvitationsResponse, error)
|
||||
GetRoleInvitation(ctx context.Context, in *GetRoleInvitationRequest, opts ...grpc.CallOption) (*GetRoleInvitationResponse, error)
|
||||
ProcessRoleInvitation(ctx context.Context, in *ProcessRoleInvitationRequest, opts ...grpc.CallOption) (*ProcessRoleInvitationResponse, error)
|
||||
@ -185,6 +188,16 @@ func (c *userHostServiceClient) ListBDAgencies(ctx context.Context, in *ListBDAg
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) ListManagerTeamAgencies(ctx context.Context, in *ListManagerTeamAgenciesRequest, opts ...grpc.CallOption) (*ListManagerTeamAgenciesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListManagerTeamAgenciesResponse)
|
||||
err := c.cc.Invoke(ctx, UserHostService_ListManagerTeamAgencies_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) ListRoleInvitations(ctx context.Context, in *ListRoleInvitationsRequest, opts ...grpc.CallOption) (*ListRoleInvitationsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListRoleInvitationsResponse)
|
||||
@ -341,6 +354,8 @@ type UserHostServiceServer interface {
|
||||
ListBDLeaderBDs(context.Context, *ListBDLeaderBDsRequest) (*ListBDLeaderBDsResponse, error)
|
||||
ListBDLeaderAgencies(context.Context, *ListBDLeaderAgenciesRequest) (*ListBDLeaderAgenciesResponse, error)
|
||||
ListBDAgencies(context.Context, *ListBDAgenciesRequest) (*ListBDAgenciesResponse, error)
|
||||
// ListManagerTeamAgencies 返回经理团队树内全部有效 Agency;经理中心团队工资统计据此圈定主播归属范围。
|
||||
ListManagerTeamAgencies(context.Context, *ListManagerTeamAgenciesRequest) (*ListManagerTeamAgenciesResponse, error)
|
||||
ListRoleInvitations(context.Context, *ListRoleInvitationsRequest) (*ListRoleInvitationsResponse, error)
|
||||
GetRoleInvitation(context.Context, *GetRoleInvitationRequest) (*GetRoleInvitationResponse, error)
|
||||
ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error)
|
||||
@ -395,6 +410,9 @@ func (UnimplementedUserHostServiceServer) ListBDLeaderAgencies(context.Context,
|
||||
func (UnimplementedUserHostServiceServer) ListBDAgencies(context.Context, *ListBDAgenciesRequest) (*ListBDAgenciesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListBDAgencies not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListManagerTeamAgencies(context.Context, *ListManagerTeamAgenciesRequest) (*ListManagerTeamAgenciesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListManagerTeamAgencies not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListRoleInvitations(context.Context, *ListRoleInvitationsRequest) (*ListRoleInvitationsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoleInvitations not implemented")
|
||||
}
|
||||
@ -638,6 +656,24 @@ func _UserHostService_ListBDAgencies_Handler(srv interface{}, ctx context.Contex
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_ListManagerTeamAgencies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListManagerTeamAgenciesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserHostServiceServer).ListManagerTeamAgencies(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserHostService_ListManagerTeamAgencies_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserHostServiceServer).ListManagerTeamAgencies(ctx, req.(*ListManagerTeamAgenciesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_ListRoleInvitations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListRoleInvitationsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -937,6 +973,10 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ListBDAgencies",
|
||||
Handler: _UserHostService_ListBDAgencies_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListManagerTeamAgencies",
|
||||
Handler: _UserHostService_ListManagerTeamAgencies_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListRoleInvitations",
|
||||
Handler: _UserHostService_ListRoleInvitations_Handler,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -80,6 +80,16 @@ message DebitGiftResponse {
|
||||
int64 gift_income_coin_amount = 20;
|
||||
// gift_income_balance_after 是收礼人 COIN 账后余额;未返币时为 0。
|
||||
int64 gift_income_balance_after = 21;
|
||||
// host_point_added 是命中 App 收益政策后给主播 POINT 钱包增加的积分;不参与房间热度/贡献榜。
|
||||
int64 host_point_added = 22;
|
||||
// host_point_balance_after 是主播 POINT 入账后的可用余额;未命中 POINT 政策时为 0。
|
||||
int64 host_point_balance_after = 23;
|
||||
// host_point_asset_type 当前固定为 POINT,显式返回避免客户端把收益积分当成 COIN。
|
||||
string host_point_asset_type = 24;
|
||||
// host_point_policy_instance_code 是本次使用的策略实例快照。
|
||||
string host_point_policy_instance_code = 25;
|
||||
string host_point_template_code = 26;
|
||||
string host_point_template_version = 27;
|
||||
}
|
||||
|
||||
// DebitGiftTarget 是一笔批量送礼中的单个接收方账务快照。
|
||||
@ -218,6 +228,33 @@ message GetHostSalaryProgressResponse {
|
||||
HostSalaryProgress progress = 1;
|
||||
}
|
||||
|
||||
// TeamHostSalaryCycleStat 是一组 Agency 收款人名下主播在单个工资周期内的工资统计。
|
||||
// estimated 口径是“预收入”:按当前累计钻石命中的政策档位工资 + 未达下一档剩余钻石按月底费率折美元,
|
||||
// 与已结算累计取大,保证已关闭周期回读时等于真实发放值。
|
||||
message TeamHostSalaryCycleStat {
|
||||
string cycle_key = 1;
|
||||
int64 estimated_host_salary_usd_minor = 2;
|
||||
int64 settled_host_salary_usd_minor = 3;
|
||||
// active_host_count 统计该周期内累计钻石大于 0 的主播数。
|
||||
int32 active_host_count = 4;
|
||||
int64 total_diamonds = 5;
|
||||
}
|
||||
|
||||
message GetTeamHostSalaryStatsRequest {
|
||||
string request_id = 1;
|
||||
string app_code = 2;
|
||||
// agency_owner_user_ids 由调用方(gateway)从 user-service 组织树解析后传入;wallet 只按周期账户上的收款人快照聚合。
|
||||
repeated int64 agency_owner_user_ids = 3;
|
||||
repeated string cycle_keys = 4;
|
||||
// now_ms 用于解析各区域当前生效的工资政策做档位估算。
|
||||
int64 now_ms = 5;
|
||||
}
|
||||
|
||||
message GetTeamHostSalaryStatsResponse {
|
||||
repeated TeamHostSalaryCycleStat stats = 1;
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
// AdminCreditAssetRequest 是后台手动入账/调账的最小审计命令。
|
||||
message AdminCreditAssetRequest {
|
||||
string command_id = 1;
|
||||
@ -410,6 +447,88 @@ message ReleaseSalaryWithdrawalResponse {
|
||||
int64 salary_usd_minor = 3;
|
||||
}
|
||||
|
||||
// FreezePointWithdrawalRequest 将 Huwaa 策略积分从可用余额冻结到提现审核池。
|
||||
message FreezePointWithdrawalRequest {
|
||||
string command_id = 1;
|
||||
int64 user_id = 2;
|
||||
// asset_type 只能是 POINT 或 COIN_SELLER_POINT;旧工资美元资产继续使用 SalaryWithdrawal 接口。
|
||||
string asset_type = 3;
|
||||
// gross_point_amount 是本次冻结的积分总额,Huwaa 普通 POINT 最低 1,000,000。
|
||||
int64 gross_point_amount = 4;
|
||||
// fee_point_amount 是平台手续费积分;为空时服务端按 fee_bps 计算。
|
||||
int64 fee_point_amount = 5;
|
||||
// net_point_amount 是预计到账积分,必须等于 gross - fee;为空时服务端计算。
|
||||
int64 net_point_amount = 6;
|
||||
// points_per_usd 是展示折算快照,Huwaa 第一套默认 100000。
|
||||
int64 points_per_usd = 7;
|
||||
int32 fee_bps = 8;
|
||||
string reason = 9;
|
||||
string app_code = 10;
|
||||
string withdrawal_ref = 11;
|
||||
}
|
||||
|
||||
message FreezePointWithdrawalResponse {
|
||||
string transaction_id = 1;
|
||||
AssetBalance balance = 2;
|
||||
int64 gross_point_amount = 3;
|
||||
int64 fee_point_amount = 4;
|
||||
int64 net_point_amount = 5;
|
||||
int64 points_per_usd = 6;
|
||||
int32 fee_bps = 7;
|
||||
}
|
||||
|
||||
// SettlePointWithdrawalRequest 在审核通过时扣掉已冻结积分;不会再次扣 available。
|
||||
message SettlePointWithdrawalRequest {
|
||||
string command_id = 1;
|
||||
int64 user_id = 2;
|
||||
string asset_type = 3;
|
||||
int64 gross_point_amount = 4;
|
||||
int64 fee_point_amount = 5;
|
||||
int64 net_point_amount = 6;
|
||||
int64 points_per_usd = 7;
|
||||
int32 fee_bps = 8;
|
||||
int64 operator_user_id = 9;
|
||||
string reason = 10;
|
||||
string app_code = 11;
|
||||
string withdrawal_application_id = 12;
|
||||
}
|
||||
|
||||
message SettlePointWithdrawalResponse {
|
||||
string transaction_id = 1;
|
||||
AssetBalance balance = 2;
|
||||
int64 gross_point_amount = 3;
|
||||
int64 fee_point_amount = 4;
|
||||
int64 net_point_amount = 5;
|
||||
int64 points_per_usd = 6;
|
||||
int32 fee_bps = 7;
|
||||
}
|
||||
|
||||
// ReleasePointWithdrawalRequest 在审核拒绝或申请创建失败时释放冻结积分。
|
||||
message ReleasePointWithdrawalRequest {
|
||||
string command_id = 1;
|
||||
int64 user_id = 2;
|
||||
string asset_type = 3;
|
||||
int64 gross_point_amount = 4;
|
||||
int64 fee_point_amount = 5;
|
||||
int64 net_point_amount = 6;
|
||||
int64 points_per_usd = 7;
|
||||
int32 fee_bps = 8;
|
||||
int64 operator_user_id = 9;
|
||||
string reason = 10;
|
||||
string app_code = 11;
|
||||
string withdrawal_application_id = 12;
|
||||
}
|
||||
|
||||
message ReleasePointWithdrawalResponse {
|
||||
string transaction_id = 1;
|
||||
AssetBalance balance = 2;
|
||||
int64 gross_point_amount = 3;
|
||||
int64 fee_point_amount = 4;
|
||||
int64 net_point_amount = 5;
|
||||
int64 points_per_usd = 6;
|
||||
int32 fee_bps = 7;
|
||||
}
|
||||
|
||||
// Resource 是资源库里可配置、可赠送、可被礼物引用的最小资源。
|
||||
message Resource {
|
||||
string app_code = 1;
|
||||
@ -1899,9 +2018,10 @@ message CreditTaskRewardRequest {
|
||||
string task_id = 6;
|
||||
string cycle_key = 7;
|
||||
string reason = 8;
|
||||
string reward_asset_type = 9;
|
||||
}
|
||||
|
||||
// CreditTaskRewardResponse 返回任务奖励入账流水和用户 COIN 账后余额。
|
||||
// CreditTaskRewardResponse 返回任务奖励入账流水和用户奖励资产账后余额。
|
||||
message CreditTaskRewardResponse {
|
||||
string transaction_id = 1;
|
||||
AssetBalance balance = 2;
|
||||
@ -2305,6 +2425,8 @@ service WalletService {
|
||||
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
|
||||
rpc GetActiveHostSalaryPolicy(GetActiveHostSalaryPolicyRequest) returns (GetActiveHostSalaryPolicyResponse);
|
||||
rpc GetHostSalaryProgress(GetHostSalaryProgressRequest) returns (GetHostSalaryProgressResponse);
|
||||
// GetTeamHostSalaryStats 按 Agency 收款人集合聚合主播预收入工资;经理中心团队工资卡片使用。
|
||||
rpc GetTeamHostSalaryStats(GetTeamHostSalaryStatsRequest) returns (GetTeamHostSalaryStatsResponse);
|
||||
rpc AdminCreditAsset(AdminCreditAssetRequest) returns (AdminCreditAssetResponse);
|
||||
rpc AdminCreditCoinSellerStock(AdminCreditCoinSellerStockRequest) returns (AdminCreditCoinSellerStockResponse);
|
||||
rpc TransferCoinFromSeller(TransferCoinFromSellerRequest) returns (TransferCoinFromSellerResponse);
|
||||
@ -2314,6 +2436,9 @@ service WalletService {
|
||||
rpc FreezeSalaryWithdrawal(FreezeSalaryWithdrawalRequest) returns (FreezeSalaryWithdrawalResponse);
|
||||
rpc SettleSalaryWithdrawal(SettleSalaryWithdrawalRequest) returns (SettleSalaryWithdrawalResponse);
|
||||
rpc ReleaseSalaryWithdrawal(ReleaseSalaryWithdrawalRequest) returns (ReleaseSalaryWithdrawalResponse);
|
||||
rpc FreezePointWithdrawal(FreezePointWithdrawalRequest) returns (FreezePointWithdrawalResponse);
|
||||
rpc SettlePointWithdrawal(SettlePointWithdrawalRequest) returns (SettlePointWithdrawalResponse);
|
||||
rpc ReleasePointWithdrawal(ReleasePointWithdrawalRequest) returns (ReleasePointWithdrawalResponse);
|
||||
rpc ListResources(ListResourcesRequest) returns (ListResourcesResponse);
|
||||
rpc GetResource(GetResourceRequest) returns (GetResourceResponse);
|
||||
rpc CreateResource(CreateResourceRequest) returns (ResourceResponse);
|
||||
|
||||
@ -208,6 +208,7 @@ const (
|
||||
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
|
||||
WalletService_GetActiveHostSalaryPolicy_FullMethodName = "/hyapp.wallet.v1.WalletService/GetActiveHostSalaryPolicy"
|
||||
WalletService_GetHostSalaryProgress_FullMethodName = "/hyapp.wallet.v1.WalletService/GetHostSalaryProgress"
|
||||
WalletService_GetTeamHostSalaryStats_FullMethodName = "/hyapp.wallet.v1.WalletService/GetTeamHostSalaryStats"
|
||||
WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset"
|
||||
WalletService_AdminCreditCoinSellerStock_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditCoinSellerStock"
|
||||
WalletService_TransferCoinFromSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferCoinFromSeller"
|
||||
@ -217,6 +218,9 @@ const (
|
||||
WalletService_FreezeSalaryWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/FreezeSalaryWithdrawal"
|
||||
WalletService_SettleSalaryWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/SettleSalaryWithdrawal"
|
||||
WalletService_ReleaseSalaryWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/ReleaseSalaryWithdrawal"
|
||||
WalletService_FreezePointWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/FreezePointWithdrawal"
|
||||
WalletService_SettlePointWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/SettlePointWithdrawal"
|
||||
WalletService_ReleasePointWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/ReleasePointWithdrawal"
|
||||
WalletService_ListResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResources"
|
||||
WalletService_GetResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResource"
|
||||
WalletService_CreateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResource"
|
||||
@ -318,6 +322,8 @@ type WalletServiceClient interface {
|
||||
GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error)
|
||||
GetActiveHostSalaryPolicy(ctx context.Context, in *GetActiveHostSalaryPolicyRequest, opts ...grpc.CallOption) (*GetActiveHostSalaryPolicyResponse, error)
|
||||
GetHostSalaryProgress(ctx context.Context, in *GetHostSalaryProgressRequest, opts ...grpc.CallOption) (*GetHostSalaryProgressResponse, error)
|
||||
// GetTeamHostSalaryStats 按 Agency 收款人集合聚合主播预收入工资;经理中心团队工资卡片使用。
|
||||
GetTeamHostSalaryStats(ctx context.Context, in *GetTeamHostSalaryStatsRequest, opts ...grpc.CallOption) (*GetTeamHostSalaryStatsResponse, error)
|
||||
AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error)
|
||||
AdminCreditCoinSellerStock(ctx context.Context, in *AdminCreditCoinSellerStockRequest, opts ...grpc.CallOption) (*AdminCreditCoinSellerStockResponse, error)
|
||||
TransferCoinFromSeller(ctx context.Context, in *TransferCoinFromSellerRequest, opts ...grpc.CallOption) (*TransferCoinFromSellerResponse, error)
|
||||
@ -327,6 +333,9 @@ type WalletServiceClient interface {
|
||||
FreezeSalaryWithdrawal(ctx context.Context, in *FreezeSalaryWithdrawalRequest, opts ...grpc.CallOption) (*FreezeSalaryWithdrawalResponse, error)
|
||||
SettleSalaryWithdrawal(ctx context.Context, in *SettleSalaryWithdrawalRequest, opts ...grpc.CallOption) (*SettleSalaryWithdrawalResponse, error)
|
||||
ReleaseSalaryWithdrawal(ctx context.Context, in *ReleaseSalaryWithdrawalRequest, opts ...grpc.CallOption) (*ReleaseSalaryWithdrawalResponse, error)
|
||||
FreezePointWithdrawal(ctx context.Context, in *FreezePointWithdrawalRequest, opts ...grpc.CallOption) (*FreezePointWithdrawalResponse, error)
|
||||
SettlePointWithdrawal(ctx context.Context, in *SettlePointWithdrawalRequest, opts ...grpc.CallOption) (*SettlePointWithdrawalResponse, error)
|
||||
ReleasePointWithdrawal(ctx context.Context, in *ReleasePointWithdrawalRequest, opts ...grpc.CallOption) (*ReleasePointWithdrawalResponse, error)
|
||||
ListResources(ctx context.Context, in *ListResourcesRequest, opts ...grpc.CallOption) (*ListResourcesResponse, error)
|
||||
GetResource(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*GetResourceResponse, error)
|
||||
CreateResource(ctx context.Context, in *CreateResourceRequest, opts ...grpc.CallOption) (*ResourceResponse, error)
|
||||
@ -493,6 +502,16 @@ func (c *walletServiceClient) GetHostSalaryProgress(ctx context.Context, in *Get
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) GetTeamHostSalaryStats(ctx context.Context, in *GetTeamHostSalaryStatsRequest, opts ...grpc.CallOption) (*GetTeamHostSalaryStatsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetTeamHostSalaryStatsResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_GetTeamHostSalaryStats_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AdminCreditAssetResponse)
|
||||
@ -583,6 +602,36 @@ func (c *walletServiceClient) ReleaseSalaryWithdrawal(ctx context.Context, in *R
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) FreezePointWithdrawal(ctx context.Context, in *FreezePointWithdrawalRequest, opts ...grpc.CallOption) (*FreezePointWithdrawalResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(FreezePointWithdrawalResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_FreezePointWithdrawal_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) SettlePointWithdrawal(ctx context.Context, in *SettlePointWithdrawalRequest, opts ...grpc.CallOption) (*SettlePointWithdrawalResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SettlePointWithdrawalResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_SettlePointWithdrawal_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) ReleasePointWithdrawal(ctx context.Context, in *ReleasePointWithdrawalRequest, opts ...grpc.CallOption) (*ReleasePointWithdrawalResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ReleasePointWithdrawalResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_ReleasePointWithdrawal_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) ListResources(ctx context.Context, in *ListResourcesRequest, opts ...grpc.CallOption) (*ListResourcesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListResourcesResponse)
|
||||
@ -1456,6 +1505,8 @@ type WalletServiceServer interface {
|
||||
GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error)
|
||||
GetActiveHostSalaryPolicy(context.Context, *GetActiveHostSalaryPolicyRequest) (*GetActiveHostSalaryPolicyResponse, error)
|
||||
GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error)
|
||||
// GetTeamHostSalaryStats 按 Agency 收款人集合聚合主播预收入工资;经理中心团队工资卡片使用。
|
||||
GetTeamHostSalaryStats(context.Context, *GetTeamHostSalaryStatsRequest) (*GetTeamHostSalaryStatsResponse, error)
|
||||
AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error)
|
||||
AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error)
|
||||
TransferCoinFromSeller(context.Context, *TransferCoinFromSellerRequest) (*TransferCoinFromSellerResponse, error)
|
||||
@ -1465,6 +1516,9 @@ type WalletServiceServer interface {
|
||||
FreezeSalaryWithdrawal(context.Context, *FreezeSalaryWithdrawalRequest) (*FreezeSalaryWithdrawalResponse, error)
|
||||
SettleSalaryWithdrawal(context.Context, *SettleSalaryWithdrawalRequest) (*SettleSalaryWithdrawalResponse, error)
|
||||
ReleaseSalaryWithdrawal(context.Context, *ReleaseSalaryWithdrawalRequest) (*ReleaseSalaryWithdrawalResponse, error)
|
||||
FreezePointWithdrawal(context.Context, *FreezePointWithdrawalRequest) (*FreezePointWithdrawalResponse, error)
|
||||
SettlePointWithdrawal(context.Context, *SettlePointWithdrawalRequest) (*SettlePointWithdrawalResponse, error)
|
||||
ReleasePointWithdrawal(context.Context, *ReleasePointWithdrawalRequest) (*ReleasePointWithdrawalResponse, error)
|
||||
ListResources(context.Context, *ListResourcesRequest) (*ListResourcesResponse, error)
|
||||
GetResource(context.Context, *GetResourceRequest) (*GetResourceResponse, error)
|
||||
CreateResource(context.Context, *CreateResourceRequest) (*ResourceResponse, error)
|
||||
@ -1582,6 +1636,9 @@ func (UnimplementedWalletServiceServer) GetActiveHostSalaryPolicy(context.Contex
|
||||
func (UnimplementedWalletServiceServer) GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetHostSalaryProgress not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetTeamHostSalaryStats(context.Context, *GetTeamHostSalaryStatsRequest) (*GetTeamHostSalaryStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetTeamHostSalaryStats not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminCreditAsset not implemented")
|
||||
}
|
||||
@ -1609,6 +1666,15 @@ func (UnimplementedWalletServiceServer) SettleSalaryWithdrawal(context.Context,
|
||||
func (UnimplementedWalletServiceServer) ReleaseSalaryWithdrawal(context.Context, *ReleaseSalaryWithdrawalRequest) (*ReleaseSalaryWithdrawalResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReleaseSalaryWithdrawal not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) FreezePointWithdrawal(context.Context, *FreezePointWithdrawalRequest) (*FreezePointWithdrawalResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method FreezePointWithdrawal not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SettlePointWithdrawal(context.Context, *SettlePointWithdrawalRequest) (*SettlePointWithdrawalResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SettlePointWithdrawal not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ReleasePointWithdrawal(context.Context, *ReleasePointWithdrawalRequest) (*ReleasePointWithdrawalResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReleasePointWithdrawal not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListResources(context.Context, *ListResourcesRequest) (*ListResourcesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListResources not implemented")
|
||||
}
|
||||
@ -2014,6 +2080,24 @@ func _WalletService_GetHostSalaryProgress_Handler(srv interface{}, ctx context.C
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_GetTeamHostSalaryStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetTeamHostSalaryStatsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).GetTeamHostSalaryStats(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_GetTeamHostSalaryStats_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).GetTeamHostSalaryStats(ctx, req.(*GetTeamHostSalaryStatsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_AdminCreditAsset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminCreditAssetRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -2176,6 +2260,60 @@ func _WalletService_ReleaseSalaryWithdrawal_Handler(srv interface{}, ctx context
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_FreezePointWithdrawal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(FreezePointWithdrawalRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).FreezePointWithdrawal(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_FreezePointWithdrawal_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).FreezePointWithdrawal(ctx, req.(*FreezePointWithdrawalRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_SettlePointWithdrawal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SettlePointWithdrawalRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).SettlePointWithdrawal(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_SettlePointWithdrawal_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).SettlePointWithdrawal(ctx, req.(*SettlePointWithdrawalRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_ReleasePointWithdrawal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReleasePointWithdrawalRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).ReleasePointWithdrawal(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_ReleasePointWithdrawal_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).ReleasePointWithdrawal(ctx, req.(*ReleasePointWithdrawalRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_ListResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListResourcesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -3759,6 +3897,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetHostSalaryProgress",
|
||||
Handler: _WalletService_GetHostSalaryProgress_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetTeamHostSalaryStats",
|
||||
Handler: _WalletService_GetTeamHostSalaryStats_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminCreditAsset",
|
||||
Handler: _WalletService_AdminCreditAsset_Handler,
|
||||
@ -3795,6 +3937,18 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ReleaseSalaryWithdrawal",
|
||||
Handler: _WalletService_ReleaseSalaryWithdrawal_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "FreezePointWithdrawal",
|
||||
Handler: _WalletService_FreezePointWithdrawal_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SettlePointWithdrawal",
|
||||
Handler: _WalletService_SettlePointWithdrawal_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ReleasePointWithdrawal",
|
||||
Handler: _WalletService_ReleasePointWithdrawal_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListResources",
|
||||
Handler: _WalletService_ListResources_Handler,
|
||||
|
||||
@ -83,6 +83,7 @@ func allServices() []serviceSpec {
|
||||
appService("user-service", []int{13005, 13105}, "36"),
|
||||
appService("activity-service", []int{13006, 13106}, "35"),
|
||||
appService("wallet-service", []int{13004, 13104}, "33"),
|
||||
appService("lucky-gift-service", []int{13013, 13113}, "31"),
|
||||
appService("robot-service", []int{13011, 13111}, "93"),
|
||||
appService("game-service", []int{13008, 13108}, "32"),
|
||||
appService("room-service", []int{13001, 13101}, "34"),
|
||||
@ -90,6 +91,13 @@ func allServices() []serviceSpec {
|
||||
appService("cron-service", []int{13007, 13107}, "95"),
|
||||
appService("gateway-service", []int{13000}, "92"),
|
||||
appService("statistics-service", []int{13010, 13110}, "94"),
|
||||
{
|
||||
Name: "luck-gateway",
|
||||
Dir: "server/luck-gateway",
|
||||
Args: []string{"run", "./cmd/server"},
|
||||
Ports: []int{13014},
|
||||
Color: "36",
|
||||
},
|
||||
{
|
||||
Name: "admin",
|
||||
Dir: "server/admin",
|
||||
@ -101,15 +109,24 @@ func allServices() []serviceSpec {
|
||||
}
|
||||
|
||||
func appService(name string, ports []int, color string) serviceSpec {
|
||||
args := []string{"run", "./services/" + name + "/cmd/server", "-config", ""}
|
||||
if devRunMQEnabled() {
|
||||
args = []string{"run", "./services/" + name + "/cmd/server", "-config", "services/" + name + "/configs/config.yaml"}
|
||||
}
|
||||
return serviceSpec{
|
||||
Name: name,
|
||||
Dir: ".",
|
||||
Args: []string{"run", "./services/" + name + "/cmd/server", "-config", "services/" + name + "/configs/config.yaml"},
|
||||
Args: args,
|
||||
Ports: ports,
|
||||
Color: color,
|
||||
}
|
||||
}
|
||||
|
||||
func devRunMQEnabled() bool {
|
||||
value := strings.ToLower(strings.TrimSpace(os.Getenv("DEV_RUN_MQ")))
|
||||
return value == "1" || value == "true" || value == "yes"
|
||||
}
|
||||
|
||||
func selectServices(raw string) ([]serviceSpec, error) {
|
||||
all := allServices()
|
||||
if strings.TrimSpace(raw) == "" || strings.TrimSpace(raw) == "all" {
|
||||
@ -130,6 +147,11 @@ func selectServices(raw string) ([]serviceSpec, error) {
|
||||
"us": "user-service",
|
||||
"activity": "activity-service",
|
||||
"as": "activity-service",
|
||||
"lucky": "lucky-gift-service",
|
||||
"lucky-gift": "lucky-gift-service",
|
||||
"lgs": "lucky-gift-service",
|
||||
"luckygift": "lucky-gift-service",
|
||||
"lg": "luck-gateway",
|
||||
"cron": "cron-service",
|
||||
"cs": "cron-service",
|
||||
"robot": "robot-service",
|
||||
|
||||
@ -49,6 +49,8 @@ services:
|
||||
condition: service_healthy
|
||||
game-service:
|
||||
condition: service_healthy
|
||||
lucky-gift-service:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rocketmq-broker:
|
||||
@ -78,6 +80,8 @@ services:
|
||||
condition: service_healthy
|
||||
wallet-service:
|
||||
condition: service_healthy
|
||||
lucky-gift-service:
|
||||
condition: service_healthy
|
||||
rocketmq-broker:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
@ -166,6 +170,51 @@ services:
|
||||
retries: 20
|
||||
start_period: 10s
|
||||
|
||||
lucky-gift-service:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: services/lucky-gift-service/Dockerfile
|
||||
args: *go-build-args
|
||||
container_name: lucky-gift-service
|
||||
environment:
|
||||
TZ: UTC
|
||||
ports:
|
||||
- "13013:13013"
|
||||
- "13113:13113"
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
wallet-service:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:13113/healthz/ready >/dev/null"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
start_period: 10s
|
||||
|
||||
luck-gateway:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: server/luck-gateway/Dockerfile
|
||||
args: *go-build-args
|
||||
container_name: luck-gateway
|
||||
environment:
|
||||
TZ: UTC
|
||||
LUCKY_GIFT_GRPC_ADDR: "lucky-gift-service:13013"
|
||||
LUCK_GATEWAY_ALLOWED_APPS: "aslan,yumi"
|
||||
ports:
|
||||
- "13014:13014"
|
||||
depends_on:
|
||||
lucky-gift-service:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:13014/healthz/ready >/dev/null"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
start_period: 10s
|
||||
|
||||
cron-service:
|
||||
build:
|
||||
context: .
|
||||
@ -315,6 +364,7 @@ services:
|
||||
- ./tmp/mysql/initdb/005_utf8mb4_chinese_support.sql:/docker-entrypoint-initdb.d/005_utf8mb4_chinese_support.sql:ro
|
||||
- ./tmp/mysql/initdb/006_admin_database.sql:/docker-entrypoint-initdb.d/006_admin_database.sql:ro
|
||||
- ./tmp/mysql/initdb/007_resource_group_wallet_asset_items.sql:/docker-entrypoint-initdb.d/007_resource_group_wallet_asset_items.sql:ro
|
||||
- ./services/lucky-gift-service/deploy/mysql/initdb/001_lucky_gift_service.sql:/docker-entrypoint-initdb.d/008_lucky_gift_service.sql:ro
|
||||
- ./services/cron-service/deploy/mysql/initdb/001_cron_service.sql:/docker-entrypoint-initdb.d/009_cron_service.sql:ro
|
||||
- ./services/game-service/deploy/mysql/initdb/001_game_service.sql:/docker-entrypoint-initdb.d/010_game_service.sql:ro
|
||||
- ./services/game-service/deploy/mysql/initdata/001_self_games.sql:/docker-entrypoint-initdb.d/010_game_service_data.sql:ro
|
||||
|
||||
@ -7,7 +7,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-root}"
|
||||
DEPLOY_PLATFORM_ROOT="${DEPLOY_PLATFORM_ROOT:-${PROJECT_ROOT}/../deploy-platform}"
|
||||
LOCAL_INITDB_DIR="${PROJECT_ROOT}/tmp/mysql/initdb"
|
||||
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
@ -16,15 +16,16 @@ SQL_FILES=(
|
||||
"services/user-service/deploy/mysql/initdb/001_user_service.sql"
|
||||
"services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql"
|
||||
"services/activity-service/deploy/mysql/initdb/001_activity_service.sql"
|
||||
"${DEPLOY_PLATFORM_ROOT}/deploy/mysql/initdb/005_utf8mb4_chinese_support.sql"
|
||||
"${DEPLOY_PLATFORM_ROOT}/deploy/mysql/initdb/006_admin_database.sql"
|
||||
"${DEPLOY_PLATFORM_ROOT}/deploy/mysql/initdb/007_resource_group_wallet_asset_items.sql"
|
||||
"${LOCAL_INITDB_DIR}/005_utf8mb4_chinese_support.sql"
|
||||
"${LOCAL_INITDB_DIR}/006_admin_database.sql"
|
||||
"${LOCAL_INITDB_DIR}/007_resource_group_wallet_asset_items.sql"
|
||||
"services/lucky-gift-service/deploy/mysql/initdb/001_lucky_gift_service.sql"
|
||||
"services/cron-service/deploy/mysql/initdb/001_cron_service.sql"
|
||||
"services/game-service/deploy/mysql/initdb/001_game_service.sql"
|
||||
"services/robot-service/deploy/mysql/initdb/001_robot_service.sql"
|
||||
"services/notice-service/deploy/mysql/initdb/001_notice_service.sql"
|
||||
"services/statistics-service/deploy/mysql/initdb/001_statistics_service.sql"
|
||||
"${DEPLOY_PLATFORM_ROOT}/deploy/mysql/initdb/999_local_grants.sql"
|
||||
"${LOCAL_INITDB_DIR}/999_local_grants.sql"
|
||||
)
|
||||
|
||||
# Keep MySQL as the only required dependency for this script. Business services
|
||||
|
||||
@ -12,12 +12,34 @@ cd "${PROJECT_ROOT}"
|
||||
"${SCRIPT_DIR}/prepare-local-rocketmq-config.sh" >/dev/null
|
||||
export ROCKETMQ_BROKER_CONFIG="${PROJECT_ROOT}/tmp/rocketmq/broker.local.conf"
|
||||
"${SCRIPT_DIR}/resolve-compose-container-conflicts.sh" rocketmq-namesrv rocketmq-broker
|
||||
docker compose up -d rocketmq-namesrv >/dev/null
|
||||
docker compose up -d --force-recreate rocketmq-broker >/dev/null
|
||||
running_services="$(docker compose ps --services --status running 2>/dev/null || true)"
|
||||
namesrv_was_running=0
|
||||
broker_was_running=0
|
||||
if grep -qx "rocketmq-namesrv" <<<"${running_services}"; then
|
||||
namesrv_was_running=1
|
||||
else
|
||||
docker compose up -d rocketmq-namesrv >/dev/null
|
||||
fi
|
||||
if grep -qx "rocketmq-broker" <<<"${running_services}"; then
|
||||
broker_was_running=1
|
||||
fi
|
||||
|
||||
# `make run` is invoked repeatedly during local development. Recreating the
|
||||
# broker on every run forces RocketMQ through store recovery, broker
|
||||
# registration, and route propagation even when the same container is already
|
||||
# healthy. Keep the existing broker hot by default, and reserve a forced
|
||||
# recreate for explicit config refreshes via ROCKETMQ_FORCE_RECREATE=1.
|
||||
if [[ "${ROCKETMQ_FORCE_RECREATE:-0}" == "1" ]]; then
|
||||
docker compose up -d --force-recreate rocketmq-broker >/dev/null
|
||||
broker_was_running=0
|
||||
elif [[ "${broker_was_running}" != "1" ]]; then
|
||||
docker compose up -d rocketmq-broker >/dev/null
|
||||
fi
|
||||
|
||||
MQADMIN="/home/rocketmq/rocketmq-5.3.1/bin/mqadmin"
|
||||
NAMESRV="rocketmq-namesrv:9876"
|
||||
CLUSTER="hyapp-local"
|
||||
TOPIC_STAMP="${PROJECT_ROOT}/tmp/rocketmq/topics.applied"
|
||||
TOPICS=(
|
||||
"hyapp_wallet_outbox"
|
||||
"hyapp_wallet_realtime_outbox"
|
||||
@ -28,22 +50,77 @@ TOPICS=(
|
||||
"hyapp_game_outbox"
|
||||
)
|
||||
|
||||
topic_fingerprint() {
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
printf '%s\n' "${TOPICS[@]}" | shasum -a 256 | awk '{print $1}'
|
||||
return
|
||||
fi
|
||||
printf '%s\n' "${TOPICS[@]}" | sha256sum | awk '{print $1}'
|
||||
}
|
||||
|
||||
write_topic_stamp() {
|
||||
local broker_container_id="$1"
|
||||
local fingerprint="$2"
|
||||
|
||||
# The stamp is only a local startup accelerator. It is scoped to the Docker
|
||||
# container id plus the exact topic list, so replacing the broker or adding a
|
||||
# new owner-outbox topic automatically falls back to mqadmin verification.
|
||||
{
|
||||
printf 'broker_container_id=%s\n' "${broker_container_id}"
|
||||
printf 'topics_sha256=%s\n' "${fingerprint}"
|
||||
for topic in "${TOPICS[@]}"; do
|
||||
printf 'topic=%s\n' "${topic}"
|
||||
done
|
||||
} >"${TOPIC_STAMP}"
|
||||
}
|
||||
|
||||
broker_container_id="$(docker compose ps -q rocketmq-broker)"
|
||||
topics_sha256="$(topic_fingerprint)"
|
||||
if [[ "${namesrv_was_running}" == "1" && "${broker_was_running}" == "1" && -n "${broker_container_id}" && -f "${TOPIC_STAMP}" ]]; then
|
||||
if grep -Fxq "broker_container_id=${broker_container_id}" "${TOPIC_STAMP}" && grep -Fxq "topics_sha256=${topics_sha256}" "${TOPIC_STAMP}"; then
|
||||
printf 'rocketmq topics already applied\n'
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# The broker process can accept a container start before it has registered with
|
||||
# NameServer. Wait for the cluster view so updateTopic targets the real broker.
|
||||
# NameServer. Wait for the cluster view so topic checks and updateTopic target
|
||||
# the real broker instead of racing the route table.
|
||||
cluster_view=""
|
||||
for _ in {1..60}; do
|
||||
if docker compose exec -T rocketmq-broker "${MQADMIN}" clusterList -n "${NAMESRV}" 2>/dev/null | grep -q "${CLUSTER}"; then
|
||||
if cluster_view="$(docker compose exec -T rocketmq-broker "${MQADMIN}" clusterList -n "${NAMESRV}" 2>/dev/null)" && grep -q "${CLUSTER}" <<<"${cluster_view}"; then
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! docker compose exec -T rocketmq-broker "${MQADMIN}" clusterList -n "${NAMESRV}" 2>/dev/null | grep -q "${CLUSTER}"; then
|
||||
if ! grep -q "${CLUSTER}" <<<"${cluster_view}"; then
|
||||
printf 'rocketmq broker did not register in cluster %s before topic init\n' "${CLUSTER}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Topic creation is idempotent, but each updateTopic call shells into the
|
||||
# broker container and blocks local startup. Query once and only create missing
|
||||
# topics; a warm `make run` then pays one cheap route read instead of seven
|
||||
# management writes.
|
||||
topic_list="$(docker compose exec -T rocketmq-broker "${MQADMIN}" topicList -n "${NAMESRV}" 2>/dev/null || true)"
|
||||
missing_topics=()
|
||||
for topic in "${TOPICS[@]}"; do
|
||||
if grep -Fxq "${topic}" <<<"${topic_list}"; then
|
||||
continue
|
||||
fi
|
||||
missing_topics+=("${topic}")
|
||||
done
|
||||
|
||||
if [[ ${#missing_topics[@]} -eq 0 ]]; then
|
||||
write_topic_stamp "${broker_container_id}" "${topics_sha256}"
|
||||
printf 'rocketmq topics already applied\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for topic in "${missing_topics[@]}"; do
|
||||
printf 'applying rocketmq topic: %s\n' "${topic}"
|
||||
docker compose exec -T rocketmq-broker "${MQADMIN}" updateTopic -n "${NAMESRV}" -c "${CLUSTER}" -t "${topic}" -r 8 -w 8 >/dev/null
|
||||
done
|
||||
write_topic_stamp "${broker_container_id}" "${topics_sha256}"
|
||||
|
||||
@ -26,10 +26,25 @@ for sql_name in "${OPTIONAL_SQL_FILES[@]}"; do
|
||||
|
||||
if [[ -f "${source_file}" ]]; then
|
||||
cp "${source_file}" "${target_file}"
|
||||
if [[ "${sql_name}" == "999_local_grants.sql" ]]; then
|
||||
# hyapp-server 新增 owner 库时,deploy-platform 的本地授权文件可能还没同步。
|
||||
# 本地启动必须以当前仓库服务清单为准,否则新服务库已经创建但 hyapp 用户无法连接。
|
||||
cat >> "${target_file}" <<'EOF'
|
||||
GRANT ALL PRIVILEGES ON hyapp_lucky_gift.* TO 'hyapp'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
fi
|
||||
printf 'prepared optional mysql init file: %s\n' "${source_file}"
|
||||
continue
|
||||
fi
|
||||
|
||||
printf -- '-- optional mysql init file is absent locally: %s\n' "${source_file}" > "${target_file}"
|
||||
if [[ "${sql_name}" == "999_local_grants.sql" ]]; then
|
||||
cat >> "${target_file}" <<'EOF'
|
||||
CREATE USER IF NOT EXISTS 'hyapp'@'%' IDENTIFIED BY 'hyapp';
|
||||
GRANT ALL PRIVILEGES ON hyapp_lucky_gift.* TO 'hyapp'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
fi
|
||||
printf 'prepared empty optional mysql init file: %s\n' "${source_file}"
|
||||
done
|
||||
|
||||
@ -97,13 +97,27 @@ 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 "lucky-gift-service" "gRPC" "grpc" "lucky-gift-service" "13013" "13013"
|
||||
print_row "luck-gateway" "HTTP JSON" "http" "luck-gateway" "13014" "13014"
|
||||
print_row "admin" "HTTP JSON" "http" "admin" "13100" "13100"
|
||||
print_row "cron-service" "gRPC" "grpc" "cron-service" "13007" "13007"
|
||||
print_row "robot-service" "gRPC" "grpc" "robot-service" "13011" "13011"
|
||||
print_row "game-service" "gRPC" "grpc" "game-service" "13008" "13008"
|
||||
print_row "notice-service" "gRPC" "grpc" "notice-service" "13009" "13009"
|
||||
print_row "statistics-service" "Health" "http" "statistics-service" "13110" "13110" "/healthz/ready"
|
||||
print_row "rocketmq-namesrv" "NameServer" "tcp" "rocketmq-namesrv" "9876" "9876"
|
||||
print_row "rocketmq-broker" "Broker" "tcp" "rocketmq-broker" "10911" "10911"
|
||||
|
||||
show_mq_addresses() {
|
||||
if [[ -n "${DEV_RUN_MQ+x}" ]]; then
|
||||
[[ "${DEV_RUN_MQ}" == "1" ]]
|
||||
return
|
||||
fi
|
||||
docker compose ps --status running --services 2>/dev/null | grep -Eq '^(rocketmq-namesrv|rocketmq-broker)$'
|
||||
}
|
||||
|
||||
if show_mq_addresses; then
|
||||
print_row "rocketmq-namesrv" "NameServer" "tcp" "rocketmq-namesrv" "9876" "9876"
|
||||
print_row "rocketmq-broker" "Broker" "tcp" "rocketmq-broker" "10911" "10911"
|
||||
fi
|
||||
print_row "mysql" "MySQL" "tcp" "mysql" "3306" "23306"
|
||||
print_row "redis" "Redis" "tcp" "redis" "6379" "13379"
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
dingtalkclient "hyapp-admin-server/internal/integration/dingtalk"
|
||||
"hyapp-admin-server/internal/integration/gameclient"
|
||||
"hyapp-admin-server/internal/integration/googleorders"
|
||||
"hyapp-admin-server/internal/integration/luckygiftadmin"
|
||||
"hyapp-admin-server/internal/integration/robotclient"
|
||||
"hyapp-admin-server/internal/integration/roomclient"
|
||||
"hyapp-admin-server/internal/integration/userclient"
|
||||
@ -60,7 +61,9 @@ import (
|
||||
levelconfigmodule "hyapp-admin-server/internal/modules/levelconfig"
|
||||
luckygiftmodule "hyapp-admin-server/internal/modules/luckygift"
|
||||
menumodule "hyapp-admin-server/internal/modules/menu"
|
||||
opscentermodule "hyapp-admin-server/internal/modules/opscenter"
|
||||
paymentmodule "hyapp-admin-server/internal/modules/payment"
|
||||
policyconfigmodule "hyapp-admin-server/internal/modules/policyconfig"
|
||||
prettyidmodule "hyapp-admin-server/internal/modules/prettyid"
|
||||
rbacmodule "hyapp-admin-server/internal/modules/rbac"
|
||||
redpacketmodule "hyapp-admin-server/internal/modules/redpacket"
|
||||
@ -251,6 +254,12 @@ func main() {
|
||||
}
|
||||
defer activityConn.Close()
|
||||
|
||||
luckyGiftConn, err := dialBackendGRPC(cfg.LuckyGiftService.Addr)
|
||||
if err != nil {
|
||||
fatalRuntime("connect_lucky_gift_service_failed", err)
|
||||
}
|
||||
defer luckyGiftConn.Close()
|
||||
|
||||
gameConn, err := dialBackendGRPC(cfg.GameService.Addr)
|
||||
if err != nil {
|
||||
fatalRuntime("connect_game_service_failed", err)
|
||||
@ -308,14 +317,16 @@ func main() {
|
||||
)
|
||||
// 社交 BI 与大屏共享同一个 dashboard 服务实例,保证外部源路由和统计口径一致。
|
||||
dashboardService := dashboardmodule.NewService(store, cfg, userclient.NewGRPC(userConn), dashboardmodule.WithExternalDashboardSources(dashboardExternalSources...))
|
||||
appRegistryService := appregistrymodule.NewService(userDB)
|
||||
databiService := databimodule.NewService(
|
||||
store,
|
||||
dashboardService,
|
||||
appregistrymodule.NewService(userDB),
|
||||
appRegistryService,
|
||||
userclient.NewGRPC(userConn),
|
||||
databimodule.WithLegacyApps(databiLegacyApps(cfg.DashboardExternalSources)...),
|
||||
databimodule.WithLegacyRegionCatalogs(databiLegacyRegionCatalogs(moneyRegionSources)...),
|
||||
)
|
||||
luckyGiftHandler := luckygiftmodule.New(luckygiftadmin.NewGRPC(luckyGiftConn), cfg.LuckyGiftService.RequestTimeout, auditHandler)
|
||||
handlers := router.Handlers{
|
||||
Audit: auditHandler,
|
||||
Auth: authmodule.New(store, auth, auditHandler, cfg),
|
||||
@ -354,9 +365,10 @@ func main() {
|
||||
InviteActivityReward: inviteactivityrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||
Job: jobmodule.New(store, cfg, auditHandler),
|
||||
LevelConfig: levelconfigmodule.New(activityclient.NewGRPC(activityConn), walletclient.NewGRPC(walletConn), auditHandler),
|
||||
LuckyGift: luckygiftmodule.New(activityclient.NewGRPC(activityConn), cfg.ActivityService.RequestTimeout, auditHandler),
|
||||
Menu: menumodule.New(store, auditHandler),
|
||||
OpsCenter: opscentermodule.New(appRegistryService, luckyGiftHandler),
|
||||
Payment: paymentmodule.New(walletclient.NewGRPC(walletConn), userDB, walletDB, store, auditHandler, paymentmodule.WithMoneyRegionSources(moneyRegionSources...), paymentmodule.WithRechargeBillSources(financeBillSources...)),
|
||||
PolicyConfig: policyconfigmodule.New(sqlDB, walletDB, userDB, activityclient.NewGRPC(activityConn), auditHandler),
|
||||
PrettyID: prettyidmodule.New(userclient.NewGRPC(userConn), auditHandler),
|
||||
RBAC: rbacmodule.New(store, auditHandler),
|
||||
RedPacket: redpacketmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
|
||||
|
||||
@ -94,6 +94,9 @@ robot_service:
|
||||
activity_service:
|
||||
addr: "10.2.1.16:13006"
|
||||
request_timeout: "3s"
|
||||
lucky_gift_service:
|
||||
addr: "lucky-gift-service.internal:13013"
|
||||
request_timeout: "3s"
|
||||
game_service:
|
||||
addr: "10.2.1.16:13008"
|
||||
request_timeout: "3s"
|
||||
|
||||
@ -93,6 +93,9 @@ robot_service:
|
||||
activity_service:
|
||||
addr: "127.0.0.1:13006"
|
||||
request_timeout: "3s"
|
||||
lucky_gift_service:
|
||||
addr: "127.0.0.1:13013"
|
||||
request_timeout: "3s"
|
||||
game_service:
|
||||
addr: "127.0.0.1:13008"
|
||||
request_timeout: "3s"
|
||||
|
||||
@ -42,6 +42,7 @@ type Config struct {
|
||||
RoomService RoomServiceConfig `yaml:"room_service"`
|
||||
RobotService RobotServiceConfig `yaml:"robot_service"`
|
||||
ActivityService ActivityServiceConfig `yaml:"activity_service"`
|
||||
LuckyGiftService LuckyGiftServiceConfig `yaml:"lucky_gift_service"`
|
||||
GameService GameServiceConfig `yaml:"game_service"`
|
||||
StatisticsService StatisticsServiceConfig `yaml:"statistics_service"`
|
||||
DashboardExternalSources []DashboardExternalSourceConfig `yaml:"dashboard_external_sources"`
|
||||
@ -80,6 +81,11 @@ type ActivityServiceConfig struct {
|
||||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||||
}
|
||||
|
||||
type LuckyGiftServiceConfig struct {
|
||||
Addr string `yaml:"addr"`
|
||||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||||
}
|
||||
|
||||
type GameServiceConfig struct {
|
||||
Addr string `yaml:"addr"`
|
||||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||||
@ -300,6 +306,10 @@ func Default() Config {
|
||||
Addr: "127.0.0.1:13006",
|
||||
RequestTimeout: 3 * time.Second,
|
||||
},
|
||||
LuckyGiftService: LuckyGiftServiceConfig{
|
||||
Addr: "127.0.0.1:13013",
|
||||
RequestTimeout: 3 * time.Second,
|
||||
},
|
||||
GameService: GameServiceConfig{
|
||||
Addr: "127.0.0.1:13008",
|
||||
RequestTimeout: 3 * time.Second,
|
||||
@ -463,6 +473,13 @@ func (cfg *Config) Normalize() {
|
||||
if cfg.ActivityService.RequestTimeout <= 0 {
|
||||
cfg.ActivityService.RequestTimeout = 3 * time.Second
|
||||
}
|
||||
cfg.LuckyGiftService.Addr = strings.TrimSpace(cfg.LuckyGiftService.Addr)
|
||||
if cfg.LuckyGiftService.Addr == "" {
|
||||
cfg.LuckyGiftService.Addr = "127.0.0.1:13013"
|
||||
}
|
||||
if cfg.LuckyGiftService.RequestTimeout <= 0 {
|
||||
cfg.LuckyGiftService.RequestTimeout = 3 * time.Second
|
||||
}
|
||||
cfg.GameService.Addr = strings.TrimSpace(cfg.GameService.Addr)
|
||||
if cfg.GameService.Addr == "" {
|
||||
cfg.GameService.Addr = "127.0.0.1:13008"
|
||||
@ -707,6 +724,12 @@ func (cfg Config) Validate() error {
|
||||
if cfg.ActivityService.RequestTimeout <= 0 {
|
||||
return errors.New("activity_service.request_timeout must be greater than 0")
|
||||
}
|
||||
if strings.TrimSpace(cfg.LuckyGiftService.Addr) == "" {
|
||||
return errors.New("lucky_gift_service.addr is required")
|
||||
}
|
||||
if cfg.LuckyGiftService.RequestTimeout <= 0 {
|
||||
return errors.New("lucky_gift_service.request_timeout must be greater than 0")
|
||||
}
|
||||
if strings.TrimSpace(cfg.GameService.Addr) == "" {
|
||||
return errors.New("game_service.addr is required")
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ 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)
|
||||
PublishTaskRewardPolicy(ctx context.Context, req *activityv1.PublishTaskRewardPolicyRequest) (*activityv1.PublishTaskRewardPolicyResponse, error)
|
||||
ListAchievementDefinitions(ctx context.Context, req *activityv1.ListAchievementsRequest) (*activityv1.ListAchievementsResponse, error)
|
||||
UpsertAchievementDefinition(ctx context.Context, req *activityv1.UpsertAchievementDefinitionRequest) (*activityv1.UpsertAchievementDefinitionResponse, error)
|
||||
DeleteAchievementDefinition(ctx context.Context, req *activityv1.DeleteAchievementDefinitionRequest) (*activityv1.DeleteAchievementDefinitionResponse, error)
|
||||
@ -53,11 +54,6 @@ type Client interface {
|
||||
GetCPWeeklyRankConfig(ctx context.Context, req *activityv1.GetCPWeeklyRankConfigRequest) (*activityv1.GetCPWeeklyRankConfigResponse, error)
|
||||
UpdateCPWeeklyRankConfig(ctx context.Context, req *activityv1.UpdateCPWeeklyRankConfigRequest) (*activityv1.UpdateCPWeeklyRankConfigResponse, error)
|
||||
ListCPWeeklyRankSettlements(ctx context.Context, req *activityv1.ListCPWeeklyRankSettlementsRequest) (*activityv1.ListCPWeeklyRankSettlementsResponse, error)
|
||||
GetLuckyGiftConfig(ctx context.Context, req *activityv1.GetLuckyGiftConfigRequest) (*activityv1.GetLuckyGiftConfigResponse, error)
|
||||
UpsertLuckyGiftConfig(ctx context.Context, req *activityv1.UpsertLuckyGiftConfigRequest) (*activityv1.UpsertLuckyGiftConfigResponse, error)
|
||||
ListLuckyGiftConfigs(ctx context.Context, req *activityv1.ListLuckyGiftConfigsRequest) (*activityv1.ListLuckyGiftConfigsResponse, error)
|
||||
ListLuckyGiftDraws(ctx context.Context, req *activityv1.ListLuckyGiftDrawsRequest) (*activityv1.ListLuckyGiftDrawsResponse, error)
|
||||
GetLuckyGiftDrawSummary(ctx context.Context, req *activityv1.GetLuckyGiftDrawSummaryRequest) (*activityv1.GetLuckyGiftDrawSummaryResponse, error)
|
||||
GetWheelConfig(ctx context.Context, req *activityv1.GetWheelConfigRequest) (*activityv1.GetWheelConfigResponse, error)
|
||||
UpsertWheelConfig(ctx context.Context, req *activityv1.UpsertWheelConfigRequest) (*activityv1.UpsertWheelConfigResponse, error)
|
||||
ListWheelDraws(ctx context.Context, req *activityv1.ListWheelDrawsRequest) (*activityv1.ListWheelDrawsResponse, error)
|
||||
@ -84,7 +80,6 @@ type GRPCClient struct {
|
||||
weeklyStarClient activityv1.AdminWeeklyStarServiceClient
|
||||
agencyOpeningClient activityv1.AdminAgencyOpeningServiceClient
|
||||
cpWeeklyRankClient activityv1.AdminCPWeeklyRankServiceClient
|
||||
luckyGiftClient activityv1.AdminLuckyGiftServiceClient
|
||||
wheelClient activityv1.AdminWheelServiceClient
|
||||
broadcastClient activityv1.BroadcastServiceClient
|
||||
levelClient activityv1.GrowthLevelServiceClient
|
||||
@ -105,7 +100,6 @@ func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
||||
weeklyStarClient: activityv1.NewAdminWeeklyStarServiceClient(conn),
|
||||
agencyOpeningClient: activityv1.NewAdminAgencyOpeningServiceClient(conn),
|
||||
cpWeeklyRankClient: activityv1.NewAdminCPWeeklyRankServiceClient(conn),
|
||||
luckyGiftClient: activityv1.NewAdminLuckyGiftServiceClient(conn),
|
||||
wheelClient: activityv1.NewAdminWheelServiceClient(conn),
|
||||
broadcastClient: activityv1.NewBroadcastServiceClient(conn),
|
||||
levelClient: activityv1.NewGrowthLevelServiceClient(conn),
|
||||
@ -126,6 +120,10 @@ func (c *GRPCClient) SetTaskDefinitionStatus(ctx context.Context, req *activityv
|
||||
return c.taskClient.SetTaskDefinitionStatus(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) PublishTaskRewardPolicy(ctx context.Context, req *activityv1.PublishTaskRewardPolicyRequest) (*activityv1.PublishTaskRewardPolicyResponse, error) {
|
||||
return c.taskClient.PublishTaskRewardPolicy(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListAchievementDefinitions(ctx context.Context, req *activityv1.ListAchievementsRequest) (*activityv1.ListAchievementsResponse, error) {
|
||||
return c.achievementClient.ListAchievementDefinitions(ctx, req)
|
||||
}
|
||||
@ -286,26 +284,6 @@ func (c *GRPCClient) ListCPWeeklyRankSettlements(ctx context.Context, req *activ
|
||||
return c.cpWeeklyRankClient.ListCPWeeklyRankSettlements(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetLuckyGiftConfig(ctx context.Context, req *activityv1.GetLuckyGiftConfigRequest) (*activityv1.GetLuckyGiftConfigResponse, error) {
|
||||
return c.luckyGiftClient.GetLuckyGiftConfig(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UpsertLuckyGiftConfig(ctx context.Context, req *activityv1.UpsertLuckyGiftConfigRequest) (*activityv1.UpsertLuckyGiftConfigResponse, error) {
|
||||
return c.luckyGiftClient.UpsertLuckyGiftConfig(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListLuckyGiftConfigs(ctx context.Context, req *activityv1.ListLuckyGiftConfigsRequest) (*activityv1.ListLuckyGiftConfigsResponse, error) {
|
||||
return c.luckyGiftClient.ListLuckyGiftConfigs(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListLuckyGiftDraws(ctx context.Context, req *activityv1.ListLuckyGiftDrawsRequest) (*activityv1.ListLuckyGiftDrawsResponse, error) {
|
||||
return c.luckyGiftClient.ListLuckyGiftDraws(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetLuckyGiftDrawSummary(ctx context.Context, req *activityv1.GetLuckyGiftDrawSummaryRequest) (*activityv1.GetLuckyGiftDrawSummaryResponse, error) {
|
||||
return c.luckyGiftClient.GetLuckyGiftDrawSummary(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetWheelConfig(ctx context.Context, req *activityv1.GetWheelConfigRequest) (*activityv1.GetWheelConfigResponse, error) {
|
||||
return c.wheelClient.GetWheelConfig(ctx, req)
|
||||
}
|
||||
|
||||
52
server/admin/internal/integration/luckygiftadmin/client.go
Normal file
52
server/admin/internal/integration/luckygiftadmin/client.go
Normal file
@ -0,0 +1,52 @@
|
||||
package luckygiftadmin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Client 是 admin-server 访问幸运礼物 owner 服务的最小依赖面。
|
||||
// 后台只通过 AdminLuckyGiftService 读写规则和审计记录,避免重新把幸运礼物表结构泄漏进 admin-server。
|
||||
type Client interface {
|
||||
GetLuckyGiftConfig(ctx context.Context, req *luckygiftv1.GetLuckyGiftConfigRequest) (*luckygiftv1.GetLuckyGiftConfigResponse, error)
|
||||
UpsertLuckyGiftConfig(ctx context.Context, req *luckygiftv1.UpsertLuckyGiftConfigRequest) (*luckygiftv1.UpsertLuckyGiftConfigResponse, error)
|
||||
ListLuckyGiftConfigs(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigsRequest) (*luckygiftv1.ListLuckyGiftConfigsResponse, error)
|
||||
ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error)
|
||||
GetLuckyGiftDrawSummary(ctx context.Context, req *luckygiftv1.GetLuckyGiftDrawSummaryRequest) (*luckygiftv1.GetLuckyGiftDrawSummaryResponse, error)
|
||||
ListLuckyGiftPoolBalances(ctx context.Context, req *luckygiftv1.ListLuckyGiftPoolBalancesRequest) (*luckygiftv1.ListLuckyGiftPoolBalancesResponse, error)
|
||||
}
|
||||
|
||||
type GRPCClient struct {
|
||||
client luckygiftv1.AdminLuckyGiftServiceClient
|
||||
}
|
||||
|
||||
func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
||||
return &GRPCClient{client: luckygiftv1.NewAdminLuckyGiftServiceClient(conn)}
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetLuckyGiftConfig(ctx context.Context, req *luckygiftv1.GetLuckyGiftConfigRequest) (*luckygiftv1.GetLuckyGiftConfigResponse, error) {
|
||||
return c.client.GetLuckyGiftConfig(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UpsertLuckyGiftConfig(ctx context.Context, req *luckygiftv1.UpsertLuckyGiftConfigRequest) (*luckygiftv1.UpsertLuckyGiftConfigResponse, error) {
|
||||
return c.client.UpsertLuckyGiftConfig(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListLuckyGiftConfigs(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigsRequest) (*luckygiftv1.ListLuckyGiftConfigsResponse, error) {
|
||||
return c.client.ListLuckyGiftConfigs(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error) {
|
||||
return c.client.ListLuckyGiftDraws(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetLuckyGiftDrawSummary(ctx context.Context, req *luckygiftv1.GetLuckyGiftDrawSummaryRequest) (*luckygiftv1.GetLuckyGiftDrawSummaryResponse, error) {
|
||||
return c.client.GetLuckyGiftDrawSummary(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListLuckyGiftPoolBalances(ctx context.Context, req *luckygiftv1.ListLuckyGiftPoolBalancesRequest) (*luckygiftv1.ListLuckyGiftPoolBalancesResponse, error) {
|
||||
return c.client.ListLuckyGiftPoolBalances(ctx, req)
|
||||
}
|
||||
@ -209,6 +209,14 @@ func (c *GRPCClient) ReleaseSalaryWithdrawal(ctx context.Context, req *walletv1.
|
||||
return c.client.ReleaseSalaryWithdrawal(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) SettlePointWithdrawal(ctx context.Context, req *walletv1.SettlePointWithdrawalRequest) (*walletv1.SettlePointWithdrawalResponse, error) {
|
||||
return c.client.SettlePointWithdrawal(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ReleasePointWithdrawal(ctx context.Context, req *walletv1.ReleasePointWithdrawalRequest) (*walletv1.ReleasePointWithdrawalResponse, error) {
|
||||
return c.client.ReleasePointWithdrawal(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListRechargeBills(ctx context.Context, req *walletv1.ListRechargeBillsRequest) (*walletv1.ListRechargeBillsResponse, error) {
|
||||
return c.client.ListRechargeBills(ctx, req)
|
||||
}
|
||||
|
||||
@ -237,6 +237,100 @@ func (AppExploreTab) TableName() string {
|
||||
return "admin_app_explore_tabs"
|
||||
}
|
||||
|
||||
type PolicyTemplate struct {
|
||||
TemplateID uint64 `gorm:"column:template_id;primaryKey" json:"templateId"`
|
||||
TemplateCode string `gorm:"column:template_code;size:96;uniqueIndex:uk_admin_policy_template_version;not null" json:"templateCode"`
|
||||
TemplateVersion string `gorm:"column:template_version;size:64;uniqueIndex:uk_admin_policy_template_version;not null" json:"templateVersion"`
|
||||
Name string `gorm:"size:160;not null" json:"name"`
|
||||
Status string `gorm:"size:24;index:idx_admin_policy_templates_status,not null;default:active" json:"status"`
|
||||
RuleJSON string `gorm:"column:rule_json;type:json;not null" json:"ruleJson"`
|
||||
Description string `gorm:"size:512;not null;default:''" json:"description"`
|
||||
CreatedByAdminID uint64 `gorm:"column:created_by_admin_id;not null;default:0" json:"createdByAdminId"`
|
||||
UpdatedByAdminID uint64 `gorm:"column:updated_by_admin_id;not null;default:0" json:"updatedByAdminId"`
|
||||
CreatedAtMS int64 `gorm:"column:created_at_ms;autoCreateTime:milli" json:"createdAtMs"`
|
||||
UpdatedAtMS int64 `gorm:"column:updated_at_ms;autoUpdateTime:milli;index:idx_admin_policy_templates_status" json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
func (PolicyTemplate) TableName() string {
|
||||
return "admin_policy_templates"
|
||||
}
|
||||
|
||||
type PolicyTemplateVersion struct {
|
||||
VersionID uint64 `gorm:"column:version_id;primaryKey" json:"versionId"`
|
||||
TemplateCode string `gorm:"column:template_code;size:96;uniqueIndex:uk_admin_policy_template_version_row;index:idx_admin_policy_template_versions_status,not null" json:"templateCode"`
|
||||
TemplateVersion string `gorm:"column:template_version;size:64;uniqueIndex:uk_admin_policy_template_version_row;not null" json:"templateVersion"`
|
||||
Status string `gorm:"size:24;index:idx_admin_policy_template_versions_status,not null;default:active" json:"status"`
|
||||
RuleJSON string `gorm:"column:rule_json;type:json;not null" json:"ruleJson"`
|
||||
CreatedByAdminID uint64 `gorm:"column:created_by_admin_id;not null;default:0" json:"createdByAdminId"`
|
||||
UpdatedByAdminID uint64 `gorm:"column:updated_by_admin_id;not null;default:0" json:"updatedByAdminId"`
|
||||
CreatedAtMS int64 `gorm:"column:created_at_ms;autoCreateTime:milli" json:"createdAtMs"`
|
||||
UpdatedAtMS int64 `gorm:"column:updated_at_ms;autoUpdateTime:milli;index:idx_admin_policy_template_versions_status" json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
func (PolicyTemplateVersion) TableName() string {
|
||||
return "admin_policy_template_versions"
|
||||
}
|
||||
|
||||
type PolicyInstance struct {
|
||||
InstanceID uint64 `gorm:"column:instance_id;primaryKey" json:"instanceId"`
|
||||
AppCode string `gorm:"column:app_code;size:32;uniqueIndex:uk_admin_policy_instance_code;index:idx_admin_policy_instances_app_status,not null" json:"appCode"`
|
||||
InstanceCode string `gorm:"column:instance_code;size:96;uniqueIndex:uk_admin_policy_instance_code;not null" json:"instanceCode"`
|
||||
TemplateCode string `gorm:"column:template_code;size:96;index:idx_admin_policy_instances_template,not null" json:"templateCode"`
|
||||
TemplateVersion string `gorm:"column:template_version;size:64;index:idx_admin_policy_instances_template,not null" json:"templateVersion"`
|
||||
RegionScope string `gorm:"column:region_scope;size:128;not null;default:all_active_regions" json:"regionScope"`
|
||||
Status string `gorm:"size:24;index:idx_admin_policy_instances_app_status,not null;default:active" json:"status"`
|
||||
EffectiveFromMS int64 `gorm:"column:effective_from_ms;not null;default:0" json:"effectiveFromMs"`
|
||||
EffectiveToMS int64 `gorm:"column:effective_to_ms;not null;default:0" json:"effectiveToMs"`
|
||||
PublishStatus string `gorm:"column:publish_status;size:24;index:idx_admin_policy_instances_app_status,not null;default:draft" json:"publishStatus"`
|
||||
PublishError string `gorm:"column:publish_error;size:512;not null;default:''" json:"publishError"`
|
||||
LastPublishedAtMS int64 `gorm:"column:last_published_at_ms;not null;default:0" json:"lastPublishedAtMs"`
|
||||
CreatedByAdminID uint64 `gorm:"column:created_by_admin_id;not null;default:0" json:"createdByAdminId"`
|
||||
UpdatedByAdminID uint64 `gorm:"column:updated_by_admin_id;not null;default:0" json:"updatedByAdminId"`
|
||||
CreatedAtMS int64 `gorm:"column:created_at_ms;autoCreateTime:milli" json:"createdAtMs"`
|
||||
UpdatedAtMS int64 `gorm:"column:updated_at_ms;autoUpdateTime:milli" json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
func (PolicyInstance) TableName() string {
|
||||
return "admin_policy_instances"
|
||||
}
|
||||
|
||||
type PolicyPublishJob struct {
|
||||
JobID uint64 `gorm:"column:job_id;primaryKey" json:"jobId"`
|
||||
InstanceID uint64 `gorm:"column:instance_id;index:idx_admin_policy_publish_jobs_instance,not null" json:"instanceId"`
|
||||
AppCode string `gorm:"column:app_code;size:32;not null" json:"appCode"`
|
||||
InstanceCode string `gorm:"column:instance_code;size:96;not null" json:"instanceCode"`
|
||||
TemplateCode string `gorm:"column:template_code;size:96;not null" json:"templateCode"`
|
||||
TemplateVersion string `gorm:"column:template_version;size:64;not null" json:"templateVersion"`
|
||||
Status string `gorm:"size:24;index:idx_admin_policy_publish_jobs_status,not null;default:pending" json:"status"`
|
||||
TargetCount int `gorm:"column:target_count;not null;default:0" json:"targetCount"`
|
||||
SuccessCount int `gorm:"column:success_count;not null;default:0" json:"successCount"`
|
||||
FailureCount int `gorm:"column:failure_count;not null;default:0" json:"failureCount"`
|
||||
ErrorMessage string `gorm:"column:error_message;size:512;not null;default:''" json:"errorMessage"`
|
||||
OperatorAdminID uint64 `gorm:"column:operator_admin_id;not null;default:0" json:"operatorAdminId"`
|
||||
CreatedAtMS int64 `gorm:"column:created_at_ms;autoCreateTime:milli;index:idx_admin_policy_publish_jobs_instance" json:"createdAtMs"`
|
||||
UpdatedAtMS int64 `gorm:"column:updated_at_ms;autoUpdateTime:milli;index:idx_admin_policy_publish_jobs_status" json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
func (PolicyPublishJob) TableName() string {
|
||||
return "admin_policy_publish_jobs"
|
||||
}
|
||||
|
||||
type PolicyPublishItem struct {
|
||||
ItemID uint64 `gorm:"column:item_id;primaryKey" json:"itemId"`
|
||||
JobID uint64 `gorm:"column:job_id;index:idx_admin_policy_publish_items_job,not null" json:"jobId"`
|
||||
OwnerService string `gorm:"column:owner_service;size:32;index:idx_admin_policy_publish_items_job,not null" json:"ownerService"`
|
||||
AppCode string `gorm:"column:app_code;size:32;index:idx_admin_policy_publish_items_region,not null" json:"appCode"`
|
||||
RegionID int64 `gorm:"column:region_id;index:idx_admin_policy_publish_items_region,not null;default:0" json:"regionId"`
|
||||
Status string `gorm:"size:24;index:idx_admin_policy_publish_items_job,not null;default:pending" json:"status"`
|
||||
ErrorMessage string `gorm:"column:error_message;size:512;not null;default:''" json:"errorMessage"`
|
||||
CreatedAtMS int64 `gorm:"column:created_at_ms;autoCreateTime:milli" json:"createdAtMs"`
|
||||
UpdatedAtMS int64 `gorm:"column:updated_at_ms;autoUpdateTime:milli" json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
func (PolicyPublishItem) TableName() string {
|
||||
return "admin_policy_publish_items"
|
||||
}
|
||||
|
||||
type HostAgencySalaryPolicy struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
AppCode string `gorm:"size:32;uniqueIndex:uk_admin_host_agency_salary_policy_name;index:idx_admin_host_agency_salary_policy_region,not null;default:lalu" json:"appCode"`
|
||||
|
||||
@ -39,6 +39,7 @@ type taskRequest struct {
|
||||
TargetValue int64 `json:"target_value"`
|
||||
TargetUnit string `json:"target_unit"`
|
||||
RewardCoinAmount int64 `json:"reward_coin_amount"`
|
||||
RewardAssetType string `json:"reward_asset_type"`
|
||||
Status string `json:"status"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
EffectiveFromMS int64 `json:"effective_from_ms"`
|
||||
@ -66,6 +67,7 @@ type taskDTO struct {
|
||||
TargetValue int64 `json:"target_value"`
|
||||
TargetUnit string `json:"target_unit"`
|
||||
RewardCoinAmount int64 `json:"reward_coin_amount"`
|
||||
RewardAssetType string `json:"reward_asset_type"`
|
||||
Status string `json:"status"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
Version int64 `json:"version"`
|
||||
@ -187,6 +189,7 @@ func (r taskRequest) toProto(c *gin.Context, taskID string, meta *activityv1.Req
|
||||
TargetValue: r.TargetValue,
|
||||
TargetUnit: strings.TrimSpace(r.TargetUnit),
|
||||
RewardCoinAmount: r.RewardCoinAmount,
|
||||
RewardAssetType: strings.TrimSpace(r.RewardAssetType),
|
||||
Status: strings.TrimSpace(r.Status),
|
||||
SortOrder: r.SortOrder,
|
||||
EffectiveFromMs: r.EffectiveFromMS,
|
||||
@ -233,6 +236,7 @@ func taskFromProto(item *activityv1.TaskDefinition) taskDTO {
|
||||
TargetValue: item.GetTargetValue(),
|
||||
TargetUnit: item.GetTargetUnit(),
|
||||
RewardCoinAmount: item.GetRewardCoinAmount(),
|
||||
RewardAssetType: item.GetRewardAssetType(),
|
||||
Status: item.GetStatus(),
|
||||
SortOrder: item.GetSortOrder(),
|
||||
Version: item.GetVersion(),
|
||||
|
||||
@ -10,6 +10,11 @@ type withdrawalApplicationDTO struct {
|
||||
SalaryAssetType string `json:"salaryAssetType"`
|
||||
WithdrawAmount string `json:"withdrawAmount"`
|
||||
WithdrawAmountMinor int64 `json:"withdrawAmountMinor"`
|
||||
PointGrossAmount int64 `json:"pointGrossAmount,omitempty"`
|
||||
PointFeeAmount int64 `json:"pointFeeAmount,omitempty"`
|
||||
PointNetAmount int64 `json:"pointNetAmount,omitempty"`
|
||||
PointsPerUSD int64 `json:"pointsPerUsd,omitempty"`
|
||||
PointFeeBPS int32 `json:"pointFeeBps,omitempty"`
|
||||
WithdrawMethod string `json:"withdrawMethod"`
|
||||
WithdrawAddress string `json:"withdrawAddress"`
|
||||
FreezeTransactionID string `json:"freezeTransactionId"`
|
||||
@ -25,7 +30,7 @@ type withdrawalApplicationDTO struct {
|
||||
}
|
||||
|
||||
func withdrawalApplicationDTOFromModel(item model.UserWithdrawalApplication) withdrawalApplicationDTO {
|
||||
return withdrawalApplicationDTO{
|
||||
dto := withdrawalApplicationDTO{
|
||||
ID: item.ID,
|
||||
AppCode: item.AppCode,
|
||||
AppName: item.AppCode,
|
||||
@ -46,6 +51,16 @@ func withdrawalApplicationDTOFromModel(item model.UserWithdrawalApplication) wit
|
||||
CreatedAtMS: item.CreatedAtMS,
|
||||
UpdatedAtMS: item.UpdatedAtMS,
|
||||
}
|
||||
if isPointWithdrawalAssetType(item.SalaryAssetType) {
|
||||
grossPoints := item.WithdrawAmountMinor
|
||||
feePoints := grossPoints * int64(pointWithdrawalDefaultFeeBPS) / 10000
|
||||
dto.PointGrossAmount = grossPoints
|
||||
dto.PointFeeAmount = feePoints
|
||||
dto.PointNetAmount = grossPoints - feePoints
|
||||
dto.PointsPerUSD = pointWithdrawalDefaultPointsPerUSD
|
||||
dto.PointFeeBPS = pointWithdrawalDefaultFeeBPS
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
func withdrawalApplicationDTOsFromModel(items []model.UserWithdrawalApplication) []withdrawalApplicationDTO {
|
||||
|
||||
@ -23,6 +23,9 @@ const (
|
||||
permissionViewWithdrawalApplications = "finance-withdrawal:view"
|
||||
permissionAuditWithdrawalApplications = "finance-withdrawal:audit"
|
||||
permissionAuditFinanceApplications = "finance-application:audit"
|
||||
|
||||
pointWithdrawalDefaultPointsPerUSD = int64(100000)
|
||||
pointWithdrawalDefaultFeeBPS = int32(500)
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@ -32,6 +35,11 @@ type Service struct {
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type pointWithdrawalWallet interface {
|
||||
SettlePointWithdrawal(ctx context.Context, req *walletv1.SettlePointWithdrawalRequest) (*walletv1.SettlePointWithdrawalResponse, error)
|
||||
ReleasePointWithdrawal(ctx context.Context, req *walletv1.ReleasePointWithdrawalRequest) (*walletv1.ReleasePointWithdrawalResponse, error)
|
||||
}
|
||||
|
||||
func NewService(store *repository.Store, wallet walletclient.Client, activity activityclient.Client) *Service {
|
||||
return &Service{store: store, wallet: wallet, activity: activity, now: func() time.Time { return time.Now().UTC() }}
|
||||
}
|
||||
@ -70,7 +78,8 @@ func (s *Service) auditApplication(ctx context.Context, actor shared.Actor, id u
|
||||
if decision == model.WithdrawalApplicationStatusRejected && remark == "" {
|
||||
return nil, errors.New("拒绝原因不能为空")
|
||||
}
|
||||
item, err := s.store.GetWithdrawalApplication(id)
|
||||
appCode := appctx.FromContext(ctx)
|
||||
item, err := s.store.GetWithdrawalApplicationForApp(appCode, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -83,10 +92,6 @@ func (s *Service) auditApplication(ctx context.Context, actor shared.Actor, id u
|
||||
}
|
||||
commandID := withdrawalAuditCommandID(id, decision)
|
||||
amountMinor := item.WithdrawAmountMinor
|
||||
appCode := appctx.FromContext(ctx)
|
||||
if strings.TrimSpace(item.AppCode) != "" {
|
||||
appCode = item.AppCode
|
||||
}
|
||||
transactionID, err := s.applyWalletDecision(ctx, decision, commandID, appCode, userID, item.SalaryAssetType, amountMinor, actor, strings.TrimSpace(remark), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -108,7 +113,7 @@ func (s *Service) auditApplication(ctx context.Context, actor shared.Actor, id u
|
||||
// 钱包同意/释放已经用 command id 做幂等;通知失败时不写后台终态,财务重试会复用同一钱包命令和消息事件,避免重复扣款、返还或重复通知。
|
||||
return nil, err
|
||||
}
|
||||
updated, err := s.store.AuditWithdrawalApplication(id, repository.WithdrawalApplicationAuditInput{
|
||||
updated, err := s.store.AuditWithdrawalApplicationForApp(appCode, id, repository.WithdrawalApplicationAuditInput{
|
||||
Decision: decision,
|
||||
ApproverUserID: actor.UserID,
|
||||
ApproverName: actor.Username,
|
||||
@ -131,6 +136,56 @@ func (s *Service) applyWalletDecision(ctx context.Context, decision string, comm
|
||||
reason = "salary withdrawal " + decision
|
||||
}
|
||||
applicationIDText := strconv.FormatUint(uint64(applicationID), 10)
|
||||
if isPointWithdrawalAssetType(assetType) {
|
||||
pointWallet, ok := s.wallet.(pointWithdrawalWallet)
|
||||
if !ok {
|
||||
return "", errors.New("point withdrawal wallet executor is not configured")
|
||||
}
|
||||
feePoints := amountMinor * int64(pointWithdrawalDefaultFeeBPS) / 10000
|
||||
netPoints := amountMinor - feePoints
|
||||
switch decision {
|
||||
case model.WithdrawalApplicationStatusApproved:
|
||||
resp, err := pointWallet.SettlePointWithdrawal(ctx, &walletv1.SettlePointWithdrawalRequest{
|
||||
CommandId: commandID,
|
||||
UserId: userID,
|
||||
AssetType: assetType,
|
||||
GrossPointAmount: amountMinor,
|
||||
FeePointAmount: feePoints,
|
||||
NetPointAmount: netPoints,
|
||||
PointsPerUsd: pointWithdrawalDefaultPointsPerUSD,
|
||||
FeeBps: pointWithdrawalDefaultFeeBPS,
|
||||
OperatorUserId: int64(actor.UserID),
|
||||
Reason: reason,
|
||||
AppCode: appCode,
|
||||
WithdrawalApplicationId: applicationIDText,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.GetTransactionId(), nil
|
||||
case model.WithdrawalApplicationStatusRejected:
|
||||
resp, err := pointWallet.ReleasePointWithdrawal(ctx, &walletv1.ReleasePointWithdrawalRequest{
|
||||
CommandId: commandID,
|
||||
UserId: userID,
|
||||
AssetType: assetType,
|
||||
GrossPointAmount: amountMinor,
|
||||
FeePointAmount: feePoints,
|
||||
NetPointAmount: netPoints,
|
||||
PointsPerUsd: pointWithdrawalDefaultPointsPerUSD,
|
||||
FeeBps: pointWithdrawalDefaultFeeBPS,
|
||||
OperatorUserId: int64(actor.UserID),
|
||||
Reason: reason,
|
||||
AppCode: appCode,
|
||||
WithdrawalApplicationId: applicationIDText,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.GetTransactionId(), nil
|
||||
default:
|
||||
return "", errors.New("审核结果不正确")
|
||||
}
|
||||
}
|
||||
switch decision {
|
||||
case model.WithdrawalApplicationStatusApproved:
|
||||
resp, err := s.wallet.SettleSalaryWithdrawal(ctx, &walletv1.SettleSalaryWithdrawalRequest{
|
||||
@ -171,6 +226,15 @@ func withdrawalAuditCommandID(id uint, decision string) string {
|
||||
return fmt.Sprintf("salary-withdrawal:%d:%s", id, decision)
|
||||
}
|
||||
|
||||
func isPointWithdrawalAssetType(assetType string) bool {
|
||||
switch strings.ToUpper(strings.TrimSpace(assetType)) {
|
||||
case "POINT", "COIN_SELLER_POINT":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type auditNoticeInput struct {
|
||||
ApplicationID uint
|
||||
AppCode string
|
||||
|
||||
120
server/admin/internal/modules/financewithdrawal/service_test.go
Normal file
120
server/admin/internal/modules/financewithdrawal/service_test.go
Normal file
@ -0,0 +1,120 @@
|
||||
package financewithdrawal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"hyapp-admin-server/internal/integration/walletclient"
|
||||
"hyapp-admin-server/internal/model"
|
||||
"hyapp-admin-server/internal/modules/shared"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
)
|
||||
|
||||
func TestApplyWalletDecisionRoutesPointApprovalToPointSettlement(t *testing.T) {
|
||||
wallet := &fakeAuditWalletClient{}
|
||||
svc := &Service{wallet: wallet}
|
||||
|
||||
transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusApproved, "audit:77:approved", "huwaa", 42001, "POINT", 1_000_000, shared.Actor{UserID: 90001}, "paid", 77)
|
||||
if err != nil {
|
||||
t.Fatalf("apply point approval failed: %v", err)
|
||||
}
|
||||
if transactionID != "point-settle-tx" {
|
||||
t.Fatalf("point approval transaction id mismatch: %s", transactionID)
|
||||
}
|
||||
if wallet.settlePoint == nil ||
|
||||
wallet.settlePoint.GetCommandId() != "audit:77:approved" ||
|
||||
wallet.settlePoint.GetAppCode() != "huwaa" ||
|
||||
wallet.settlePoint.GetUserId() != 42001 ||
|
||||
wallet.settlePoint.GetAssetType() != "POINT" ||
|
||||
wallet.settlePoint.GetGrossPointAmount() != 1_000_000 ||
|
||||
wallet.settlePoint.GetFeePointAmount() != 50_000 ||
|
||||
wallet.settlePoint.GetNetPointAmount() != 950_000 ||
|
||||
wallet.settlePoint.GetOperatorUserId() != 90001 ||
|
||||
wallet.settlePoint.GetWithdrawalApplicationId() != "77" {
|
||||
t.Fatalf("SettlePointWithdrawal payload mismatch: %+v", wallet.settlePoint)
|
||||
}
|
||||
if wallet.settleSalary != nil || wallet.releasePoint != nil || wallet.releaseSalary != nil {
|
||||
t.Fatalf("point approval must not call legacy salary or release RPCs: wallet=%+v", wallet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyWalletDecisionRoutesPointRejectionToPointRelease(t *testing.T) {
|
||||
wallet := &fakeAuditWalletClient{}
|
||||
svc := &Service{wallet: wallet}
|
||||
|
||||
transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusRejected, "audit:78:rejected", "huwaa", 42002, "COIN_SELLER_POINT", 2_000_000, shared.Actor{UserID: 90002}, "bad address", 78)
|
||||
if err != nil {
|
||||
t.Fatalf("apply point rejection failed: %v", err)
|
||||
}
|
||||
if transactionID != "point-release-tx" {
|
||||
t.Fatalf("point rejection transaction id mismatch: %s", transactionID)
|
||||
}
|
||||
if wallet.releasePoint == nil ||
|
||||
wallet.releasePoint.GetCommandId() != "audit:78:rejected" ||
|
||||
wallet.releasePoint.GetAppCode() != "huwaa" ||
|
||||
wallet.releasePoint.GetUserId() != 42002 ||
|
||||
wallet.releasePoint.GetAssetType() != "COIN_SELLER_POINT" ||
|
||||
wallet.releasePoint.GetGrossPointAmount() != 2_000_000 ||
|
||||
wallet.releasePoint.GetFeePointAmount() != 100_000 ||
|
||||
wallet.releasePoint.GetNetPointAmount() != 1_900_000 ||
|
||||
wallet.releasePoint.GetOperatorUserId() != 90002 ||
|
||||
wallet.releasePoint.GetReason() != "bad address" ||
|
||||
wallet.releasePoint.GetWithdrawalApplicationId() != "78" {
|
||||
t.Fatalf("ReleasePointWithdrawal payload mismatch: %+v", wallet.releasePoint)
|
||||
}
|
||||
if wallet.settlePoint != nil || wallet.settleSalary != nil || wallet.releaseSalary != nil {
|
||||
t.Fatalf("point rejection must not call legacy salary or settle RPCs: wallet=%+v", wallet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyWalletDecisionKeepsLegacySalaryAssetsOnSalaryRPC(t *testing.T) {
|
||||
wallet := &fakeAuditWalletClient{}
|
||||
svc := &Service{wallet: wallet}
|
||||
|
||||
transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusApproved, "audit:79:approved", "lalu", 42003, "HOST_SALARY_USD", 12_345, shared.Actor{UserID: 90003}, "", 79)
|
||||
if err != nil {
|
||||
t.Fatalf("apply salary approval failed: %v", err)
|
||||
}
|
||||
if transactionID != "salary-settle-tx" {
|
||||
t.Fatalf("salary approval transaction id mismatch: %s", transactionID)
|
||||
}
|
||||
if wallet.settleSalary == nil ||
|
||||
wallet.settleSalary.GetAppCode() != "lalu" ||
|
||||
wallet.settleSalary.GetUserId() != 42003 ||
|
||||
wallet.settleSalary.GetSalaryAssetType() != "HOST_SALARY_USD" ||
|
||||
wallet.settleSalary.GetSalaryUsdMinor() != 12_345 ||
|
||||
wallet.settleSalary.GetWithdrawalApplicationId() != "79" {
|
||||
t.Fatalf("SettleSalaryWithdrawal payload mismatch: %+v", wallet.settleSalary)
|
||||
}
|
||||
if wallet.settlePoint != nil || wallet.releasePoint != nil {
|
||||
t.Fatalf("legacy salary asset must not call point RPCs: wallet=%+v", wallet)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuditWalletClient struct {
|
||||
walletclient.Client
|
||||
settlePoint *walletv1.SettlePointWithdrawalRequest
|
||||
releasePoint *walletv1.ReleasePointWithdrawalRequest
|
||||
settleSalary *walletv1.SettleSalaryWithdrawalRequest
|
||||
releaseSalary *walletv1.ReleaseSalaryWithdrawalRequest
|
||||
}
|
||||
|
||||
func (f *fakeAuditWalletClient) SettlePointWithdrawal(_ context.Context, req *walletv1.SettlePointWithdrawalRequest) (*walletv1.SettlePointWithdrawalResponse, error) {
|
||||
f.settlePoint = req
|
||||
return &walletv1.SettlePointWithdrawalResponse{TransactionId: "point-settle-tx"}, nil
|
||||
}
|
||||
|
||||
func (f *fakeAuditWalletClient) ReleasePointWithdrawal(_ context.Context, req *walletv1.ReleasePointWithdrawalRequest) (*walletv1.ReleasePointWithdrawalResponse, error) {
|
||||
f.releasePoint = req
|
||||
return &walletv1.ReleasePointWithdrawalResponse{TransactionId: "point-release-tx"}, nil
|
||||
}
|
||||
|
||||
func (f *fakeAuditWalletClient) SettleSalaryWithdrawal(_ context.Context, req *walletv1.SettleSalaryWithdrawalRequest) (*walletv1.SettleSalaryWithdrawalResponse, error) {
|
||||
f.settleSalary = req
|
||||
return &walletv1.SettleSalaryWithdrawalResponse{TransactionId: "salary-settle-tx"}, nil
|
||||
}
|
||||
|
||||
func (f *fakeAuditWalletClient) ReleaseSalaryWithdrawal(_ context.Context, req *walletv1.ReleaseSalaryWithdrawalRequest) (*walletv1.ReleaseSalaryWithdrawalResponse, error) {
|
||||
f.releaseSalary = req
|
||||
return &walletv1.ReleaseSalaryWithdrawalResponse{TransactionId: "salary-release-tx"}, nil
|
||||
}
|
||||
@ -7,30 +7,30 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
"hyapp-admin-server/internal/integration/luckygiftadmin"
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
"hyapp-admin-server/internal/modules/shared"
|
||||
"hyapp-admin-server/internal/response"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
activity activityclient.Client
|
||||
luckyGift luckygiftadmin.Client
|
||||
requestTimeout time.Duration
|
||||
audit shared.OperationLogger
|
||||
}
|
||||
|
||||
func New(activity activityclient.Client, requestTimeout time.Duration, audit shared.OperationLogger) *Handler {
|
||||
func New(luckyGift luckygiftadmin.Client, requestTimeout time.Duration, audit shared.OperationLogger) *Handler {
|
||||
if requestTimeout <= 0 {
|
||||
requestTimeout = 3 * time.Second
|
||||
}
|
||||
return &Handler{activity: activity, requestTimeout: requestTimeout, audit: audit}
|
||||
return &Handler{luckyGift: luckyGift, requestTimeout: requestTimeout, audit: audit}
|
||||
}
|
||||
|
||||
type configRequest struct {
|
||||
AppCode string `json:"app_code"`
|
||||
PoolID string `json:"pool_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
TargetRTPPercent float64 `json:"target_rtp_percent"`
|
||||
@ -40,22 +40,15 @@ type configRequest struct {
|
||||
GiftPriceReference int64 `json:"gift_price_reference"`
|
||||
NoviceMaxEquivalentDraws int64 `json:"novice_max_equivalent_draws"`
|
||||
NormalMaxEquivalentDraws int64 `json:"normal_max_equivalent_draws"`
|
||||
MaxSinglePayout int64 `json:"max_single_payout"`
|
||||
UserHourlyPayoutCap int64 `json:"user_hourly_payout_cap"`
|
||||
UserDailyPayoutCap int64 `json:"user_daily_payout_cap"`
|
||||
DeviceDailyPayoutCap int64 `json:"device_daily_payout_cap"`
|
||||
RoomHourlyPayoutCap int64 `json:"room_hourly_payout_cap"`
|
||||
AnchorDailyPayoutCap int64 `json:"anchor_daily_payout_cap"`
|
||||
EffectiveFromMS int64 `json:"effective_from_ms"`
|
||||
Stages []stageDTO `json:"stages"`
|
||||
}
|
||||
|
||||
type configDTO struct {
|
||||
configRequest
|
||||
AppCode string `json:"app_code"`
|
||||
RuleVersion int64 `json:"rule_version"`
|
||||
CreatedByAdminID int64 `json:"created_by_admin_id"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
RuleVersion int64 `json:"rule_version"`
|
||||
CreatedByAdminID int64 `json:"created_by_admin_id"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
}
|
||||
|
||||
type stageDTO struct {
|
||||
@ -67,56 +60,68 @@ type tierDTO struct {
|
||||
TierID string `json:"tier_id"`
|
||||
Multiplier float64 `json:"multiplier"`
|
||||
ProbabilityPercent float64 `json:"probability_percent"`
|
||||
RewardSource string `json:"reward_source"`
|
||||
HighWaterOnly bool `json:"high_water_only"`
|
||||
BroadcastLevel string `json:"broadcast_level"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type drawDTO struct {
|
||||
DrawID string `json:"draw_id"`
|
||||
CommandID string `json:"command_id"`
|
||||
PoolID string `json:"pool_id"`
|
||||
GiftID string `json:"gift_id"`
|
||||
RuleVersion int64 `json:"rule_version"`
|
||||
ExperiencePool string `json:"experience_pool"`
|
||||
SelectedTierID string `json:"selected_tier_id"`
|
||||
MultiplierPPM int64 `json:"multiplier_ppm"`
|
||||
BaseRewardCoins int64 `json:"base_reward_coins"`
|
||||
RoomAtmosphereRewardCoins int64 `json:"room_atmosphere_reward_coins"`
|
||||
ActivitySubsidyCoins int64 `json:"activity_subsidy_coins"`
|
||||
EffectiveRewardCoins int64 `json:"effective_reward_coins"`
|
||||
BudgetSourcesJSON string `json:"budget_sources_json"`
|
||||
RewardStatus string `json:"reward_status"`
|
||||
RTPWindowIndex int64 `json:"rtp_window_index"`
|
||||
GiftRTPWindowIndex int64 `json:"gift_rtp_window_index"`
|
||||
GlobalBaseRTPPPM int64 `json:"global_base_rtp_ppm"`
|
||||
GiftBaseRTPPPM int64 `json:"gift_base_rtp_ppm"`
|
||||
StageFeedback bool `json:"stage_feedback"`
|
||||
HighMultiplier bool `json:"high_multiplier"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
DrawID string `json:"draw_id"`
|
||||
CommandID string `json:"command_id"`
|
||||
PoolID string `json:"pool_id"`
|
||||
GiftID string `json:"gift_id"`
|
||||
RuleVersion int64 `json:"rule_version"`
|
||||
ExperiencePool string `json:"experience_pool"`
|
||||
SelectedTierID string `json:"selected_tier_id"`
|
||||
MultiplierPPM int64 `json:"multiplier_ppm"`
|
||||
BaseRewardCoins int64 `json:"base_reward_coins"`
|
||||
EffectiveRewardCoins int64 `json:"effective_reward_coins"`
|
||||
RewardStatus string `json:"reward_status"`
|
||||
RTPWindowIndex int64 `json:"rtp_window_index"`
|
||||
GiftRTPWindowIndex int64 `json:"gift_rtp_window_index"`
|
||||
GlobalBaseRTPPPM int64 `json:"global_base_rtp_ppm"`
|
||||
GiftBaseRTPPPM int64 `json:"gift_base_rtp_ppm"`
|
||||
StageFeedback bool `json:"stage_feedback"`
|
||||
HighMultiplier bool `json:"high_multiplier"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
AppCode string `json:"app_code"`
|
||||
UserID int64 `json:"user_id"`
|
||||
ExternalUserID string `json:"external_user_id"`
|
||||
}
|
||||
|
||||
type drawSummaryDTO struct {
|
||||
PoolID string `json:"pool_id"`
|
||||
TotalDraws int64 `json:"total_draws"`
|
||||
UniqueUsers int64 `json:"unique_users"`
|
||||
UniqueRooms int64 `json:"unique_rooms"`
|
||||
TotalSpentCoins int64 `json:"total_spent_coins"`
|
||||
TotalRewardCoins int64 `json:"total_reward_coins"`
|
||||
BaseRewardCoins int64 `json:"base_reward_coins"`
|
||||
RoomAtmosphereRewardCoins int64 `json:"room_atmosphere_reward_coins"`
|
||||
ActivitySubsidyCoins int64 `json:"activity_subsidy_coins"`
|
||||
ActualRTPPPM int64 `json:"actual_rtp_ppm"`
|
||||
PendingDraws int64 `json:"pending_draws"`
|
||||
GrantedDraws int64 `json:"granted_draws"`
|
||||
FailedDraws int64 `json:"failed_draws"`
|
||||
PoolID string `json:"pool_id"`
|
||||
TotalDraws int64 `json:"total_draws"`
|
||||
UniqueUsers int64 `json:"unique_users"`
|
||||
UniqueRooms int64 `json:"unique_rooms"`
|
||||
TotalSpentCoins int64 `json:"total_spent_coins"`
|
||||
TotalRewardCoins int64 `json:"total_reward_coins"`
|
||||
BaseRewardCoins int64 `json:"base_reward_coins"`
|
||||
ActualRTPPPM int64 `json:"actual_rtp_ppm"`
|
||||
PendingDraws int64 `json:"pending_draws"`
|
||||
GrantedDraws int64 `json:"granted_draws"`
|
||||
FailedDraws int64 `json:"failed_draws"`
|
||||
}
|
||||
|
||||
type poolBalanceDTO struct {
|
||||
AppCode string `json:"app_code"`
|
||||
PoolID string `json:"pool_id"`
|
||||
Balance int64 `json:"balance"`
|
||||
ReserveFloor int64 `json:"reserve_floor"`
|
||||
AvailableBalance int64 `json:"available_balance"`
|
||||
TotalIn int64 `json:"total_in"`
|
||||
TotalOut int64 `json:"total_out"`
|
||||
Materialized bool `json:"materialized"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
func (h *Handler) GetLuckyGiftConfig(c *gin.Context) {
|
||||
ctx, cancel := h.activityContext(c)
|
||||
if strings.TrimSpace(c.Query("app_code")) == "" {
|
||||
response.BadRequest(c, "app_code is required")
|
||||
return
|
||||
}
|
||||
ctx, cancel := h.luckyGiftContext(c)
|
||||
defer cancel()
|
||||
resp, err := h.activity.GetLuckyGiftConfig(ctx, &activityv1.GetLuckyGiftConfigRequest{
|
||||
resp, err := h.luckyGift.GetLuckyGiftConfig(ctx, &luckygiftv1.GetLuckyGiftConfigRequest{
|
||||
Meta: h.meta(c),
|
||||
PoolId: strings.TrimSpace(c.Query("pool_id")),
|
||||
})
|
||||
@ -136,10 +141,17 @@ func (h *Handler) UpsertLuckyGiftConfig(c *gin.Context) {
|
||||
if req.PoolID == "" {
|
||||
req.PoolID = strings.TrimSpace(c.Query("pool_id"))
|
||||
}
|
||||
ctx, cancel := h.activityContext(c)
|
||||
if req.AppCode == "" {
|
||||
req.AppCode = strings.TrimSpace(c.Query("app_code"))
|
||||
}
|
||||
if strings.TrimSpace(req.AppCode) == "" {
|
||||
response.BadRequest(c, "app_code is required")
|
||||
return
|
||||
}
|
||||
ctx, cancel := h.luckyGiftContext(c)
|
||||
defer cancel()
|
||||
resp, err := h.activity.UpsertLuckyGiftConfig(ctx, &activityv1.UpsertLuckyGiftConfigRequest{
|
||||
Meta: h.meta(c),
|
||||
resp, err := h.luckyGift.UpsertLuckyGiftConfig(ctx, &luckygiftv1.UpsertLuckyGiftConfigRequest{
|
||||
Meta: h.metaForApp(c, req.AppCode),
|
||||
Config: configToProto(req),
|
||||
OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||
})
|
||||
@ -153,9 +165,13 @@ func (h *Handler) UpsertLuckyGiftConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Handler) ListLuckyGiftConfigs(c *gin.Context) {
|
||||
ctx, cancel := h.activityContext(c)
|
||||
if strings.TrimSpace(c.Query("app_code")) == "" {
|
||||
response.BadRequest(c, "app_code is required")
|
||||
return
|
||||
}
|
||||
ctx, cancel := h.luckyGiftContext(c)
|
||||
defer cancel()
|
||||
resp, err := h.activity.ListLuckyGiftConfigs(ctx, &activityv1.ListLuckyGiftConfigsRequest{
|
||||
resp, err := h.luckyGift.ListLuckyGiftConfigs(ctx, &luckygiftv1.ListLuckyGiftConfigsRequest{
|
||||
Meta: h.meta(c),
|
||||
})
|
||||
if err != nil {
|
||||
@ -170,18 +186,24 @@ func (h *Handler) ListLuckyGiftConfigs(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Handler) ListLuckyGiftDraws(c *gin.Context) {
|
||||
if strings.TrimSpace(c.Query("app_code")) == "" {
|
||||
response.BadRequest(c, "app_code is required")
|
||||
return
|
||||
}
|
||||
options := shared.ListOptions(c)
|
||||
ctx, cancel := h.activityContext(c)
|
||||
ctx, cancel := h.luckyGiftContext(c)
|
||||
defer cancel()
|
||||
resp, err := h.activity.ListLuckyGiftDraws(ctx, &activityv1.ListLuckyGiftDrawsRequest{
|
||||
Meta: h.meta(c),
|
||||
PoolId: strings.TrimSpace(c.Query("pool_id")),
|
||||
GiftId: strings.TrimSpace(c.Query("gift_id")),
|
||||
UserId: parseOptionalInt64(c.Query("user_id")),
|
||||
RoomId: strings.TrimSpace(c.Query("room_id")),
|
||||
Status: strings.TrimSpace(options.Status),
|
||||
Page: int32(options.Page),
|
||||
PageSize: int32(options.PageSize),
|
||||
resp, err := h.luckyGift.ListLuckyGiftDraws(ctx, &luckygiftv1.ListLuckyGiftDrawsRequest{
|
||||
Meta: h.meta(c),
|
||||
PoolId: strings.TrimSpace(c.Query("pool_id")),
|
||||
GiftId: strings.TrimSpace(c.Query("gift_id")),
|
||||
UserId: parseOptionalInt64(c.Query("user_id")),
|
||||
RoomId: strings.TrimSpace(c.Query("room_id")),
|
||||
Status: strings.TrimSpace(options.Status),
|
||||
Page: int32(options.Page),
|
||||
PageSize: int32(options.PageSize),
|
||||
ExternalUserId: strings.TrimSpace(c.Query("external_user_id")),
|
||||
ExternalOnly: parseOptionalBool(c.Query("external_only")),
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取幸运礼物抽奖记录失败")
|
||||
@ -195,15 +217,21 @@ func (h *Handler) ListLuckyGiftDraws(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Handler) GetLuckyGiftDrawSummary(c *gin.Context) {
|
||||
ctx, cancel := h.activityContext(c)
|
||||
if strings.TrimSpace(c.Query("app_code")) == "" {
|
||||
response.BadRequest(c, "app_code is required")
|
||||
return
|
||||
}
|
||||
ctx, cancel := h.luckyGiftContext(c)
|
||||
defer cancel()
|
||||
resp, err := h.activity.GetLuckyGiftDrawSummary(ctx, &activityv1.GetLuckyGiftDrawSummaryRequest{
|
||||
Meta: h.meta(c),
|
||||
PoolId: strings.TrimSpace(c.Query("pool_id")),
|
||||
GiftId: strings.TrimSpace(c.Query("gift_id")),
|
||||
UserId: parseOptionalInt64(c.Query("user_id")),
|
||||
RoomId: strings.TrimSpace(c.Query("room_id")),
|
||||
Status: strings.TrimSpace(c.Query("status")),
|
||||
resp, err := h.luckyGift.GetLuckyGiftDrawSummary(ctx, &luckygiftv1.GetLuckyGiftDrawSummaryRequest{
|
||||
Meta: h.meta(c),
|
||||
PoolId: strings.TrimSpace(c.Query("pool_id")),
|
||||
GiftId: strings.TrimSpace(c.Query("gift_id")),
|
||||
UserId: parseOptionalInt64(c.Query("user_id")),
|
||||
RoomId: strings.TrimSpace(c.Query("room_id")),
|
||||
Status: strings.TrimSpace(c.Query("status")),
|
||||
ExternalUserId: strings.TrimSpace(c.Query("external_user_id")),
|
||||
ExternalOnly: parseOptionalBool(c.Query("external_only")),
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取幸运礼物抽奖汇总失败")
|
||||
@ -212,46 +240,77 @@ func (h *Handler) GetLuckyGiftDrawSummary(c *gin.Context) {
|
||||
response.OK(c, drawSummaryFromProto(resp.GetSummary()))
|
||||
}
|
||||
|
||||
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
|
||||
return &activityv1.RequestMeta{
|
||||
func (h *Handler) ListLuckyGiftPoolBalances(c *gin.Context) {
|
||||
if strings.TrimSpace(c.Query("app_code")) == "" {
|
||||
response.BadRequest(c, "app_code is required")
|
||||
return
|
||||
}
|
||||
ctx, cancel := h.luckyGiftContext(c)
|
||||
defer cancel()
|
||||
resp, err := h.luckyGift.ListLuckyGiftPoolBalances(ctx, &luckygiftv1.ListLuckyGiftPoolBalancesRequest{
|
||||
Meta: h.meta(c),
|
||||
PoolId: strings.TrimSpace(c.Query("pool_id")),
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取幸运礼物奖池余额失败")
|
||||
return
|
||||
}
|
||||
items := make([]poolBalanceDTO, 0, len(resp.GetPools()))
|
||||
for _, pool := range resp.GetPools() {
|
||||
items = append(items, poolBalanceFromProto(pool))
|
||||
}
|
||||
response.OK(c, gin.H{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
func (h *Handler) meta(c *gin.Context) *luckygiftv1.RequestMeta {
|
||||
return h.metaForApp(c, "")
|
||||
}
|
||||
|
||||
func (h *Handler) metaForApp(c *gin.Context, explicitAppCode string) *luckygiftv1.RequestMeta {
|
||||
appCode := strings.ToLower(strings.TrimSpace(explicitAppCode))
|
||||
if appCode == "" {
|
||||
appCode = strings.ToLower(strings.TrimSpace(c.Query("app_code")))
|
||||
}
|
||||
return &luckygiftv1.RequestMeta{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
Caller: "admin-server",
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
SentAtMs: time.Now().UTC().UnixMilli(),
|
||||
// ops-center 是 admin 的衍生平台,筛选 app_code 通过 query/body 传入;
|
||||
// 这里把它放进 Admin RPC meta,让 lucky-gift-service 继续作为 app 维度过滤和配置发布的唯一 owner。
|
||||
AppCode: appCode,
|
||||
SentAtMs: time.Now().UTC().UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) activityContext(c *gin.Context) (context.Context, context.CancelFunc) {
|
||||
func (h *Handler) luckyGiftContext(c *gin.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(c.Request.Context(), h.requestTimeout)
|
||||
}
|
||||
|
||||
func configToProto(req configRequest) *activityv1.LuckyGiftRuleConfig {
|
||||
stages := make([]*activityv1.LuckyGiftRuleStage, 0, len(req.Stages))
|
||||
func configToProto(req configRequest) *luckygiftv1.LuckyGiftRuleConfig {
|
||||
stages := make([]*luckygiftv1.LuckyGiftRuleStage, 0, len(req.Stages))
|
||||
for _, stage := range req.Stages {
|
||||
stageName := strings.TrimSpace(stage.Stage)
|
||||
tierIDCounts := make(map[string]int, len(stage.Tiers))
|
||||
tiers := make([]*activityv1.LuckyGiftRuleTier, 0, len(stage.Tiers))
|
||||
tiers := make([]*luckygiftv1.LuckyGiftRuleTier, 0, len(stage.Tiers))
|
||||
for _, tier := range stage.Tiers {
|
||||
multiplierPPM := multiplierToPPM(tier.Multiplier)
|
||||
tiers = append(tiers, &activityv1.LuckyGiftRuleTier{
|
||||
tiers = append(tiers, &luckygiftv1.LuckyGiftRuleTier{
|
||||
Stage: stageName,
|
||||
// 奖档 ID 是 activity-service 持久化主键的一部分,不能让运营手工维护;admin-server 按阶段和倍率生成稳定 ID,
|
||||
// 同一阶段同倍率出现多行时追加序号,保证进入 activity-service 前已经满足唯一性。
|
||||
TierId: generatedLuckyGiftTierID(stageName, multiplierPPM, tierIDCounts),
|
||||
MultiplierPpm: multiplierPPM,
|
||||
BaseWeightPpm: percentToPPM(tier.ProbabilityPercent),
|
||||
RewardSource: strings.TrimSpace(tier.RewardSource),
|
||||
HighWaterOnly: tier.HighWaterOnly,
|
||||
BroadcastLevel: strings.TrimSpace(tier.BroadcastLevel),
|
||||
Enabled: tier.Enabled,
|
||||
// 奖档 ID 是 lucky-gift-service 持久化主键的一部分,不能让运营手工维护;admin-server 按阶段和倍率生成稳定 ID,
|
||||
// 同一阶段同倍率出现多行时追加序号,保证进入 lucky-gift-service 前已经满足唯一性。
|
||||
TierId: generatedLuckyGiftTierID(stageName, multiplierPPM, tierIDCounts),
|
||||
MultiplierPpm: multiplierPPM,
|
||||
BaseWeightPpm: percentToPPM(tier.ProbabilityPercent),
|
||||
HighWaterOnly: tier.HighWaterOnly,
|
||||
Enabled: tier.Enabled,
|
||||
})
|
||||
}
|
||||
stages = append(stages, &activityv1.LuckyGiftRuleStage{
|
||||
stages = append(stages, &luckygiftv1.LuckyGiftRuleStage{
|
||||
Stage: stageName,
|
||||
Tiers: tiers,
|
||||
})
|
||||
}
|
||||
return &activityv1.LuckyGiftRuleConfig{
|
||||
return &luckygiftv1.LuckyGiftRuleConfig{
|
||||
AppCode: strings.ToLower(strings.TrimSpace(req.AppCode)),
|
||||
PoolId: strings.TrimSpace(req.PoolID),
|
||||
Enabled: req.Enabled,
|
||||
TargetRtpPpm: percentToPPM(req.TargetRTPPercent),
|
||||
@ -261,18 +320,12 @@ func configToProto(req configRequest) *activityv1.LuckyGiftRuleConfig {
|
||||
GiftPriceReference: req.GiftPriceReference,
|
||||
NoviceMaxEquivalentDraws: req.NoviceMaxEquivalentDraws,
|
||||
NormalMaxEquivalentDraws: req.NormalMaxEquivalentDraws,
|
||||
MaxSinglePayout: req.MaxSinglePayout,
|
||||
UserHourlyPayoutCap: req.UserHourlyPayoutCap,
|
||||
UserDailyPayoutCap: req.UserDailyPayoutCap,
|
||||
DeviceDailyPayoutCap: req.DeviceDailyPayoutCap,
|
||||
RoomHourlyPayoutCap: req.RoomHourlyPayoutCap,
|
||||
AnchorDailyPayoutCap: req.AnchorDailyPayoutCap,
|
||||
EffectiveFromMs: req.EffectiveFromMS,
|
||||
Stages: stages,
|
||||
}
|
||||
}
|
||||
|
||||
func configFromProto(config *activityv1.LuckyGiftRuleConfig) configDTO {
|
||||
func configFromProto(config *luckygiftv1.LuckyGiftRuleConfig) configDTO {
|
||||
if config == nil {
|
||||
return configDTO{}
|
||||
}
|
||||
@ -284,9 +337,7 @@ func configFromProto(config *activityv1.LuckyGiftRuleConfig) configDTO {
|
||||
TierID: tier.GetTierId(),
|
||||
Multiplier: ppmToMultiplier(tier.GetMultiplierPpm()),
|
||||
ProbabilityPercent: ppmToPercent(tier.GetBaseWeightPpm()),
|
||||
RewardSource: tier.GetRewardSource(),
|
||||
HighWaterOnly: tier.GetHighWaterOnly(),
|
||||
BroadcastLevel: tier.GetBroadcastLevel(),
|
||||
Enabled: tier.GetEnabled(),
|
||||
})
|
||||
}
|
||||
@ -297,11 +348,11 @@ func configFromProto(config *activityv1.LuckyGiftRuleConfig) configDTO {
|
||||
}
|
||||
poolID := strings.TrimSpace(config.GetPoolId())
|
||||
return configDTO{
|
||||
AppCode: config.GetAppCode(),
|
||||
RuleVersion: config.GetRuleVersion(),
|
||||
CreatedByAdminID: config.GetCreatedByAdminId(),
|
||||
CreatedAtMS: config.GetCreatedAtMs(),
|
||||
configRequest: configRequest{
|
||||
AppCode: config.GetAppCode(),
|
||||
PoolID: poolID,
|
||||
Enabled: config.GetEnabled(),
|
||||
TargetRTPPercent: ppmToPercent(config.GetTargetRtpPpm()),
|
||||
@ -311,12 +362,6 @@ func configFromProto(config *activityv1.LuckyGiftRuleConfig) configDTO {
|
||||
GiftPriceReference: config.GetGiftPriceReference(),
|
||||
NoviceMaxEquivalentDraws: config.GetNoviceMaxEquivalentDraws(),
|
||||
NormalMaxEquivalentDraws: config.GetNormalMaxEquivalentDraws(),
|
||||
MaxSinglePayout: config.GetMaxSinglePayout(),
|
||||
UserHourlyPayoutCap: config.GetUserHourlyPayoutCap(),
|
||||
UserDailyPayoutCap: config.GetUserDailyPayoutCap(),
|
||||
DeviceDailyPayoutCap: config.GetDeviceDailyPayoutCap(),
|
||||
RoomHourlyPayoutCap: config.GetRoomHourlyPayoutCap(),
|
||||
AnchorDailyPayoutCap: config.GetAnchorDailyPayoutCap(),
|
||||
EffectiveFromMS: config.GetEffectiveFromMs(),
|
||||
Stages: stages,
|
||||
},
|
||||
@ -362,53 +407,68 @@ func ppmToMultiplier(value int64) float64 {
|
||||
return float64(value) / 1_000_000
|
||||
}
|
||||
|
||||
func drawFromProto(draw *activityv1.LuckyGiftDrawResult) drawDTO {
|
||||
func drawFromProto(draw *luckygiftv1.LuckyGiftDrawResult) drawDTO {
|
||||
if draw == nil {
|
||||
return drawDTO{}
|
||||
}
|
||||
return drawDTO{
|
||||
DrawID: draw.GetDrawId(),
|
||||
CommandID: draw.GetCommandId(),
|
||||
PoolID: draw.GetPoolId(),
|
||||
GiftID: draw.GetGiftId(),
|
||||
RuleVersion: draw.GetRuleVersion(),
|
||||
ExperiencePool: draw.GetExperiencePool(),
|
||||
SelectedTierID: draw.GetSelectedTierId(),
|
||||
MultiplierPPM: draw.GetMultiplierPpm(),
|
||||
BaseRewardCoins: draw.GetBaseRewardCoins(),
|
||||
RoomAtmosphereRewardCoins: draw.GetRoomAtmosphereRewardCoins(),
|
||||
ActivitySubsidyCoins: draw.GetActivitySubsidyCoins(),
|
||||
EffectiveRewardCoins: draw.GetEffectiveRewardCoins(),
|
||||
BudgetSourcesJSON: draw.GetBudgetSourcesJson(),
|
||||
RewardStatus: draw.GetRewardStatus(),
|
||||
RTPWindowIndex: draw.GetRtpWindowIndex(),
|
||||
GiftRTPWindowIndex: draw.GetGiftRtpWindowIndex(),
|
||||
GlobalBaseRTPPPM: draw.GetGlobalBaseRtpPpm(),
|
||||
GiftBaseRTPPPM: draw.GetGiftBaseRtpPpm(),
|
||||
StageFeedback: draw.GetStageFeedback(),
|
||||
HighMultiplier: draw.GetHighMultiplier(),
|
||||
CreatedAtMS: draw.GetCreatedAtMs(),
|
||||
DrawID: draw.GetDrawId(),
|
||||
CommandID: draw.GetCommandId(),
|
||||
PoolID: draw.GetPoolId(),
|
||||
GiftID: draw.GetGiftId(),
|
||||
RuleVersion: draw.GetRuleVersion(),
|
||||
ExperiencePool: draw.GetExperiencePool(),
|
||||
SelectedTierID: draw.GetSelectedTierId(),
|
||||
MultiplierPPM: draw.GetMultiplierPpm(),
|
||||
BaseRewardCoins: draw.GetBaseRewardCoins(),
|
||||
EffectiveRewardCoins: draw.GetEffectiveRewardCoins(),
|
||||
RewardStatus: draw.GetRewardStatus(),
|
||||
RTPWindowIndex: draw.GetRtpWindowIndex(),
|
||||
GiftRTPWindowIndex: draw.GetGiftRtpWindowIndex(),
|
||||
GlobalBaseRTPPPM: draw.GetGlobalBaseRtpPpm(),
|
||||
GiftBaseRTPPPM: draw.GetGiftBaseRtpPpm(),
|
||||
StageFeedback: draw.GetStageFeedback(),
|
||||
HighMultiplier: draw.GetHighMultiplier(),
|
||||
CreatedAtMS: draw.GetCreatedAtMs(),
|
||||
AppCode: draw.GetAppCode(),
|
||||
UserID: draw.GetUserId(),
|
||||
ExternalUserID: draw.GetExternalUserId(),
|
||||
}
|
||||
}
|
||||
|
||||
func drawSummaryFromProto(summary *activityv1.LuckyGiftDrawSummary) drawSummaryDTO {
|
||||
func drawSummaryFromProto(summary *luckygiftv1.LuckyGiftDrawSummary) drawSummaryDTO {
|
||||
if summary == nil {
|
||||
return drawSummaryDTO{}
|
||||
}
|
||||
return drawSummaryDTO{
|
||||
PoolID: summary.GetPoolId(),
|
||||
TotalDraws: summary.GetTotalDraws(),
|
||||
UniqueUsers: summary.GetUniqueUsers(),
|
||||
UniqueRooms: summary.GetUniqueRooms(),
|
||||
TotalSpentCoins: summary.GetTotalSpentCoins(),
|
||||
TotalRewardCoins: summary.GetTotalRewardCoins(),
|
||||
BaseRewardCoins: summary.GetBaseRewardCoins(),
|
||||
RoomAtmosphereRewardCoins: summary.GetRoomAtmosphereRewardCoins(),
|
||||
ActivitySubsidyCoins: summary.GetActivitySubsidyCoins(),
|
||||
ActualRTPPPM: summary.GetActualRtpPpm(),
|
||||
PendingDraws: summary.GetPendingDraws(),
|
||||
GrantedDraws: summary.GetGrantedDraws(),
|
||||
FailedDraws: summary.GetFailedDraws(),
|
||||
PoolID: summary.GetPoolId(),
|
||||
TotalDraws: summary.GetTotalDraws(),
|
||||
UniqueUsers: summary.GetUniqueUsers(),
|
||||
UniqueRooms: summary.GetUniqueRooms(),
|
||||
TotalSpentCoins: summary.GetTotalSpentCoins(),
|
||||
TotalRewardCoins: summary.GetTotalRewardCoins(),
|
||||
BaseRewardCoins: summary.GetBaseRewardCoins(),
|
||||
ActualRTPPPM: summary.GetActualRtpPpm(),
|
||||
PendingDraws: summary.GetPendingDraws(),
|
||||
GrantedDraws: summary.GetGrantedDraws(),
|
||||
FailedDraws: summary.GetFailedDraws(),
|
||||
}
|
||||
}
|
||||
|
||||
func poolBalanceFromProto(pool *luckygiftv1.LuckyGiftPoolBalance) poolBalanceDTO {
|
||||
if pool == nil {
|
||||
return poolBalanceDTO{}
|
||||
}
|
||||
return poolBalanceDTO{
|
||||
AppCode: pool.GetAppCode(),
|
||||
PoolID: pool.GetPoolId(),
|
||||
Balance: pool.GetBalance(),
|
||||
ReserveFloor: pool.GetReserveFloor(),
|
||||
AvailableBalance: pool.GetAvailableBalance(),
|
||||
TotalIn: pool.GetTotalIn(),
|
||||
TotalOut: pool.GetTotalOut(),
|
||||
Materialized: pool.GetMaterialized(),
|
||||
UpdatedAtMS: pool.GetUpdatedAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -426,3 +486,12 @@ func parseOptionalInt64(value string) int64 {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseOptionalBool(value string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "1", "true", "yes", "y":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ package luckygift
|
||||
import (
|
||||
"testing"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
|
||||
)
|
||||
|
||||
func TestConfigToProtoConvertsBusinessUnitsToPPM(t *testing.T) {
|
||||
@ -25,8 +25,6 @@ func TestConfigToProtoConvertsBusinessUnitsToPPM(t *testing.T) {
|
||||
TierID: " novice_0_3x ",
|
||||
Multiplier: 0.3,
|
||||
ProbabilityPercent: 22,
|
||||
RewardSource: " base_rtp ",
|
||||
BroadcastLevel: " room ",
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
@ -71,10 +69,10 @@ func TestConfigToProtoGeneratesUniqueTierIDs(t *testing.T) {
|
||||
{
|
||||
Stage: "advanced",
|
||||
Tiers: []tierDTO{
|
||||
{TierID: "manual_one", Multiplier: 0, ProbabilityPercent: 50, RewardSource: "base_rtp", Enabled: true},
|
||||
{TierID: "manual_two", Multiplier: 5, ProbabilityPercent: 25, RewardSource: "base_rtp", Enabled: true},
|
||||
{TierID: "manual_two", Multiplier: 5, ProbabilityPercent: 25, RewardSource: "base_rtp", Enabled: true},
|
||||
{TierID: "manual_decimal", Multiplier: 0.5, ProbabilityPercent: 0, RewardSource: "base_rtp"},
|
||||
{TierID: "manual_one", Multiplier: 0, ProbabilityPercent: 50, Enabled: true},
|
||||
{TierID: "manual_two", Multiplier: 5, ProbabilityPercent: 25, Enabled: true},
|
||||
{TierID: "manual_two", Multiplier: 5, ProbabilityPercent: 25, Enabled: true},
|
||||
{TierID: "manual_decimal", Multiplier: 0.5, ProbabilityPercent: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -91,7 +89,7 @@ func TestConfigToProtoGeneratesUniqueTierIDs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigFromProtoConvertsPPMToBusinessUnits(t *testing.T) {
|
||||
config := configFromProto(&activityv1.LuckyGiftRuleConfig{
|
||||
config := configFromProto(&luckygiftv1.LuckyGiftRuleConfig{
|
||||
AppCode: "hyapp",
|
||||
PoolId: "lucky_100",
|
||||
RuleVersion: 7,
|
||||
@ -103,25 +101,17 @@ func TestConfigFromProtoConvertsPPMToBusinessUnits(t *testing.T) {
|
||||
GiftPriceReference: 100,
|
||||
NoviceMaxEquivalentDraws: 2_000,
|
||||
NormalMaxEquivalentDraws: 20_000,
|
||||
MaxSinglePayout: 50_000,
|
||||
UserHourlyPayoutCap: 34_200,
|
||||
UserDailyPayoutCap: 615_600,
|
||||
DeviceDailyPayoutCap: 1_026_000,
|
||||
RoomHourlyPayoutCap: 684_000,
|
||||
AnchorDailyPayoutCap: 12_312_000,
|
||||
CreatedByAdminId: 12,
|
||||
CreatedAtMs: 1_700_000_000_000,
|
||||
Stages: []*activityv1.LuckyGiftRuleStage{
|
||||
Stages: []*luckygiftv1.LuckyGiftRuleStage{
|
||||
{
|
||||
Stage: "novice",
|
||||
Tiers: []*activityv1.LuckyGiftRuleTier{
|
||||
Tiers: []*luckygiftv1.LuckyGiftRuleTier{
|
||||
{
|
||||
TierId: "novice_0_3x",
|
||||
MultiplierPpm: 300_000,
|
||||
BaseWeightPpm: 220_000,
|
||||
RewardSource: "base_rtp",
|
||||
BroadcastLevel: "room",
|
||||
Enabled: true,
|
||||
TierId: "novice_0_3x",
|
||||
MultiplierPpm: 300_000,
|
||||
BaseWeightPpm: 220_000,
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -137,9 +127,6 @@ func TestConfigFromProtoConvertsPPMToBusinessUnits(t *testing.T) {
|
||||
if config.ControlBandPercent != 1 {
|
||||
t.Fatalf("control band percent = %v, want 1", config.ControlBandPercent)
|
||||
}
|
||||
if config.RoomHourlyPayoutCap != 684_000 || config.AnchorDailyPayoutCap != 12_312_000 {
|
||||
t.Fatalf("risk caps were not copied from proto: room=%d anchor=%d", config.RoomHourlyPayoutCap, config.AnchorDailyPayoutCap)
|
||||
}
|
||||
if config.NoviceMaxEquivalentDraws != 2_000 || config.NormalMaxEquivalentDraws != 20_000 {
|
||||
t.Fatalf("equivalent draw thresholds were not copied from proto: novice=%d normal=%d", config.NoviceMaxEquivalentDraws, config.NormalMaxEquivalentDraws)
|
||||
}
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
package luckygift
|
||||
|
||||
import (
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 幸运礼物当前只保留 v2 规则接口:外部路径显式带 v2,避免后台或前端再误接旧 gift_id 规则模型。
|
||||
protected.GET("/admin/activity/lucky-gifts/v2/config", middleware.RequirePermission("lucky-gift:view"), h.GetLuckyGiftConfig)
|
||||
protected.PUT("/admin/activity/lucky-gifts/v2/config", middleware.RequirePermission("lucky-gift:update"), h.UpsertLuckyGiftConfig)
|
||||
protected.GET("/admin/activity/lucky-gifts/v2/configs", middleware.RequirePermission("lucky-gift:view"), h.ListLuckyGiftConfigs)
|
||||
protected.GET("/admin/activity/lucky-gifts/v2/draws", middleware.RequirePermission("lucky-gift:view"), h.ListLuckyGiftDraws)
|
||||
protected.GET("/admin/activity/lucky-gifts/v2/draw-summary", middleware.RequirePermission("lucky-gift:view"), h.GetLuckyGiftDrawSummary)
|
||||
}
|
||||
125
server/admin/internal/modules/opscenter/handler.go
Normal file
125
server/admin/internal/modules/opscenter/handler.go
Normal file
@ -0,0 +1,125 @@
|
||||
package opscenter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/modules/appregistry"
|
||||
"hyapp-admin-server/internal/modules/luckygift"
|
||||
"hyapp-admin-server/internal/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
apps *appregistry.Service
|
||||
luckyGift *luckygift.Handler
|
||||
}
|
||||
|
||||
type appDTO struct {
|
||||
AppID int64 `json:"app_id"`
|
||||
AppCode string `json:"app_code"`
|
||||
AppName string `json:"app_name"`
|
||||
PackageName string `json:"package_name"`
|
||||
Platform string `json:"platform"`
|
||||
Status string `json:"status"`
|
||||
Source string `json:"source"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
func New(apps *appregistry.Service, luckyGift *luckygift.Handler) *Handler {
|
||||
return &Handler{apps: apps, luckyGift: luckyGift}
|
||||
}
|
||||
|
||||
func (h *Handler) ListApps(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
items := make([]appDTO, 0, 4)
|
||||
if h.apps != nil {
|
||||
registryApps, err := h.apps.ListApps(ctx)
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取应用列表失败")
|
||||
return
|
||||
}
|
||||
for _, item := range registryApps {
|
||||
items = append(items, appDTO{
|
||||
AppID: item.AppID,
|
||||
AppCode: strings.ToLower(strings.TrimSpace(item.AppCode)),
|
||||
AppName: strings.TrimSpace(item.AppName),
|
||||
PackageName: strings.TrimSpace(item.PackageName),
|
||||
Platform: strings.TrimSpace(item.Platform),
|
||||
Status: strings.TrimSpace(item.Status),
|
||||
Source: "registry",
|
||||
CreatedAtMS: item.CreatedAtMs,
|
||||
UpdatedAtMS: item.UpdatedAtMs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ops-center 的外部幸运礼物接入只认 app_code。这里强制补齐 yumi/aslan,使运营首屏能配置外部 App,
|
||||
// 但不反向写入 admin app registry,避免把临时接入白名单伪装成完整 App 主数据。
|
||||
nowMS := time.Now().UTC().UnixMilli()
|
||||
items = append(items,
|
||||
appDTO{AppCode: "yumi", AppName: "Yumi", Status: "active", Source: "fixed", CreatedAtMS: nowMS, UpdatedAtMS: nowMS},
|
||||
appDTO{AppCode: "aslan", AppName: "Aslan", Status: "active", Source: "fixed", CreatedAtMS: nowMS, UpdatedAtMS: nowMS},
|
||||
)
|
||||
|
||||
items = filterApps(dedupeApps(items), c.Query("app_code"), c.Query("status"))
|
||||
response.OK(c, gin.H{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
func filterApps(items []appDTO, appCode string, status string) []appDTO {
|
||||
appCode = strings.ToLower(strings.TrimSpace(appCode))
|
||||
status = strings.ToLower(strings.TrimSpace(status))
|
||||
if appCode == "" && status == "" {
|
||||
return items
|
||||
}
|
||||
filtered := make([]appDTO, 0, len(items))
|
||||
for _, item := range items {
|
||||
if appCode != "" && !strings.Contains(strings.ToLower(item.AppCode), appCode) {
|
||||
continue
|
||||
}
|
||||
if status != "" && strings.ToLower(item.Status) != status {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func dedupeApps(items []appDTO) []appDTO {
|
||||
seen := make(map[string]appDTO, len(items))
|
||||
for _, item := range items {
|
||||
code := strings.ToLower(strings.TrimSpace(item.AppCode))
|
||||
if code == "" {
|
||||
continue
|
||||
}
|
||||
item.AppCode = code
|
||||
if item.AppName == "" {
|
||||
item.AppName = code
|
||||
}
|
||||
if item.Status == "" {
|
||||
item.Status = "active"
|
||||
}
|
||||
// registry 里已存在同 app_code 时以真实主数据为准;固定 app 只补缺口,不覆盖包名、平台和时间戳。
|
||||
if existing, ok := seen[code]; ok && existing.Source == "registry" {
|
||||
continue
|
||||
}
|
||||
seen[code] = item
|
||||
}
|
||||
result := make([]appDTO, 0, len(seen))
|
||||
for _, item := range seen {
|
||||
result = append(result, item)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Source != result[j].Source {
|
||||
return result[i].Source == "registry"
|
||||
}
|
||||
return result[i].AppCode < result[j].AppCode
|
||||
})
|
||||
return result
|
||||
}
|
||||
32
server/admin/internal/modules/opscenter/handler_test.go
Normal file
32
server/admin/internal/modules/opscenter/handler_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package opscenter
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDedupeAppsKeepsRegistryAndAddsFixedApps(t *testing.T) {
|
||||
items := dedupeApps([]appDTO{
|
||||
{AppCode: "YUMI", AppName: "Yumi From Registry", Source: "registry", Status: "active", PackageName: "com.yumi"},
|
||||
{AppCode: "yumi", AppName: "Yumi", Source: "fixed", Status: "active"},
|
||||
{AppCode: "aslan", AppName: "Aslan", Source: "fixed", Status: "active"},
|
||||
})
|
||||
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("items len = %d, want 2: %+v", len(items), items)
|
||||
}
|
||||
if items[0].AppCode != "yumi" || items[0].Source != "registry" || items[0].PackageName != "com.yumi" {
|
||||
t.Fatalf("registry app should win duplicate app_code, got %+v", items[0])
|
||||
}
|
||||
if items[1].AppCode != "aslan" || items[1].Source != "fixed" {
|
||||
t.Fatalf("fixed app not appended correctly, got %+v", items[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterAppsByAppCodeAndStatus(t *testing.T) {
|
||||
items := filterApps([]appDTO{
|
||||
{AppCode: "aslan", Status: "active"},
|
||||
{AppCode: "yumi", Status: "disabled"},
|
||||
}, "yu", "disabled")
|
||||
|
||||
if len(items) != 1 || items[0].AppCode != "yumi" {
|
||||
t.Fatalf("filter result = %+v, want only yumi", items)
|
||||
}
|
||||
}
|
||||
28
server/admin/internal/modules/opscenter/routes.go
Normal file
28
server/admin/internal/modules/opscenter/routes.go
Normal file
@ -0,0 +1,28 @@
|
||||
package opscenter
|
||||
|
||||
import (
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
|
||||
group := protected.Group("/admin/ops-center")
|
||||
group.GET("/apps", h.ListApps)
|
||||
if h.luckyGift == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// ops-center 是 admin-server 的衍生平台,沿用 admin 登录、审计和 lucky-gift 权限;
|
||||
// 路由层只换平台路径,不复制幸运礼物业务逻辑,避免形成第二套管理 API。
|
||||
group.GET("/lucky-gifts/config", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.GetLuckyGiftConfig)
|
||||
group.PUT("/lucky-gifts/config", middleware.RequirePermission("lucky-gift:update"), h.luckyGift.UpsertLuckyGiftConfig)
|
||||
group.GET("/lucky-gifts/configs", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftConfigs)
|
||||
group.GET("/lucky-gifts/draws", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftDraws)
|
||||
group.GET("/lucky-gifts/summary", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.GetLuckyGiftDrawSummary)
|
||||
group.GET("/lucky-gifts/pools", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftPoolBalances)
|
||||
}
|
||||
69
server/admin/internal/modules/policyconfig/dto.go
Normal file
69
server/admin/internal/modules/policyconfig/dto.go
Normal file
@ -0,0 +1,69 @@
|
||||
package policyconfig
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type templateRequest struct {
|
||||
TemplateCode string `json:"template_code"`
|
||||
TemplateVersion string `json:"template_version"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
RuleJSON json.RawMessage `json:"rule_json"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type instanceRequest struct {
|
||||
InstanceCode string `json:"instance_code"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
TemplateVersion string `json:"template_version"`
|
||||
RegionScope string `json:"region_scope"`
|
||||
Status string `json:"status"`
|
||||
EffectiveFromMS int64 `json:"effective_from_ms"`
|
||||
EffectiveToMS int64 `json:"effective_to_ms"`
|
||||
}
|
||||
|
||||
type statusRequest struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type templateDTO struct {
|
||||
TemplateID uint64 `json:"template_id"`
|
||||
VersionID uint64 `json:"version_id"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
TemplateVersion string `json:"template_version"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
RuleJSON json.RawMessage `json:"rule_json"`
|
||||
Description string `json:"description"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
type instanceDTO struct {
|
||||
InstanceID uint64 `json:"instance_id"`
|
||||
AppCode string `json:"app_code"`
|
||||
InstanceCode string `json:"instance_code"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
TemplateVersion string `json:"template_version"`
|
||||
RegionScope string `json:"region_scope"`
|
||||
Status string `json:"status"`
|
||||
EffectiveFromMS int64 `json:"effective_from_ms"`
|
||||
EffectiveToMS int64 `json:"effective_to_ms"`
|
||||
PublishStatus string `json:"publish_status"`
|
||||
PublishError string `json:"publish_error"`
|
||||
LastPublishedAtMS int64 `json:"last_published_at_ms"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
type publishDTO struct {
|
||||
JobID uint64 `json:"job_id"`
|
||||
InstanceID uint64 `json:"instance_id"`
|
||||
AppCode string `json:"app_code"`
|
||||
InstanceCode string `json:"instance_code"`
|
||||
TemplateCode string `json:"template_code"`
|
||||
TemplateVersion string `json:"template_version"`
|
||||
Status string `json:"status"`
|
||||
TargetCount int `json:"target_count"`
|
||||
PublishedRegionID []int64 `json:"published_region_ids"`
|
||||
PublishedAtMS int64 `json:"published_at_ms"`
|
||||
}
|
||||
127
server/admin/internal/modules/policyconfig/handler.go
Normal file
127
server/admin/internal/modules/policyconfig/handler.go
Normal file
@ -0,0 +1,127 @@
|
||||
package policyconfig
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
"hyapp-admin-server/internal/modules/shared"
|
||||
"hyapp-admin-server/internal/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
audit shared.OperationLogger
|
||||
}
|
||||
|
||||
func New(adminDB *sql.DB, walletDB *sql.DB, userDB *sql.DB, activity activityclient.Client, audit shared.OperationLogger) *Handler {
|
||||
return &Handler{service: NewService(adminDB, walletDB, userDB, activity), audit: audit}
|
||||
}
|
||||
|
||||
func (h *Handler) ListTemplates(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
items, total, err := h.service.ListTemplates(c.Request.Context(), listTemplatesQuery{
|
||||
Keyword: strings.TrimSpace(options.Keyword),
|
||||
Status: strings.TrimSpace(options.Status),
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取收益政策模板失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: total})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateTemplate(c *gin.Context) {
|
||||
var req templateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "收益政策模板参数不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.CreateTemplate(c.Request.Context(), shared.ActorFromContext(c).UserID, req)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
shared.OperationLogWithResourceID(c, h.audit, "create-policy-template", "admin_policy_templates", item.TemplateCode+":"+item.TemplateVersion, "success", item.Name)
|
||||
response.Created(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) ListInstances(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
items, total, err := h.service.ListInstances(c.Request.Context(), appctx.FromContext(c.Request.Context()), listInstancesQuery{
|
||||
Keyword: strings.TrimSpace(options.Keyword),
|
||||
Status: strings.TrimSpace(options.Status),
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取收益政策实例失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: total})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateInstance(c *gin.Context) {
|
||||
var req instanceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "收益政策实例参数不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.CreateInstance(c.Request.Context(), appctx.FromContext(c.Request.Context()), shared.ActorFromContext(c).UserID, req)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
shared.OperationLogWithResourceID(c, h.audit, "create-policy-instance", "admin_policy_instances", strconv.FormatUint(item.InstanceID, 10), "success", fmt.Sprintf("app_code=%s", item.AppCode))
|
||||
response.Created(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) PublishInstance(c *gin.Context) {
|
||||
instanceID, ok := parseUintParam(c, "instance_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
result, err := h.service.PublishInstance(c.Request.Context(), appctx.FromContext(c.Request.Context()), shared.ActorFromContext(c).UserID, instanceID)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
shared.OperationLogWithResourceID(c, h.audit, "publish-policy-instance", "admin_policy_instances", strconv.FormatUint(instanceID, 10), "success", fmt.Sprintf("targets=%d", result.TargetCount))
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateInstanceStatus(c *gin.Context) {
|
||||
instanceID, ok := parseUintParam(c, "instance_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req statusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "收益政策实例状态参数不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.UpdateInstanceStatus(c.Request.Context(), appctx.FromContext(c.Request.Context()), shared.ActorFromContext(c).UserID, instanceID, req.Status)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
shared.OperationLogWithResourceID(c, h.audit, "update-policy-instance-status", "admin_policy_instances", strconv.FormatUint(instanceID, 10), "success", item.Status)
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func parseUintParam(c *gin.Context, key string) (uint64, bool) {
|
||||
value := strings.TrimSpace(c.Param(key))
|
||||
parsed, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil || parsed == 0 {
|
||||
response.BadRequest(c, "ID 参数不正确")
|
||||
return 0, false
|
||||
}
|
||||
return parsed, true
|
||||
}
|
||||
20
server/admin/internal/modules/policyconfig/routes.go
Normal file
20
server/admin/internal/modules/policyconfig/routes.go
Normal file
@ -0,0 +1,20 @@
|
||||
package policyconfig
|
||||
|
||||
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/policy-templates", middleware.RequirePermission("policy-template:view"), h.ListTemplates)
|
||||
protected.POST("/admin/policy-templates", middleware.RequirePermission("policy-template:create"), h.CreateTemplate)
|
||||
protected.GET("/admin/policy-instances", middleware.RequirePermission("policy-template:view"), h.ListInstances)
|
||||
protected.POST("/admin/policy-instances", middleware.RequirePermission("policy-instance:create"), h.CreateInstance)
|
||||
protected.POST("/admin/policy-instances/:instance_id/publish", middleware.RequirePermission("policy-instance:publish"), h.PublishInstance)
|
||||
protected.PUT("/admin/policy-instances/:instance_id/status", middleware.RequirePermission("policy-instance:update"), h.UpdateInstanceStatus)
|
||||
}
|
||||
829
server/admin/internal/modules/policyconfig/service.go
Normal file
829
server/admin/internal/modules/policyconfig/service.go
Normal file
@ -0,0 +1,829 @@
|
||||
package policyconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
policyStatusActive = "active"
|
||||
policyStatusDisabled = "disabled"
|
||||
|
||||
publishStatusDraft = "draft"
|
||||
publishStatusPublished = "published"
|
||||
publishStatusFailed = "failed"
|
||||
|
||||
policyTaskRewardAssetCoin = "COIN"
|
||||
|
||||
defaultPolicyTemplateCode = "first_google70000_coin_seller_92000_100000_v1"
|
||||
defaultPolicyTemplateVersion = "v1"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
adminDB *sql.DB
|
||||
walletDB *sql.DB
|
||||
userDB *sql.DB
|
||||
activity activityclient.Client
|
||||
}
|
||||
|
||||
type listTemplatesQuery struct {
|
||||
Keyword string
|
||||
Status string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type listInstancesQuery struct {
|
||||
Keyword string
|
||||
Status string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type policyInstanceRow struct {
|
||||
InstanceID uint64
|
||||
AppCode string
|
||||
InstanceCode string
|
||||
TemplateCode string
|
||||
TemplateVersion string
|
||||
RegionScope string
|
||||
Status string
|
||||
EffectiveFromMS int64
|
||||
EffectiveToMS int64
|
||||
}
|
||||
|
||||
type compiledPolicy struct {
|
||||
HostPointRatioPPM int64
|
||||
PointsPerUSD int64
|
||||
WithdrawFeeBPS int64
|
||||
TaskRewardAssetType string
|
||||
RuleJSON json.RawMessage
|
||||
}
|
||||
|
||||
func NewService(adminDB *sql.DB, walletDB *sql.DB, userDB *sql.DB, activity activityclient.Client) *Service {
|
||||
return &Service{adminDB: adminDB, walletDB: walletDB, userDB: userDB, activity: activity}
|
||||
}
|
||||
|
||||
func (s *Service) ListTemplates(ctx context.Context, query listTemplatesQuery) ([]templateDTO, int64, error) {
|
||||
if s == nil || s.adminDB == nil {
|
||||
return nil, 0, errors.New("admin database is not configured")
|
||||
}
|
||||
where, args := templateWhere(query)
|
||||
var total int64
|
||||
if err := s.adminDB.QueryRowContext(ctx, `SELECT COUNT(*) FROM admin_policy_templates t JOIN admin_policy_template_versions v ON v.template_code = t.template_code AND v.template_version = t.template_version `+where, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
page, pageSize := normalizePage(query.Page, query.PageSize)
|
||||
args = append(args, pageSize, (page-1)*pageSize)
|
||||
rows, err := s.adminDB.QueryContext(ctx, `
|
||||
SELECT t.template_id, v.version_id, t.template_code, v.template_version, t.name, v.status,
|
||||
COALESCE(CAST(v.rule_json AS CHAR), '{}'), t.description, v.created_at_ms, v.updated_at_ms
|
||||
FROM admin_policy_templates t
|
||||
JOIN admin_policy_template_versions v ON v.template_code = t.template_code AND v.template_version = t.template_version `+where+`
|
||||
ORDER BY t.updated_at_ms DESC, v.updated_at_ms DESC
|
||||
LIMIT ? OFFSET ?`, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]templateDTO, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanTemplate(rows)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, total, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Service) CreateTemplate(ctx context.Context, actorID uint, req templateRequest) (templateDTO, error) {
|
||||
if s == nil || s.adminDB == nil {
|
||||
return templateDTO{}, errors.New("admin database is not configured")
|
||||
}
|
||||
templateCode := normalizeCode(req.TemplateCode)
|
||||
version := normalizeVersion(req.TemplateVersion)
|
||||
name := strings.TrimSpace(req.Name)
|
||||
status := normalizeStatus(req.Status)
|
||||
if templateCode == "" || version == "" || name == "" {
|
||||
return templateDTO{}, errors.New("template_code, template_version and name are required")
|
||||
}
|
||||
ruleJSON, err := normalizeRuleJSON(req.RuleJSON)
|
||||
if err != nil {
|
||||
return templateDTO{}, err
|
||||
}
|
||||
nowMS := time.Now().UTC().UnixMilli()
|
||||
tx, err := s.adminDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return templateDTO{}, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO admin_policy_templates (
|
||||
template_code, template_version, name, status, rule_json, description,
|
||||
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name), status = VALUES(status), rule_json = VALUES(rule_json),
|
||||
description = VALUES(description), updated_by_admin_id = VALUES(updated_by_admin_id), updated_at_ms = VALUES(updated_at_ms)`,
|
||||
templateCode, version, name, status, string(ruleJSON), strings.TrimSpace(req.Description), actorID, actorID, nowMS, nowMS,
|
||||
); err != nil {
|
||||
return templateDTO{}, err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO admin_policy_template_versions (
|
||||
template_code, template_version, status, rule_json,
|
||||
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status = VALUES(status), rule_json = VALUES(rule_json),
|
||||
updated_by_admin_id = VALUES(updated_by_admin_id), updated_at_ms = VALUES(updated_at_ms)`,
|
||||
templateCode, version, status, string(ruleJSON), actorID, actorID, nowMS, nowMS,
|
||||
); err != nil {
|
||||
return templateDTO{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return templateDTO{}, err
|
||||
}
|
||||
return s.getTemplate(ctx, templateCode, version)
|
||||
}
|
||||
|
||||
func (s *Service) ListInstances(ctx context.Context, appCode string, query listInstancesQuery) ([]instanceDTO, int64, error) {
|
||||
if s == nil || s.adminDB == nil {
|
||||
return nil, 0, errors.New("admin database is not configured")
|
||||
}
|
||||
appCode = appctx.Normalize(appCode)
|
||||
where, args := instanceWhere(appCode, query)
|
||||
var total int64
|
||||
if err := s.adminDB.QueryRowContext(ctx, `SELECT COUNT(*) FROM admin_policy_instances `+where, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
page, pageSize := normalizePage(query.Page, query.PageSize)
|
||||
args = append(args, pageSize, (page-1)*pageSize)
|
||||
rows, err := s.adminDB.QueryContext(ctx, `
|
||||
SELECT instance_id, app_code, instance_code, template_code, template_version, region_scope, status,
|
||||
effective_from_ms, effective_to_ms, publish_status, publish_error, last_published_at_ms,
|
||||
created_at_ms, updated_at_ms
|
||||
FROM admin_policy_instances `+where+`
|
||||
ORDER BY updated_at_ms DESC, instance_id DESC
|
||||
LIMIT ? OFFSET ?`, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]instanceDTO, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanInstanceDTO(rows)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, total, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Service) CreateInstance(ctx context.Context, appCode string, actorID uint, req instanceRequest) (instanceDTO, error) {
|
||||
if s == nil || s.adminDB == nil {
|
||||
return instanceDTO{}, errors.New("admin database is not configured")
|
||||
}
|
||||
appCode = appctx.Normalize(appCode)
|
||||
instanceCode := normalizeCode(req.InstanceCode)
|
||||
templateCode := normalizeCode(req.TemplateCode)
|
||||
version := normalizeVersion(req.TemplateVersion)
|
||||
if instanceCode == "" || templateCode == "" || version == "" {
|
||||
return instanceDTO{}, errors.New("instance_code, template_code and template_version are required")
|
||||
}
|
||||
if _, err := s.getTemplate(ctx, templateCode, version); err != nil {
|
||||
return instanceDTO{}, err
|
||||
}
|
||||
status := normalizeStatus(req.Status)
|
||||
regionScope := normalizeRegionScope(req.RegionScope)
|
||||
if err := validateEffectiveRange(req.EffectiveFromMS, req.EffectiveToMS); err != nil {
|
||||
return instanceDTO{}, err
|
||||
}
|
||||
nowMS := time.Now().UTC().UnixMilli()
|
||||
effectiveFromMS := req.EffectiveFromMS
|
||||
if effectiveFromMS <= 0 {
|
||||
effectiveFromMS = nowMS
|
||||
}
|
||||
result, err := s.adminDB.ExecContext(ctx, `
|
||||
INSERT INTO admin_policy_instances (
|
||||
app_code, instance_code, template_code, template_version, region_scope, status,
|
||||
effective_from_ms, effective_to_ms, publish_status, created_by_admin_id, updated_by_admin_id,
|
||||
created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
appCode, instanceCode, templateCode, version, regionScope, status,
|
||||
effectiveFromMS, req.EffectiveToMS, publishStatusDraft, actorID, actorID, nowMS, nowMS,
|
||||
)
|
||||
if err != nil {
|
||||
return instanceDTO{}, err
|
||||
}
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return instanceDTO{}, err
|
||||
}
|
||||
return s.getInstanceDTO(ctx, appCode, uint64(id))
|
||||
}
|
||||
|
||||
func (s *Service) UpdateInstanceStatus(ctx context.Context, appCode string, actorID uint, instanceID uint64, status string) (instanceDTO, error) {
|
||||
if s == nil || s.adminDB == nil {
|
||||
return instanceDTO{}, errors.New("admin database is not configured")
|
||||
}
|
||||
appCode = appctx.Normalize(appCode)
|
||||
status = normalizeStatus(status)
|
||||
nowMS := time.Now().UTC().UnixMilli()
|
||||
instance, err := s.getInstanceRow(ctx, appCode, instanceID)
|
||||
if err != nil {
|
||||
return instanceDTO{}, err
|
||||
}
|
||||
ruleJSON, err := s.getTemplateRuleJSONSnapshot(ctx, instance.TemplateCode, instance.TemplateVersion)
|
||||
if err != nil {
|
||||
return instanceDTO{}, err
|
||||
}
|
||||
compiled, err := compilePolicy(ruleJSON)
|
||||
if err != nil {
|
||||
return instanceDTO{}, err
|
||||
}
|
||||
result, err := s.adminDB.ExecContext(ctx, `
|
||||
UPDATE admin_policy_instances
|
||||
SET status = ?, updated_by_admin_id = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND instance_id = ?`,
|
||||
status, actorID, nowMS, appCode, instanceID,
|
||||
)
|
||||
if err != nil {
|
||||
return instanceDTO{}, err
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return instanceDTO{}, err
|
||||
}
|
||||
if affected == 0 {
|
||||
return instanceDTO{}, errors.New("policy instance not found")
|
||||
}
|
||||
instance.Status = status
|
||||
if err := s.syncWalletRuntimeStatus(ctx, instance, status, nowMS); err != nil {
|
||||
return instanceDTO{}, err
|
||||
}
|
||||
if err := s.publishActivityRuntime(ctx, instance, compiled, actorID, nowMS); err != nil {
|
||||
return instanceDTO{}, err
|
||||
}
|
||||
return s.getInstanceDTO(ctx, appCode, instanceID)
|
||||
}
|
||||
|
||||
func (s *Service) PublishInstance(ctx context.Context, appCode string, actorID uint, instanceID uint64) (publishDTO, error) {
|
||||
if s == nil || s.adminDB == nil || s.walletDB == nil || s.userDB == nil || s.activity == nil {
|
||||
return publishDTO{}, errors.New("policy publish database is not configured")
|
||||
}
|
||||
appCode = appctx.Normalize(appCode)
|
||||
instance, err := s.getInstanceRow(ctx, appCode, instanceID)
|
||||
if err != nil {
|
||||
return publishDTO{}, err
|
||||
}
|
||||
if instance.Status != policyStatusActive {
|
||||
return publishDTO{}, errors.New("only active policy instance can be published")
|
||||
}
|
||||
ruleJSON, err := s.getTemplateRuleJSON(ctx, instance.TemplateCode, instance.TemplateVersion)
|
||||
if err != nil {
|
||||
return publishDTO{}, err
|
||||
}
|
||||
compiled, err := compilePolicy(ruleJSON)
|
||||
if err != nil {
|
||||
return publishDTO{}, err
|
||||
}
|
||||
regionIDs, err := s.resolveRegionScope(ctx, instance.AppCode, instance.RegionScope)
|
||||
if err != nil {
|
||||
return publishDTO{}, err
|
||||
}
|
||||
nowMS := time.Now().UTC().UnixMilli()
|
||||
targetCount := len(regionIDs) + 1
|
||||
jobID, err := s.createPublishJob(ctx, instance, actorID, targetCount, nowMS)
|
||||
if err != nil {
|
||||
return publishDTO{}, err
|
||||
}
|
||||
if err := s.publishWalletRuntime(ctx, instance, compiled, regionIDs, actorID, nowMS); err != nil {
|
||||
_ = s.finishPublishJob(ctx, jobID, publishStatusFailed, targetCount, 0, targetCount, err.Error(), nowMS)
|
||||
_ = s.markInstancePublishFailed(ctx, instance.AppCode, instance.InstanceID, err.Error(), nowMS)
|
||||
return publishDTO{}, err
|
||||
}
|
||||
if err := s.publishActivityRuntime(ctx, instance, compiled, actorID, nowMS); err != nil {
|
||||
_ = s.finishPublishJob(ctx, jobID, publishStatusFailed, targetCount, len(regionIDs), 1, err.Error(), nowMS)
|
||||
_ = s.markInstancePublishFailed(ctx, instance.AppCode, instance.InstanceID, err.Error(), nowMS)
|
||||
return publishDTO{}, err
|
||||
}
|
||||
if err := s.insertPublishItems(ctx, jobID, instance.AppCode, regionIDs, true, nowMS); err != nil {
|
||||
_ = s.finishPublishJob(ctx, jobID, publishStatusFailed, targetCount, targetCount, 0, err.Error(), nowMS)
|
||||
_ = s.markInstancePublishFailed(ctx, instance.AppCode, instance.InstanceID, err.Error(), nowMS)
|
||||
return publishDTO{}, err
|
||||
}
|
||||
if err := s.finishPublishJob(ctx, jobID, "succeeded", targetCount, targetCount, 0, "", nowMS); err != nil {
|
||||
return publishDTO{}, err
|
||||
}
|
||||
if _, err := s.adminDB.ExecContext(ctx, `
|
||||
UPDATE admin_policy_instances
|
||||
SET publish_status = ?, publish_error = '', last_published_at_ms = ?, updated_by_admin_id = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND instance_id = ?`,
|
||||
publishStatusPublished, nowMS, actorID, nowMS, instance.AppCode, instance.InstanceID,
|
||||
); err != nil {
|
||||
return publishDTO{}, err
|
||||
}
|
||||
return publishDTO{
|
||||
JobID: jobID,
|
||||
InstanceID: instance.InstanceID,
|
||||
AppCode: instance.AppCode,
|
||||
InstanceCode: instance.InstanceCode,
|
||||
TemplateCode: instance.TemplateCode,
|
||||
TemplateVersion: instance.TemplateVersion,
|
||||
Status: publishStatusPublished,
|
||||
TargetCount: targetCount,
|
||||
PublishedRegionID: regionIDs,
|
||||
PublishedAtMS: nowMS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) getTemplate(ctx context.Context, templateCode string, version string) (templateDTO, error) {
|
||||
row := s.adminDB.QueryRowContext(ctx, `
|
||||
SELECT t.template_id, v.version_id, t.template_code, v.template_version, t.name, v.status,
|
||||
COALESCE(CAST(v.rule_json AS CHAR), '{}'), t.description, v.created_at_ms, v.updated_at_ms
|
||||
FROM admin_policy_templates t
|
||||
JOIN admin_policy_template_versions v ON v.template_code = t.template_code AND v.template_version = t.template_version
|
||||
WHERE t.template_code = ? AND v.template_version = ?`,
|
||||
templateCode, version,
|
||||
)
|
||||
item, err := scanTemplate(row)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return templateDTO{}, errors.New("policy template version not found")
|
||||
}
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *Service) getTemplateRuleJSON(ctx context.Context, templateCode string, version string) (json.RawMessage, error) {
|
||||
var raw string
|
||||
if err := s.adminDB.QueryRowContext(ctx, `
|
||||
SELECT COALESCE(CAST(rule_json AS CHAR), '{}')
|
||||
FROM admin_policy_template_versions
|
||||
WHERE template_code = ? AND template_version = ? AND status = ?`,
|
||||
templateCode, version, policyStatusActive,
|
||||
).Scan(&raw); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, errors.New("active policy template version not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return normalizeRuleJSON(json.RawMessage(raw))
|
||||
}
|
||||
|
||||
func (s *Service) getTemplateRuleJSONSnapshot(ctx context.Context, templateCode string, version string) (json.RawMessage, error) {
|
||||
var raw string
|
||||
if err := s.adminDB.QueryRowContext(ctx, `
|
||||
SELECT COALESCE(CAST(rule_json AS CHAR), '{}')
|
||||
FROM admin_policy_template_versions
|
||||
WHERE template_code = ? AND template_version = ?`,
|
||||
templateCode, version,
|
||||
).Scan(&raw); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, errors.New("policy template version not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return normalizeRuleJSON(json.RawMessage(raw))
|
||||
}
|
||||
|
||||
func (s *Service) getInstanceDTO(ctx context.Context, appCode string, instanceID uint64) (instanceDTO, error) {
|
||||
row := s.adminDB.QueryRowContext(ctx, `
|
||||
SELECT instance_id, app_code, instance_code, template_code, template_version, region_scope, status,
|
||||
effective_from_ms, effective_to_ms, publish_status, publish_error, last_published_at_ms,
|
||||
created_at_ms, updated_at_ms
|
||||
FROM admin_policy_instances
|
||||
WHERE app_code = ? AND instance_id = ?`,
|
||||
appCode, instanceID,
|
||||
)
|
||||
item, err := scanInstanceDTO(row)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return instanceDTO{}, errors.New("policy instance not found")
|
||||
}
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *Service) getInstanceRow(ctx context.Context, appCode string, instanceID uint64) (policyInstanceRow, error) {
|
||||
row := s.adminDB.QueryRowContext(ctx, `
|
||||
SELECT instance_id, app_code, instance_code, template_code, template_version, region_scope, status,
|
||||
effective_from_ms, effective_to_ms
|
||||
FROM admin_policy_instances
|
||||
WHERE app_code = ? AND instance_id = ?`,
|
||||
appCode, instanceID,
|
||||
)
|
||||
var item policyInstanceRow
|
||||
err := row.Scan(&item.InstanceID, &item.AppCode, &item.InstanceCode, &item.TemplateCode, &item.TemplateVersion, &item.RegionScope, &item.Status, &item.EffectiveFromMS, &item.EffectiveToMS)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return policyInstanceRow{}, errors.New("policy instance not found")
|
||||
}
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *Service) resolveRegionScope(ctx context.Context, appCode string, scope string) ([]int64, error) {
|
||||
scope = normalizeRegionScope(scope)
|
||||
if strings.HasPrefix(scope, "region_id:") {
|
||||
value := strings.TrimPrefix(scope, "region_id:")
|
||||
regionID, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || regionID <= 0 {
|
||||
return nil, errors.New("region_scope is invalid")
|
||||
}
|
||||
return []int64{regionID}, nil
|
||||
}
|
||||
rows, err := s.userDB.QueryContext(ctx, `
|
||||
SELECT region_id
|
||||
FROM regions
|
||||
WHERE app_code = ? AND status = 'active'
|
||||
ORDER BY sort_order ASC, region_id ASC`, appCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
regionIDs := []int64{0}
|
||||
for rows.Next() {
|
||||
var regionID int64
|
||||
if err := rows.Scan(®ionID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if regionID > 0 {
|
||||
regionIDs = append(regionIDs, regionID)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return regionIDs, nil
|
||||
}
|
||||
|
||||
func (s *Service) createPublishJob(ctx context.Context, item policyInstanceRow, actorID uint, targetCount int, nowMS int64) (uint64, error) {
|
||||
result, err := s.adminDB.ExecContext(ctx, `
|
||||
INSERT INTO admin_policy_publish_jobs (
|
||||
instance_id, app_code, instance_code, template_code, template_version, status,
|
||||
target_count, operator_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?)`,
|
||||
item.InstanceID, item.AppCode, item.InstanceCode, item.TemplateCode, item.TemplateVersion, targetCount, actorID, nowMS, nowMS,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
id, err := result.LastInsertId()
|
||||
return uint64(id), err
|
||||
}
|
||||
|
||||
func (s *Service) publishWalletRuntime(ctx context.Context, item policyInstanceRow, compiled compiledPolicy, regionIDs []int64, actorID uint, nowMS int64) error {
|
||||
if err := ensureWalletPolicyRuntimeTable(ctx, s.walletDB); err != nil {
|
||||
return err
|
||||
}
|
||||
tx, err := s.walletDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM wallet_policy_instances WHERE app_code = ? AND instance_code = ?`, item.AppCode, item.InstanceCode); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, regionID := range regionIDs {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO wallet_policy_instances (
|
||||
app_code, instance_code, template_code, template_version, region_id, status,
|
||||
effective_from_ms, effective_to_ms, host_point_asset_type, host_point_ratio_ppm,
|
||||
points_per_usd, withdraw_fee_bps, rule_json, published_by_admin_id,
|
||||
published_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'POINT', ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?)`,
|
||||
item.AppCode, item.InstanceCode, item.TemplateCode, item.TemplateVersion, regionID, item.Status,
|
||||
item.EffectiveFromMS, item.EffectiveToMS, compiled.HostPointRatioPPM, compiled.PointsPerUSD,
|
||||
compiled.WithdrawFeeBPS, string(compiled.RuleJSON), actorID, nowMS, nowMS, nowMS,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (s *Service) syncWalletRuntimeStatus(ctx context.Context, item policyInstanceRow, status string, nowMS int64) error {
|
||||
if s.walletDB == nil {
|
||||
return nil
|
||||
}
|
||||
if err := ensureWalletPolicyRuntimeTable(ctx, s.walletDB); err != nil {
|
||||
return err
|
||||
}
|
||||
// 运行表是 owner service 的最终读取面;后台停用实例必须同步停用旧快照,不能只改 admin 状态。
|
||||
_, err := s.walletDB.ExecContext(ctx, `
|
||||
UPDATE wallet_policy_instances
|
||||
SET status = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND instance_code = ?`,
|
||||
status, nowMS, item.AppCode, item.InstanceCode,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) publishActivityRuntime(ctx context.Context, item policyInstanceRow, compiled compiledPolicy, actorID uint, nowMS int64) error {
|
||||
if s.activity == nil {
|
||||
return nil
|
||||
}
|
||||
// activity 目前只需要 App 级任务奖励默认资产;具体任务定义仍由 activity-service 自己持久化和快照。
|
||||
_, err := s.activity.PublishTaskRewardPolicy(ctx, &activityv1.PublishTaskRewardPolicyRequest{
|
||||
Meta: &activityv1.RequestMeta{
|
||||
Caller: "admin-server",
|
||||
AppCode: item.AppCode,
|
||||
SentAtMs: nowMS,
|
||||
},
|
||||
InstanceCode: item.InstanceCode,
|
||||
TemplateCode: item.TemplateCode,
|
||||
TemplateVersion: item.TemplateVersion,
|
||||
Status: item.Status,
|
||||
RewardAssetType: compiled.TaskRewardAssetType,
|
||||
RuleJson: string(compiled.RuleJSON),
|
||||
OperatorAdminId: int64(actorID),
|
||||
PublishedAtMs: nowMS,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func ensureWalletPolicyRuntimeTable(ctx context.Context, db *sql.DB) error {
|
||||
_, err := db.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS wallet_policy_instances (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
instance_code VARCHAR(96) NOT NULL COMMENT '后台策略实例编码',
|
||||
template_code VARCHAR(96) NOT NULL COMMENT '模板编码快照',
|
||||
template_version VARCHAR(64) NOT NULL COMMENT '模板版本快照',
|
||||
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '适用地区,0 表示 App 兜底',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
|
||||
effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'UTC epoch ms',
|
||||
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '0 表示长期有效',
|
||||
host_point_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '主播收益积分资产',
|
||||
host_point_ratio_ppm BIGINT NOT NULL DEFAULT 700000 COMMENT '有效付费礼物转 POINT 比例,ppm',
|
||||
points_per_usd BIGINT NOT NULL DEFAULT 100000 COMMENT 'POINT/USD 展示换算比例',
|
||||
withdraw_fee_bps INT NOT NULL DEFAULT 500 COMMENT '提现手续费 bps',
|
||||
rule_json JSON NOT NULL COMMENT '完整政策快照',
|
||||
published_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
published_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, instance_code, region_id),
|
||||
KEY idx_wallet_policy_active (app_code, region_id, status, effective_from_ms, effective_to_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包运行侧收益政策实例表'`)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) insertPublishItems(ctx context.Context, jobID uint64, appCode string, regionIDs []int64, activitySucceeded bool, nowMS int64) error {
|
||||
tx, err := s.adminDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
for _, regionID := range regionIDs {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO admin_policy_publish_items (
|
||||
job_id, owner_service, app_code, region_id, status, created_at_ms, updated_at_ms
|
||||
) VALUES (?, 'wallet', ?, ?, 'succeeded', ?, ?)`,
|
||||
jobID, appCode, regionID, nowMS, nowMS,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
activityStatus := "failed"
|
||||
if activitySucceeded {
|
||||
activityStatus = "succeeded"
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO admin_policy_publish_items (
|
||||
job_id, owner_service, app_code, region_id, status, created_at_ms, updated_at_ms
|
||||
) VALUES (?, 'activity', ?, 0, ?, ?, ?)`,
|
||||
jobID, appCode, activityStatus, nowMS, nowMS,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (s *Service) finishPublishJob(ctx context.Context, jobID uint64, status string, targetCount int, successCount int, failureCount int, errMsg string, nowMS int64) error {
|
||||
_, err := s.adminDB.ExecContext(ctx, `
|
||||
UPDATE admin_policy_publish_jobs
|
||||
SET status = ?, target_count = ?, success_count = ?, failure_count = ?, error_message = ?, updated_at_ms = ?
|
||||
WHERE job_id = ?`,
|
||||
status, targetCount, successCount, failureCount, truncate(errMsg, 512), nowMS, jobID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) markInstancePublishFailed(ctx context.Context, appCode string, instanceID uint64, errMsg string, nowMS int64) error {
|
||||
_, err := s.adminDB.ExecContext(ctx, `
|
||||
UPDATE admin_policy_instances
|
||||
SET publish_status = ?, publish_error = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND instance_id = ?`,
|
||||
publishStatusFailed, truncate(errMsg, 512), nowMS, appCode, instanceID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func compilePolicy(raw json.RawMessage) (compiledPolicy, error) {
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil {
|
||||
return compiledPolicy{}, errors.New("rule_json is invalid")
|
||||
}
|
||||
pointsPerUSD := int64FromJSON(decoded["points_per_usd"], 100000)
|
||||
host, _ := decoded["host"].(map[string]any)
|
||||
hostRatioPercent := floatFromJSON(host["point_ratio_percent"], 70)
|
||||
withdrawFeeBPS := int64FromJSON(host["withdraw_fee_bps"], 500)
|
||||
ratioPPM := int64(math.Round(hostRatioPercent * 10000))
|
||||
tasks, _ := decoded["tasks"].(map[string]any)
|
||||
taskRewardAssetType := strings.ToUpper(strings.TrimSpace(stringFromJSON(tasks["reward_asset_type"], policyTaskRewardAssetCoin)))
|
||||
if pointsPerUSD <= 0 || ratioPPM <= 0 || ratioPPM > 1000000 || withdrawFeeBPS < 0 || withdrawFeeBPS > 10000 {
|
||||
return compiledPolicy{}, errors.New("rule_json host point policy is invalid")
|
||||
}
|
||||
if taskRewardAssetType != policyTaskRewardAssetCoin && taskRewardAssetType != "POINT" {
|
||||
return compiledPolicy{}, errors.New("rule_json task reward asset_type is invalid")
|
||||
}
|
||||
return compiledPolicy{
|
||||
HostPointRatioPPM: ratioPPM,
|
||||
PointsPerUSD: pointsPerUSD,
|
||||
WithdrawFeeBPS: withdrawFeeBPS,
|
||||
TaskRewardAssetType: taskRewardAssetType,
|
||||
RuleJSON: raw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func scanTemplate(row interface{ Scan(dest ...any) error }) (templateDTO, error) {
|
||||
var item templateDTO
|
||||
var rule string
|
||||
err := row.Scan(&item.TemplateID, &item.VersionID, &item.TemplateCode, &item.TemplateVersion, &item.Name, &item.Status, &rule, &item.Description, &item.CreatedAtMS, &item.UpdatedAtMS)
|
||||
item.RuleJSON = json.RawMessage(rule)
|
||||
return item, err
|
||||
}
|
||||
|
||||
func scanInstanceDTO(row interface{ Scan(dest ...any) error }) (instanceDTO, error) {
|
||||
var item instanceDTO
|
||||
err := row.Scan(&item.InstanceID, &item.AppCode, &item.InstanceCode, &item.TemplateCode, &item.TemplateVersion, &item.RegionScope, &item.Status, &item.EffectiveFromMS, &item.EffectiveToMS, &item.PublishStatus, &item.PublishError, &item.LastPublishedAtMS, &item.CreatedAtMS, &item.UpdatedAtMS)
|
||||
return item, err
|
||||
}
|
||||
|
||||
func templateWhere(query listTemplatesQuery) (string, []any) {
|
||||
conditions := []string{"1 = 1"}
|
||||
args := make([]any, 0)
|
||||
if query.Status != "" {
|
||||
conditions = append(conditions, "v.status = ?")
|
||||
args = append(args, normalizeStatus(query.Status))
|
||||
}
|
||||
if query.Keyword != "" {
|
||||
conditions = append(conditions, "(t.template_code LIKE ? OR t.name LIKE ?)")
|
||||
like := "%" + query.Keyword + "%"
|
||||
args = append(args, like, like)
|
||||
}
|
||||
return "WHERE " + strings.Join(conditions, " AND "), args
|
||||
}
|
||||
|
||||
func instanceWhere(appCode string, query listInstancesQuery) (string, []any) {
|
||||
conditions := []string{"app_code = ?"}
|
||||
args := []any{appCode}
|
||||
if query.Status != "" {
|
||||
conditions = append(conditions, "status = ?")
|
||||
args = append(args, normalizeStatus(query.Status))
|
||||
}
|
||||
if query.Keyword != "" {
|
||||
conditions = append(conditions, "(instance_code LIKE ? OR template_code LIKE ?)")
|
||||
like := "%" + query.Keyword + "%"
|
||||
args = append(args, like, like)
|
||||
}
|
||||
return "WHERE " + strings.Join(conditions, " AND "), args
|
||||
}
|
||||
|
||||
func normalizePage(page int, pageSize int) (int, int) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
func normalizeCode(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
value = strings.ReplaceAll(value, " ", "_")
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeVersion(value string) string {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func normalizeStatus(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", "enabled", policyStatusActive:
|
||||
return policyStatusActive
|
||||
case "inactive", policyStatusDisabled:
|
||||
return policyStatusDisabled
|
||||
default:
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRegionScope(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "all_active_regions"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func validateEffectiveRange(fromMS int64, toMS int64) error {
|
||||
if fromMS < 0 || toMS < 0 {
|
||||
return errors.New("effective time is invalid")
|
||||
}
|
||||
if toMS > 0 && fromMS > 0 && toMS <= fromMS {
|
||||
return errors.New("effective_to_ms must be greater than effective_from_ms")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeRuleJSON(raw json.RawMessage) (json.RawMessage, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, errors.New("rule_json is required")
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil || decoded == nil {
|
||||
return nil, errors.New("rule_json must be a JSON object")
|
||||
}
|
||||
normalized, err := json.Marshal(decoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.RawMessage(normalized), nil
|
||||
}
|
||||
|
||||
func int64FromJSON(value any, fallback int64) int64 {
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
if typed > 0 {
|
||||
return int64(typed)
|
||||
}
|
||||
case json.Number:
|
||||
if parsed, err := typed.Int64(); err == nil && parsed > 0 {
|
||||
return parsed
|
||||
}
|
||||
case string:
|
||||
if parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64); err == nil && parsed > 0 {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func floatFromJSON(value any, fallback float64) float64 {
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
if typed > 0 {
|
||||
return typed
|
||||
}
|
||||
case json.Number:
|
||||
if parsed, err := typed.Float64(); err == nil && parsed > 0 {
|
||||
return parsed
|
||||
}
|
||||
case string:
|
||||
if parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 64); err == nil && parsed > 0 {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func stringFromJSON(value any, fallback string) string {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
if strings.TrimSpace(typed) != "" {
|
||||
return typed
|
||||
}
|
||||
case json.Number:
|
||||
if strings.TrimSpace(typed.String()) != "" {
|
||||
return typed.String()
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func truncate(value string, limit int) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) <= limit {
|
||||
return value
|
||||
}
|
||||
return value[:limit]
|
||||
}
|
||||
|
||||
func init() {
|
||||
_ = defaultPolicyTemplateCode
|
||||
_ = defaultPolicyTemplateVersion
|
||||
}
|
||||
217
server/admin/internal/modules/policyconfig/service_test.go
Normal file
217
server/admin/internal/modules/policyconfig/service_test.go
Normal file
@ -0,0 +1,217 @@
|
||||
package policyconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
)
|
||||
|
||||
func TestGetTemplateJoinsVersionByTemplateCodeAndVersion(t *testing.T) {
|
||||
adminDB, mock, closeDB := newPolicySQLMock(t)
|
||||
defer closeDB()
|
||||
svc := NewService(adminDB, nil, nil, nil)
|
||||
|
||||
mock.ExpectQuery(`(?s)JOIN admin_policy_template_versions v ON v\.template_code = t\.template_code AND v\.template_version = t\.template_version.*WHERE t\.template_code = \? AND v\.template_version = \?`).
|
||||
WithArgs("huwaa_point_policy", "v2").
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"template_id",
|
||||
"version_id",
|
||||
"template_code",
|
||||
"template_version",
|
||||
"name",
|
||||
"status",
|
||||
"rule_json",
|
||||
"description",
|
||||
"created_at_ms",
|
||||
"updated_at_ms",
|
||||
}).AddRow(1, 22, "huwaa_point_policy", "v2", "Huwaa Point Policy", "active", `{"tasks":{"reward_asset_type":"POINT"}}`, "version scoped", int64(1700000000000), int64(1700000001000)))
|
||||
|
||||
got, err := svc.getTemplate(context.Background(), "huwaa_point_policy", "v2")
|
||||
if err != nil {
|
||||
t.Fatalf("getTemplate failed: %v", err)
|
||||
}
|
||||
if got.TemplateCode != "huwaa_point_policy" || got.TemplateVersion != "v2" || got.VersionID != 22 {
|
||||
t.Fatalf("template version join result mismatch: %+v", got)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("admin sql expectations mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishRuntimeWritesWalletRowsAndActivityTaskRewardPolicy(t *testing.T) {
|
||||
walletDB, walletMock, closeWallet := newPolicySQLMock(t)
|
||||
defer closeWallet()
|
||||
activity := &fakePolicyActivityClient{}
|
||||
svc := NewService(nil, walletDB, nil, activity)
|
||||
ctx := context.Background()
|
||||
nowMS := int64(1700000005000)
|
||||
item := policyInstanceRow{
|
||||
AppCode: "huwaa",
|
||||
InstanceCode: "huwaa-point-live",
|
||||
TemplateCode: "huwaa_point_policy",
|
||||
TemplateVersion: "v2",
|
||||
Status: policyStatusActive,
|
||||
EffectiveFromMS: 1700000000000,
|
||||
EffectiveToMS: 0,
|
||||
}
|
||||
compiled := compiledPolicy{
|
||||
HostPointRatioPPM: 700000,
|
||||
PointsPerUSD: 100000,
|
||||
WithdrawFeeBPS: 500,
|
||||
TaskRewardAssetType: "POINT",
|
||||
RuleJSON: []byte(`{"tasks":{"reward_asset_type":"POINT"},"host":{"point_ratio_percent":70}}`),
|
||||
}
|
||||
|
||||
walletMock.ExpectExec(`(?s)CREATE TABLE IF NOT EXISTS wallet_policy_instances`).WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
walletMock.ExpectBegin()
|
||||
walletMock.ExpectExec(`DELETE FROM wallet_policy_instances WHERE app_code = \? AND instance_code = \?`).
|
||||
WithArgs("huwaa", "huwaa-point-live").
|
||||
WillReturnResult(sqlmock.NewResult(0, 2))
|
||||
for _, regionID := range []int64{0, 12} {
|
||||
walletMock.ExpectExec(`(?s)INSERT INTO wallet_policy_instances`).
|
||||
WithArgs(
|
||||
"huwaa",
|
||||
"huwaa-point-live",
|
||||
"huwaa_point_policy",
|
||||
"v2",
|
||||
regionID,
|
||||
policyStatusActive,
|
||||
int64(1700000000000),
|
||||
int64(0),
|
||||
int64(700000),
|
||||
int64(100000),
|
||||
int64(500),
|
||||
string(compiled.RuleJSON),
|
||||
uint(90001),
|
||||
nowMS,
|
||||
nowMS,
|
||||
nowMS,
|
||||
).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
}
|
||||
walletMock.ExpectCommit()
|
||||
|
||||
if err := svc.publishWalletRuntime(ctx, item, compiled, []int64{0, 12}, 90001, nowMS); err != nil {
|
||||
t.Fatalf("publishWalletRuntime failed: %v", err)
|
||||
}
|
||||
if err := svc.publishActivityRuntime(ctx, item, compiled, 90001, nowMS); err != nil {
|
||||
t.Fatalf("publishActivityRuntime failed: %v", err)
|
||||
}
|
||||
if activity.last == nil ||
|
||||
activity.last.GetMeta().GetAppCode() != "huwaa" ||
|
||||
activity.last.GetInstanceCode() != "huwaa-point-live" ||
|
||||
activity.last.GetTemplateCode() != "huwaa_point_policy" ||
|
||||
activity.last.GetTemplateVersion() != "v2" ||
|
||||
activity.last.GetStatus() != policyStatusActive ||
|
||||
activity.last.GetRewardAssetType() != "POINT" ||
|
||||
activity.last.GetOperatorAdminId() != 90001 ||
|
||||
activity.last.GetPublishedAtMs() != nowMS {
|
||||
t.Fatalf("activity task reward policy payload mismatch: %+v", activity.last)
|
||||
}
|
||||
if err := walletMock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("wallet sql expectations mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateInstanceStatusSyncsWalletRuntimeAndActivityRuntime(t *testing.T) {
|
||||
adminDB, adminMock, closeAdmin := newPolicySQLMock(t)
|
||||
defer closeAdmin()
|
||||
walletDB, walletMock, closeWallet := newPolicySQLMock(t)
|
||||
defer closeWallet()
|
||||
activity := &fakePolicyActivityClient{}
|
||||
svc := NewService(adminDB, walletDB, nil, activity)
|
||||
ctx := context.Background()
|
||||
ruleJSON := `{"points_per_usd":100000,"host":{"point_ratio_percent":70,"withdraw_fee_bps":500},"tasks":{"reward_asset_type":"POINT"}}`
|
||||
|
||||
adminMock.ExpectQuery(`(?s)SELECT instance_id, app_code, instance_code, template_code, template_version, region_scope, status,\s+effective_from_ms, effective_to_ms\s+FROM admin_policy_instances\s+WHERE app_code = \? AND instance_id = \?`).
|
||||
WithArgs("huwaa", uint64(9)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"instance_id",
|
||||
"app_code",
|
||||
"instance_code",
|
||||
"template_code",
|
||||
"template_version",
|
||||
"region_scope",
|
||||
"status",
|
||||
"effective_from_ms",
|
||||
"effective_to_ms",
|
||||
}).AddRow(9, "huwaa", "huwaa-point-live", "huwaa_point_policy", "v2", "all_active_regions", policyStatusActive, int64(1700000000000), int64(0)))
|
||||
adminMock.ExpectQuery(`(?s)SELECT COALESCE\(CAST\(rule_json AS CHAR\), '\{\}'\)\s+FROM admin_policy_template_versions\s+WHERE template_code = \? AND template_version = \?`).
|
||||
WithArgs("huwaa_point_policy", "v2").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"rule_json"}).AddRow(ruleJSON))
|
||||
adminMock.ExpectExec(`(?s)UPDATE admin_policy_instances\s+SET status = \?, updated_by_admin_id = \?, updated_at_ms = \?\s+WHERE app_code = \? AND instance_id = \?`).
|
||||
WithArgs(policyStatusDisabled, uint(90002), sqlmock.AnyArg(), "huwaa", uint64(9)).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
walletMock.ExpectExec(`(?s)CREATE TABLE IF NOT EXISTS wallet_policy_instances`).WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
walletMock.ExpectExec(`(?s)UPDATE wallet_policy_instances\s+SET status = \?, updated_at_ms = \?\s+WHERE app_code = \? AND instance_code = \?`).
|
||||
WithArgs(policyStatusDisabled, sqlmock.AnyArg(), "huwaa", "huwaa-point-live").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
adminMock.ExpectQuery(`(?s)SELECT instance_id, app_code, instance_code, template_code, template_version, region_scope, status,\s+effective_from_ms, effective_to_ms, publish_status, publish_error, last_published_at_ms,\s+created_at_ms, updated_at_ms\s+FROM admin_policy_instances\s+WHERE app_code = \? AND instance_id = \?`).
|
||||
WithArgs("huwaa", uint64(9)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"instance_id",
|
||||
"app_code",
|
||||
"instance_code",
|
||||
"template_code",
|
||||
"template_version",
|
||||
"region_scope",
|
||||
"status",
|
||||
"effective_from_ms",
|
||||
"effective_to_ms",
|
||||
"publish_status",
|
||||
"publish_error",
|
||||
"last_published_at_ms",
|
||||
"created_at_ms",
|
||||
"updated_at_ms",
|
||||
}).AddRow(9, "huwaa", "huwaa-point-live", "huwaa_point_policy", "v2", "all_active_regions", policyStatusDisabled, int64(1700000000000), int64(0), publishStatusPublished, "", int64(1700000005000), int64(1700000000000), int64(1700000006000)))
|
||||
|
||||
got, err := svc.UpdateInstanceStatus(ctx, " Huwaa ", 90002, 9, "disabled")
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateInstanceStatus failed: %v", err)
|
||||
}
|
||||
if got.Status != policyStatusDisabled {
|
||||
t.Fatalf("instance status mismatch: %+v", got)
|
||||
}
|
||||
if activity.last == nil ||
|
||||
activity.last.GetMeta().GetAppCode() != "huwaa" ||
|
||||
activity.last.GetInstanceCode() != "huwaa-point-live" ||
|
||||
activity.last.GetStatus() != policyStatusDisabled ||
|
||||
activity.last.GetRewardAssetType() != "POINT" {
|
||||
t.Fatalf("activity status sync payload mismatch: %+v", activity.last)
|
||||
}
|
||||
if err := adminMock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("admin sql expectations mismatch: %v", err)
|
||||
}
|
||||
if err := walletMock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("wallet sql expectations mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type fakePolicyActivityClient struct {
|
||||
activityclient.Client
|
||||
last *activityv1.PublishTaskRewardPolicyRequest
|
||||
}
|
||||
|
||||
func (f *fakePolicyActivityClient) PublishTaskRewardPolicy(_ context.Context, req *activityv1.PublishTaskRewardPolicyRequest) (*activityv1.PublishTaskRewardPolicyResponse, error) {
|
||||
f.last = req
|
||||
return &activityv1.PublishTaskRewardPolicyResponse{
|
||||
AppCode: req.GetMeta().GetAppCode(),
|
||||
InstanceCode: req.GetInstanceCode(),
|
||||
Status: req.GetStatus(),
|
||||
RewardAssetType: req.GetRewardAssetType(),
|
||||
PublishedAtMs: req.GetPublishedAtMs(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newPolicySQLMock(t *testing.T) (*sql.DB, sqlmock.Sqlmock, func()) {
|
||||
t.Helper()
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("create sql mock failed: %v", err)
|
||||
}
|
||||
return db, mock, func() { _ = db.Close() }
|
||||
}
|
||||
@ -73,6 +73,11 @@ func (s *Store) AutoMigrate() error {
|
||||
&model.AppExploreTab{},
|
||||
&model.HostAgencySalaryPolicy{},
|
||||
&model.HostAgencySalaryLevel{},
|
||||
&model.PolicyTemplate{},
|
||||
&model.PolicyTemplateVersion{},
|
||||
&model.PolicyInstance{},
|
||||
&model.PolicyPublishJob{},
|
||||
&model.PolicyPublishItem{},
|
||||
&model.TeamSalaryPolicy{},
|
||||
&model.TeamSalaryLevel{},
|
||||
&model.RefreshToken{},
|
||||
|
||||
@ -111,6 +111,11 @@ var defaultPermissions = []model.Permission{
|
||||
{Name: "举报列表查看", Code: "report:view", Kind: "menu"},
|
||||
{Name: "礼物钻石查看", Code: "gift-diamond:view", Kind: "menu"},
|
||||
{Name: "礼物钻石更新", Code: "gift-diamond:update", Kind: "button"},
|
||||
{Name: "收益政策模板查看", Code: "policy-template:view", Kind: "menu"},
|
||||
{Name: "收益政策模板创建", Code: "policy-template:create", Kind: "button"},
|
||||
{Name: "收益政策实例创建", Code: "policy-instance:create", Kind: "button"},
|
||||
{Name: "收益政策实例更新", Code: "policy-instance:update", Kind: "button"},
|
||||
{Name: "收益政策实例发布", Code: "policy-instance:publish", Kind: "button"},
|
||||
{Name: "系统消息推送查看", Code: "full-server-notice:view", Kind: "menu"},
|
||||
{Name: "系统消息推送发送", Code: "full-server-notice:send", Kind: "button"},
|
||||
{Name: "支付账单查看", Code: "payment-bill:view", Kind: "menu"},
|
||||
@ -318,6 +323,7 @@ func (s *Store) seedMenus() error {
|
||||
{ParentID: &operationsID, Title: "幸运礼物", Code: "lucky-gift", Path: "/operations/lucky-gift", Icon: "redeem", PermissionCode: "lucky-gift:view", Sort: 71, Visible: true},
|
||||
{ParentID: &operationsID, Title: "举报列表", Code: "operation-reports", Path: "/operations/reports", Icon: "flag", PermissionCode: "report:view", Sort: 73, Visible: true},
|
||||
{ParentID: &operationsID, Title: "礼物钻石", Code: "operation-gift-diamond", Path: "/operations/gift-diamonds", Icon: "diamond", PermissionCode: "gift-diamond:view", Sort: 74, Visible: true},
|
||||
{ParentID: &operationsID, Title: "收益政策模板", Code: "policy-template", Path: "/policy/templates", Icon: "settings", PermissionCode: "policy-template:view", Sort: 88, Visible: true},
|
||||
{ParentID: &paymentID, Title: "账单列表", Code: "payment-bill-list", Path: "/payment/bills", Icon: "receipt", PermissionCode: "payment-bill:view", Sort: 68, Visible: true},
|
||||
{ParentID: &paymentID, Title: "三方支付", Code: "payment-third-party", Path: "/payment/third-party", Icon: "wallet", PermissionCode: "payment-third-party:view", Sort: 69, Visible: true},
|
||||
{ParentID: &paymentID, Title: "三方临时支付链接", Code: "payment-temporary-links", Path: "/payment/temporary-links", Icon: "receipt", PermissionCode: "payment-temporary-link:view", Sort: 70, Visible: true},
|
||||
@ -650,7 +656,7 @@ func defaultRolePermissionCodes(code string) []string {
|
||||
"agency:view", "agency:create", "agency:status", "agency:delete",
|
||||
"bd:view", "bd:create", "bd:update",
|
||||
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "coin-seller:exchange-rate",
|
||||
"coin-ledger:view", "coin-seller-ledger:view", "host-withdrawal:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "full-server-notice:view", "full-server-notice:send", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-temporary-link:view", "payment-temporary-link:create", "finance-application:create", "finance-order:coin-seller-recharge:view", "finance-order:coin-seller-recharge:create", "finance-order:coin-seller-recharge:verify", "finance-order:coin-seller-recharge:grant", "finance-withdrawal:view", "finance-operation:user-coin-credit", "finance-operation:user-coin-debit", "finance-operation:user-wallet-credit", "finance-operation:user-wallet-debit", "finance-operation:coin-seller-coin-credit", "finance-operation:coin-seller-coin-debit", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
||||
"coin-ledger:view", "coin-seller-ledger:view", "host-withdrawal:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "policy-template:view", "policy-template:create", "policy-instance:create", "policy-instance:update", "policy-instance:publish", "full-server-notice:view", "full-server-notice:send", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-temporary-link:view", "payment-temporary-link:create", "finance-application:create", "finance-order:coin-seller-recharge:view", "finance-order:coin-seller-recharge:create", "finance-order:coin-seller-recharge:verify", "finance-order:coin-seller-recharge:grant", "finance-withdrawal:view", "finance-operation:user-coin-credit", "finance-operation:user-coin-debit", "finance-operation:user-wallet-credit", "finance-operation:user-wallet-debit", "finance-operation:coin-seller-coin-credit", "finance-operation:coin-seller-coin-debit", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
||||
"lucky-gift:view", "lucky-gift:update", "wheel:view", "wheel:update",
|
||||
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
||||
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
||||
@ -667,7 +673,7 @@ func defaultRolePermissionCodes(code string) []string {
|
||||
"upload:create",
|
||||
}
|
||||
case "auditor":
|
||||
return []string{"overview:view", "log:view", "user:view", "team:view", "app-user:view", "level-config:view", "pretty-id:view", "risk-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-whitelist:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "host-withdrawal:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "full-server-notice:view", "lucky-gift:view", "wheel:view", "payment-bill:view", "payment-third-party:view", "payment-temporary-link:view", "finance-order:coin-seller-recharge:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "cp-weekly-rank:view", "vip-config:view", "weekly-star:view", "agency-opening:view", "role:view", "permission:view", "job:view"}
|
||||
return []string{"overview:view", "log:view", "user:view", "team:view", "app-user:view", "level-config:view", "pretty-id:view", "risk-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-whitelist:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "host-withdrawal:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "policy-template:view", "full-server-notice:view", "lucky-gift:view", "wheel:view", "payment-bill:view", "payment-third-party:view", "payment-temporary-link:view", "finance-order:coin-seller-recharge:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "cp-weekly-rank:view", "vip-config:view", "weekly-star:view", "agency-opening:view", "role:view", "permission:view", "job:view"}
|
||||
case "readonly":
|
||||
return []string{
|
||||
"overview:view",
|
||||
@ -706,6 +712,7 @@ func defaultRolePermissionCodes(code string) []string {
|
||||
"coin-adjustment:view",
|
||||
"report:view",
|
||||
"gift-diamond:view",
|
||||
"policy-template:view",
|
||||
"full-server-notice:view",
|
||||
"lucky-gift:view",
|
||||
"wheel:view",
|
||||
@ -760,7 +767,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
||||
"region:view", "region:create", "region:update", "region:status",
|
||||
"host-agency-policy:view", "host-agency-policy:create", "host-agency-policy:update", "host-agency-policy:delete", "host-agency-policy:publish", "team-salary-policy:view", "team-salary-policy:create", "team-salary-policy:update", "team-salary-policy:delete", "host-salary-settlement:view", "host-salary-settlement:settle",
|
||||
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit", "coin-seller:exchange-rate",
|
||||
"coin-ledger:view", "coin-seller-ledger:view", "host-withdrawal:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "full-server-notice:view", "full-server-notice:send", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-temporary-link:view", "payment-temporary-link:create", "finance-application:create", "finance-order:coin-seller-recharge:view", "finance-order:coin-seller-recharge:create", "finance-order:coin-seller-recharge:verify", "finance-order:coin-seller-recharge:grant", "finance-operation:user-coin-credit", "finance-operation:user-coin-debit", "finance-operation:user-wallet-credit", "finance-operation:user-wallet-debit", "finance-operation:coin-seller-coin-credit", "finance-operation:coin-seller-coin-debit", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
||||
"coin-ledger:view", "coin-seller-ledger:view", "host-withdrawal:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "gift-diamond:view", "gift-diamond:update", "policy-template:view", "policy-template:create", "policy-instance:create", "policy-instance:update", "policy-instance:publish", "full-server-notice:view", "full-server-notice:send", "payment-bill:view", "payment-third-party:view", "payment-third-party:update", "payment-temporary-link:view", "payment-temporary-link:create", "finance-application:create", "finance-order:coin-seller-recharge:view", "finance-order:coin-seller-recharge:create", "finance-order:coin-seller-recharge:verify", "finance-order:coin-seller-recharge:grant", "finance-operation:user-coin-credit", "finance-operation:user-coin-debit", "finance-operation:user-wallet-credit", "finance-operation:user-wallet-debit", "finance-operation:coin-seller-coin-credit", "finance-operation:coin-seller-coin-debit", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
|
||||
"lucky-gift:view", "lucky-gift:update", "wheel:view", "wheel:update",
|
||||
"game:view", "game:create", "game:update", "game:status", "game:delete",
|
||||
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
|
||||
@ -774,7 +781,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
||||
"agency-opening:view", "agency-opening:create", "agency-opening:update",
|
||||
}
|
||||
case "auditor", "readonly":
|
||||
return []string{"team:view", "level-config:view", "pretty-id:view", "risk-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-whitelist:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "host-withdrawal:view", "coin-seller:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "full-server-notice:view", "lucky-gift:view", "wheel:view", "payment-bill:view", "payment-third-party:view", "payment-temporary-link:view", "finance-order:coin-seller-recharge:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "cp-weekly-rank:view", "vip-config:view", "weekly-star:view", "agency-opening:view"}
|
||||
return []string{"team:view", "level-config:view", "pretty-id:view", "risk-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-whitelist:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "host-withdrawal:view", "coin-seller:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "policy-template:view", "full-server-notice:view", "lucky-gift:view", "wheel:view", "payment-bill:view", "payment-third-party:view", "payment-temporary-link:view", "finance-order:coin-seller-recharge:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "cp-weekly-rank:view", "vip-config:view", "weekly-star:view", "agency-opening:view"}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -38,6 +38,14 @@ func (s *Store) GetWithdrawalApplication(id uint) (*model.UserWithdrawalApplicat
|
||||
return &application, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetWithdrawalApplicationForApp(appCode string, id uint) (*model.UserWithdrawalApplication, error) {
|
||||
var application model.UserWithdrawalApplication
|
||||
if err := s.db.Where("app_code = ? AND id = ?", strings.TrimSpace(appCode), id).First(&application).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &application, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListWithdrawalApplications(options WithdrawalApplicationListOptions) ([]model.UserWithdrawalApplication, int64, error) {
|
||||
page, pageSize := normalizePage(options.Page, options.PageSize)
|
||||
query := s.db.Model(&model.UserWithdrawalApplication{})
|
||||
@ -58,6 +66,10 @@ func (s *Store) ListWithdrawalApplications(options WithdrawalApplicationListOpti
|
||||
}
|
||||
|
||||
func (s *Store) AuditWithdrawalApplication(id uint, input WithdrawalApplicationAuditInput) (*model.UserWithdrawalApplication, error) {
|
||||
return s.AuditWithdrawalApplicationForApp("", id, input)
|
||||
}
|
||||
|
||||
func (s *Store) AuditWithdrawalApplicationForApp(appCode string, id uint, input WithdrawalApplicationAuditInput) (*model.UserWithdrawalApplication, error) {
|
||||
if id == 0 {
|
||||
return nil, errors.New("withdrawal application id is required")
|
||||
}
|
||||
@ -68,7 +80,12 @@ func (s *Store) AuditWithdrawalApplication(id uint, input WithdrawalApplicationA
|
||||
var application model.UserWithdrawalApplication
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 审核只能从 pending 进入终态;行锁阻止两个财务同时覆盖同一张提现单的执行结果。
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&application, id).Error; err != nil {
|
||||
query := tx.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
if strings.TrimSpace(appCode) != "" {
|
||||
// 后台按当前 App 上下文审核,不能只凭全局自增 ID 跨 App 锁定和审核提现单。
|
||||
query = query.Where("app_code = ?", strings.TrimSpace(appCode))
|
||||
}
|
||||
if err := query.First(&application, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if application.Status != model.WithdrawalApplicationStatusPending {
|
||||
@ -89,10 +106,14 @@ func (s *Store) AuditWithdrawalApplication(id uint, input WithdrawalApplicationA
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(appCode) != "" {
|
||||
return s.GetWithdrawalApplicationForApp(appCode, id)
|
||||
}
|
||||
return s.GetWithdrawalApplication(id)
|
||||
}
|
||||
|
||||
// ApprovedWithdrawalStats 按审批通过时间聚合用户提现申请的 USDT 金额(withdraw_amount_minor,美分口径)。
|
||||
// ApprovedWithdrawalStats 按审批通过时间聚合用户提现申请的 USDT 金额。
|
||||
// 旧工资提现用 withdraw_amount_minor 保存美分;Huwaa POINT 提现该字段保存 gross points,因此必须用 withdraw_amount 的 USD 展示值折算。
|
||||
type ApprovedWithdrawalStats struct {
|
||||
ApprovedCount int64
|
||||
ApprovedUSDMinor int64
|
||||
@ -111,7 +132,12 @@ func (s *Store) ApprovedWithdrawalStats(appCode string, startAtMS int64, endAtMS
|
||||
query = query.Where("approved_at_ms < ?", endAtMS)
|
||||
}
|
||||
var stats ApprovedWithdrawalStats
|
||||
row := query.Select("COUNT(*), COALESCE(SUM(withdraw_amount_minor), 0)").Row()
|
||||
row := query.Select(`
|
||||
COUNT(*),
|
||||
COALESCE(SUM(CASE
|
||||
WHEN salary_asset_type IN ('POINT', 'COIN_SELLER_POINT') THEN CAST(withdraw_amount * 100 AS SIGNED)
|
||||
ELSE withdraw_amount_minor
|
||||
END), 0)`).Row()
|
||||
if err := row.Scan(&stats.ApprovedCount, &stats.ApprovedUSDMinor); err != nil {
|
||||
return ApprovedWithdrawalStats{}, err
|
||||
}
|
||||
|
||||
@ -50,3 +50,65 @@ func TestListWithdrawalApplicationsFiltersAndSorts(t *testing.T) {
|
||||
t.Fatalf("sql expectations mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetWithdrawalApplicationForAppScopesByAppCode(t *testing.T) {
|
||||
store, mock, closeStore := newRepositorySQLMock(t)
|
||||
defer closeStore()
|
||||
|
||||
mock.ExpectQuery("SELECT \\* FROM `admin_user_withdrawal_applications` WHERE app_code = \\? AND id = \\?").
|
||||
WithArgs("huwaa", uint(77), 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{
|
||||
"id",
|
||||
"app_code",
|
||||
"user_id",
|
||||
"salary_asset_type",
|
||||
"withdraw_amount",
|
||||
"withdraw_amount_minor",
|
||||
"withdraw_method",
|
||||
"withdraw_address",
|
||||
"freeze_command_id",
|
||||
"freeze_transaction_id",
|
||||
"audit_command_id",
|
||||
"audit_transaction_id",
|
||||
"status",
|
||||
"approver_user_id",
|
||||
"approver_name",
|
||||
"audit_remark",
|
||||
"audit_image_url",
|
||||
"approved_at_ms",
|
||||
"created_at_ms",
|
||||
"updated_at_ms",
|
||||
}).AddRow(
|
||||
77,
|
||||
"huwaa",
|
||||
"42001",
|
||||
"POINT",
|
||||
"9.50",
|
||||
int64(1000000),
|
||||
"usdt_trc20",
|
||||
"TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
||||
"cmd-point-freeze",
|
||||
"tx-freeze",
|
||||
"",
|
||||
"",
|
||||
"pending",
|
||||
nil,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
nil,
|
||||
int64(1700000000000),
|
||||
int64(1700000000000),
|
||||
))
|
||||
|
||||
item, err := store.GetWithdrawalApplicationForApp("huwaa", 77)
|
||||
if err != nil {
|
||||
t.Fatalf("get withdrawal application for app failed: %v", err)
|
||||
}
|
||||
if item.ID != 77 || item.AppCode != "huwaa" || item.SalaryAssetType != "POINT" || item.FreezeCommandID != "cmd-point-freeze" {
|
||||
t.Fatalf("withdrawal app-scoped row mismatch: %+v", item)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,9 +34,10 @@ import (
|
||||
"hyapp-admin-server/internal/modules/inviteactivityreward"
|
||||
"hyapp-admin-server/internal/modules/job"
|
||||
"hyapp-admin-server/internal/modules/levelconfig"
|
||||
"hyapp-admin-server/internal/modules/luckygift"
|
||||
"hyapp-admin-server/internal/modules/menu"
|
||||
"hyapp-admin-server/internal/modules/opscenter"
|
||||
"hyapp-admin-server/internal/modules/payment"
|
||||
"hyapp-admin-server/internal/modules/policyconfig"
|
||||
"hyapp-admin-server/internal/modules/prettyid"
|
||||
"hyapp-admin-server/internal/modules/rbac"
|
||||
"hyapp-admin-server/internal/modules/redpacket"
|
||||
@ -96,9 +97,10 @@ type Handlers struct {
|
||||
InviteActivityReward *inviteactivityreward.Handler
|
||||
Job *job.Handler
|
||||
LevelConfig *levelconfig.Handler
|
||||
LuckyGift *luckygift.Handler
|
||||
Menu *menu.Handler
|
||||
OpsCenter *opscenter.Handler
|
||||
Payment *payment.Handler
|
||||
PolicyConfig *policyconfig.Handler
|
||||
PrettyID *prettyid.Handler
|
||||
RBAC *rbac.Handler
|
||||
RedPacket *redpacket.Handler
|
||||
@ -173,10 +175,11 @@ func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
|
||||
inviteactivityreward.RegisterRoutes(protected, h.InviteActivityReward)
|
||||
audit.RegisterRoutes(protected, h.Audit)
|
||||
payment.RegisterRoutes(protected, h.Payment)
|
||||
policyconfig.RegisterRoutes(protected, h.PolicyConfig)
|
||||
prettyid.RegisterRoutes(protected, h.PrettyID)
|
||||
job.RegisterRoutes(protected, h.Job)
|
||||
levelconfig.RegisterRoutes(protected, h.LevelConfig)
|
||||
luckygift.RegisterRoutes(protected, h.LuckyGift)
|
||||
opscenter.RegisterRoutes(protected, h.OpsCenter)
|
||||
upload.RegisterRoutes(protected, h.Upload)
|
||||
search.RegisterRoutes(protected, h.Search)
|
||||
sevendaycheckin.RegisterRoutes(protected, h.SevenDayCheckIn)
|
||||
|
||||
246
server/admin/migrations/082_policy_template_instance.sql
Normal file
246
server/admin/migrations/082_policy_template_instance.sql
Normal file
@ -0,0 +1,246 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_policy_templates (
|
||||
template_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
template_code VARCHAR(96) NOT NULL COMMENT '模板编码,跨 App 复用',
|
||||
template_version VARCHAR(64) NOT NULL COMMENT '模板版本',
|
||||
name VARCHAR(160) NOT NULL COMMENT '模板名称',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
|
||||
rule_json JSON NOT NULL COMMENT '完整策略规则 JSON,发布时编译到 owner service 运行表',
|
||||
description VARCHAR(512) NOT NULL DEFAULT '' COMMENT '策略边界和适用说明',
|
||||
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (template_id),
|
||||
UNIQUE KEY uk_admin_policy_template_version (template_code, template_version),
|
||||
KEY idx_admin_policy_templates_status (status, updated_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收益政策模板表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_policy_template_versions (
|
||||
version_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
template_code VARCHAR(96) NOT NULL COMMENT '模板编码',
|
||||
template_version VARCHAR(64) NOT NULL COMMENT '模板版本',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
|
||||
rule_json JSON NOT NULL COMMENT '完整策略规则 JSON,发布时编译到 owner service 运行表',
|
||||
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (version_id),
|
||||
UNIQUE KEY uk_admin_policy_template_version_row (template_code, template_version),
|
||||
KEY idx_admin_policy_template_versions_status (template_code, status, updated_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收益政策模板版本表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_policy_instances (
|
||||
instance_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
instance_code VARCHAR(96) NOT NULL COMMENT '实例编码',
|
||||
template_code VARCHAR(96) NOT NULL COMMENT '模板编码快照',
|
||||
template_version VARCHAR(64) NOT NULL COMMENT '模板版本快照',
|
||||
region_scope VARCHAR(128) NOT NULL DEFAULT 'all_active_regions' COMMENT 'all_active_regions 或 region_id:<id>',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
|
||||
effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'UTC epoch ms',
|
||||
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '0 表示长期有效',
|
||||
publish_status VARCHAR(24) NOT NULL DEFAULT 'draft' COMMENT 'draft/published/failed',
|
||||
publish_error VARCHAR(512) NOT NULL DEFAULT '',
|
||||
last_published_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (instance_id),
|
||||
UNIQUE KEY uk_admin_policy_instance_code (app_code, instance_code),
|
||||
KEY idx_admin_policy_instances_template (template_code, template_version),
|
||||
KEY idx_admin_policy_instances_app_status (app_code, status, publish_status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收益政策实例表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_policy_publish_jobs (
|
||||
job_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
instance_id BIGINT UNSIGNED NOT NULL,
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
instance_code VARCHAR(96) NOT NULL,
|
||||
template_code VARCHAR(96) NOT NULL,
|
||||
template_version VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'pending' COMMENT 'pending/succeeded/failed',
|
||||
target_count INT NOT NULL DEFAULT 0,
|
||||
success_count INT NOT NULL DEFAULT 0,
|
||||
failure_count INT NOT NULL DEFAULT 0,
|
||||
error_message VARCHAR(512) NOT NULL DEFAULT '',
|
||||
operator_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (job_id),
|
||||
KEY idx_admin_policy_publish_jobs_instance (instance_id, created_at_ms),
|
||||
KEY idx_admin_policy_publish_jobs_status (status, updated_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收益政策发布任务表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_policy_publish_items (
|
||||
item_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
job_id BIGINT UNSIGNED NOT NULL,
|
||||
owner_service VARCHAR(32) NOT NULL COMMENT 'wallet/activity/game',
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
region_id BIGINT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'pending',
|
||||
error_message VARCHAR(512) NOT NULL DEFAULT '',
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (item_id),
|
||||
KEY idx_admin_policy_publish_items_job (job_id, owner_service, status),
|
||||
KEY idx_admin_policy_publish_items_region (app_code, region_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收益政策发布明细表';
|
||||
|
||||
INSERT INTO admin_policy_templates (
|
||||
template_code, template_version, name, status, rule_json, description,
|
||||
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (
|
||||
'first_google70000_coin_seller_92000_100000_v1',
|
||||
'v1',
|
||||
'第一套政策:Google 70000 + 币商 92000-100000',
|
||||
'active',
|
||||
JSON_OBJECT(
|
||||
'points_per_usd', 100000,
|
||||
'allow_self_brushing', TRUE,
|
||||
'google_coin_per_usd', 70000,
|
||||
'host', JSON_OBJECT('point_ratio_percent', 70, 'affects_room_heat', FALSE, 'minimum_withdraw_points', 1000000, 'withdraw_fee_bps', 500),
|
||||
'agent', JSON_OBJECT(
|
||||
'period', 'rolling_30d',
|
||||
'support_ratio_percent', 8,
|
||||
'support_days', 30,
|
||||
'qualified_host_min', 3,
|
||||
'levels', JSON_ARRAY(
|
||||
JSON_OBJECT('level', 1, 'name', 'Lv1', 'min_usd', 0, 'max_usd', 200, 'ratio_percent', 4, 'qualified_host_min', 3),
|
||||
JSON_OBJECT('level', 2, 'name', 'Lv2', 'min_usd', 200, 'max_usd', 500, 'ratio_percent', 8, 'qualified_host_min', 5),
|
||||
JSON_OBJECT('level', 3, 'name', 'Lv3', 'min_usd', 500, 'max_usd', 2000, 'ratio_percent', 16, 'qualified_host_min', 10),
|
||||
JSON_OBJECT('level', 4, 'name', 'Lv4', 'min_usd', 2000, 'max_usd', 0, 'ratio_percent', 20, 'manual_review', TRUE)
|
||||
),
|
||||
'differential', JSON_OBJECT('enabled', TRUE, 'max_depth', 2, 'cross_level_formula', 'parent_minus_direct_child')
|
||||
),
|
||||
'bd', JSON_OBJECT(
|
||||
'period', 'utc_calendar_month',
|
||||
'qualified_agency_min', 3,
|
||||
'qualified_host_per_agency_min', 3,
|
||||
'base', 'host_effective_income',
|
||||
'levels', JSON_ARRAY(
|
||||
JSON_OBJECT('level', 1, 'name', '测试BD-1', 'threshold_usd', 100, 'salary_usd', 5, 'commission_percent', 1.0),
|
||||
JSON_OBJECT('level', 2, 'name', '测试BD-2', 'threshold_usd', 300, 'salary_usd', 15, 'commission_percent', 1.0),
|
||||
JSON_OBJECT('level', 3, 'name', '正式BD-1', 'threshold_usd', 700, 'salary_usd', 35, 'commission_percent', 1.5),
|
||||
JSON_OBJECT('level', 4, 'name', '正式BD-2', 'threshold_usd', 1000, 'salary_usd', 50, 'commission_percent', 1.5),
|
||||
JSON_OBJECT('level', 5, 'name', '高级BD-1', 'threshold_usd', 2000, 'salary_usd', 100, 'commission_percent', 2.0),
|
||||
JSON_OBJECT('level', 6, 'name', '高级BD-2', 'threshold_usd', 3000, 'salary_usd', 150, 'commission_percent', 2.0),
|
||||
JSON_OBJECT('level', 7, 'name', '普通BD-1', 'threshold_usd', 5000, 'salary_usd', 250, 'commission_percent', 2.5),
|
||||
JSON_OBJECT('level', 8, 'name', '普通BD-2', 'threshold_usd', 7000, 'salary_usd', 350, 'commission_percent', 2.5),
|
||||
JSON_OBJECT('level', 9, 'name', '普通Admin-1', 'threshold_usd', 10000, 'salary_usd', 500, 'commission_percent', 3.0),
|
||||
JSON_OBJECT('level', 10, 'name', '普通Admin-2', 'threshold_usd', 20000, 'salary_usd', 1000, 'commission_percent', 3.0),
|
||||
JSON_OBJECT('level', 11, 'name', '超级Admin-1', 'threshold_usd', 30000, 'salary_usd', 1500, 'commission_percent', 3.0),
|
||||
JSON_OBJECT('level', 12, 'name', '超级Admin-2', 'threshold_usd', 40000, 'salary_usd', 2000, 'commission_percent', 3.0)
|
||||
)
|
||||
),
|
||||
'manager', JSON_OBJECT('coin_seller_recharge_threshold_usd', 2000, 'coin_seller_recharge_commission_percent', 3, 'host_point_commission_percent', 2),
|
||||
'coin_seller', JSON_OBJECT(
|
||||
'withdraw_order_enabled', TRUE,
|
||||
'levels', JSON_ARRAY(
|
||||
JSON_OBJECT('name', 'Beginner', 'threshold_usd', 100, 'coin_per_usd', 92000, 'withdraw_order_enabled', FALSE),
|
||||
JSON_OBJECT('name', 'Standard', 'threshold_usd', 500, 'coin_per_usd', 96000, 'withdraw_order_enabled', FALSE),
|
||||
JSON_OBJECT('name', 'Senior', 'single_recharge_threshold_usd', 1000, 'coin_per_usd', 100000, 'withdraw_order_enabled', TRUE)
|
||||
),
|
||||
'order_timeout_seconds', 7200,
|
||||
'seller_point_settle_percent', 95,
|
||||
'seller_point_reward_percent', 5,
|
||||
'success_rate_close_threshold_percent', 95
|
||||
),
|
||||
'tasks', JSON_OBJECT('reward_asset_type', 'POINT', 'new_host_7d_max_points', 350000),
|
||||
'game_invite', JSON_OBJECT('enabled', TRUE, 'period', 'rolling_30d')
|
||||
),
|
||||
'新文档口径:允许自刷;BD 按 UTC 当月自然月;Huwaa 使用 POINT/COIN_SELLER_POINT。',
|
||||
0, 0, @now_ms, @now_ms
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
status = VALUES(status),
|
||||
rule_json = VALUES(rule_json),
|
||||
description = VALUES(description),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
INSERT INTO admin_policy_template_versions (
|
||||
template_code, template_version, status, rule_json,
|
||||
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
)
|
||||
SELECT
|
||||
template_code, template_version, status, rule_json,
|
||||
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
FROM admin_policy_templates
|
||||
WHERE template_code = 'first_google70000_coin_seller_92000_100000_v1'
|
||||
AND template_version = 'v1'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status = VALUES(status),
|
||||
rule_json = VALUES(rule_json),
|
||||
updated_by_admin_id = VALUES(updated_by_admin_id),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
INSERT INTO admin_policy_instances (
|
||||
app_code, instance_code, template_code, template_version, region_scope, status,
|
||||
effective_from_ms, effective_to_ms, publish_status, created_by_admin_id, updated_by_admin_id,
|
||||
created_at_ms, updated_at_ms
|
||||
) VALUES (
|
||||
'huwaa',
|
||||
'huwaa_first_google70000_coin_seller_92000_100000',
|
||||
'first_google70000_coin_seller_92000_100000_v1',
|
||||
'v1',
|
||||
'all_active_regions',
|
||||
'active',
|
||||
@now_ms,
|
||||
0,
|
||||
'draft',
|
||||
0,
|
||||
0,
|
||||
@now_ms,
|
||||
@now_ms
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
template_code = VALUES(template_code),
|
||||
template_version = VALUES(template_version),
|
||||
region_scope = VALUES(region_scope),
|
||||
status = VALUES(status),
|
||||
effective_to_ms = VALUES(effective_to_ms),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
|
||||
('收益政策模板查看', 'policy-template:view', 'menu', '允许查看收益政策模板和实例', @now_ms, @now_ms),
|
||||
('收益政策模板创建', 'policy-template:create', 'button', '允许创建收益政策模板', @now_ms, @now_ms),
|
||||
('收益政策实例创建', 'policy-instance:create', 'button', '允许创建收益政策实例', @now_ms, @now_ms),
|
||||
('收益政策实例更新', 'policy-instance:update', 'button', '允许更新收益政策实例状态', @now_ms, @now_ms),
|
||||
('收益政策实例发布', 'policy-instance:publish', 'button', '允许发布收益政策实例到运行侧', @now_ms, @now_ms)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
kind = VALUES(kind),
|
||||
description = VALUES(description),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
|
||||
SELECT parent.id, '收益政策模板', 'policy-template', '/policy/templates', 'settings', 'policy-template:view', 88, TRUE, @now_ms, @now_ms
|
||||
FROM admin_menus parent
|
||||
WHERE parent.code = 'operations'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
parent_id = VALUES(parent_id),
|
||||
title = VALUES(title),
|
||||
path = VALUES(path),
|
||||
icon = VALUES(icon),
|
||||
permission_code = VALUES(permission_code),
|
||||
sort = VALUES(sort),
|
||||
visible = VALUES(visible),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM admin_roles r
|
||||
JOIN admin_permissions p
|
||||
WHERE r.code IN ('platform-admin', 'ops-admin')
|
||||
AND p.code IN ('policy-template:view', 'policy-template:create', 'policy-instance:create', 'policy-instance:update', 'policy-instance:publish');
|
||||
|
||||
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM admin_roles r
|
||||
JOIN admin_permissions p
|
||||
WHERE r.code IN ('auditor', 'readonly')
|
||||
AND p.code = 'policy-template:view';
|
||||
@ -0,0 +1,23 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
UPDATE admin_user_withdrawal_applications
|
||||
SET freeze_command_id = CONCAT('legacy:', id)
|
||||
WHERE freeze_command_id = '';
|
||||
|
||||
SET @idx_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'admin_user_withdrawal_applications'
|
||||
AND INDEX_NAME = 'uk_admin_withdrawal_freeze_command'
|
||||
);
|
||||
|
||||
SET @ddl = IF(
|
||||
@idx_exists = 0,
|
||||
'ALTER TABLE admin_user_withdrawal_applications ADD UNIQUE KEY uk_admin_withdrawal_freeze_command (app_code, freeze_command_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
32
server/luck-gateway/Dockerfile
Normal file
32
server/luck-gateway/Dockerfile
Normal file
@ -0,0 +1,32 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM golang:1.26.3-alpine AS builder
|
||||
|
||||
WORKDIR /src/server/luck-gateway
|
||||
|
||||
ARG GOPROXY=https://goproxy.cn|https://proxy.golang.org|direct
|
||||
ARG GOSUMDB=sum.golang.org
|
||||
ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
|
||||
|
||||
COPY api /src/api
|
||||
COPY server/luck-gateway /src/server/luck-gateway
|
||||
|
||||
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||
GOWORK=off go mod download && \
|
||||
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./cmd/server
|
||||
|
||||
FROM alpine:3.20
|
||||
|
||||
ENV TZ=UTC
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache ca-certificates && adduser -D -g '' appuser
|
||||
|
||||
COPY --from=builder /out/server /app/server
|
||||
|
||||
USER appuser
|
||||
|
||||
EXPOSE 13014
|
||||
|
||||
ENTRYPOINT ["/app/server"]
|
||||
32
server/luck-gateway/cmd/server/main.go
Normal file
32
server/luck-gateway/cmd/server/main.go
Normal file
@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"hyapp-luck-gateway/internal/config"
|
||||
"hyapp-luck-gateway/internal/integration/luckygiftclient"
|
||||
"hyapp-luck-gateway/internal/modules/luckygift"
|
||||
"hyapp-luck-gateway/internal/router"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Default()
|
||||
conn, err := grpc.NewClient(cfg.LuckyGiftGRPCAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
log.Fatalf("create lucky gift grpc client: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
luckyGiftClient := luckygiftclient.NewGRPC(conn)
|
||||
handler := router.New(cfg, router.Handlers{
|
||||
LuckyGift: luckygift.New(luckyGiftClient, cfg.RequestTimeout, cfg.AllowedApps),
|
||||
})
|
||||
log.Printf("luck-gateway listening on %s", cfg.HTTPAddr)
|
||||
if err := http.ListenAndServe(cfg.HTTPAddr, handler); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
18
server/luck-gateway/go.mod
Normal file
18
server/luck-gateway/go.mod
Normal file
@ -0,0 +1,18 @@
|
||||
module hyapp-luck-gateway
|
||||
|
||||
go 1.26.3
|
||||
|
||||
require (
|
||||
google.golang.org/grpc v1.68.0
|
||||
hyapp.local/api v0.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.29.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
)
|
||||
|
||||
replace hyapp.local/api => ../../api
|
||||
16
server/luck-gateway/go.sum
Normal file
16
server/luck-gateway/go.sum
Normal file
@ -0,0 +1,16 @@
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
47
server/luck-gateway/internal/config/config.go
Normal file
47
server/luck-gateway/internal/config/config.go
Normal file
@ -0,0 +1,47 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config 只承载 luck-gateway 自己启动所需的边界配置;业务规则仍由 lucky gift owner service 决策。
|
||||
type Config struct {
|
||||
HTTPAddr string
|
||||
LuckyGiftGRPCAddr string
|
||||
AllowedApps map[string]struct{}
|
||||
RequestTimeout time.Duration
|
||||
}
|
||||
|
||||
func Default() Config {
|
||||
return Config{
|
||||
HTTPAddr: envDefault("LUCK_GATEWAY_HTTP_ADDR", ":13014"),
|
||||
LuckyGiftGRPCAddr: envDefault("LUCKY_GIFT_GRPC_ADDR", "127.0.0.1:13013"),
|
||||
AllowedApps: parseAllowedApps(envDefault("LUCK_GATEWAY_ALLOWED_APPS", "aslan,yumi")),
|
||||
RequestTimeout: 3 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func parseAllowedApps(raw string) map[string]struct{} {
|
||||
apps := map[string]struct{}{}
|
||||
for _, entry := range strings.Split(raw, ",") {
|
||||
app := normalizeAppCode(entry)
|
||||
if app == "" {
|
||||
continue
|
||||
}
|
||||
apps[app] = struct{}{}
|
||||
}
|
||||
return apps
|
||||
}
|
||||
|
||||
func normalizeAppCode(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func envDefault(key string, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package luckygiftclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type SendCommand struct {
|
||||
TraceRequestID string
|
||||
AppCode string
|
||||
RequestID string
|
||||
ExternalUserID string
|
||||
GiftCount int64
|
||||
UnitAmount int64
|
||||
TotalAmount int64
|
||||
Currency string
|
||||
MetadataJSON string
|
||||
PaidAtMS int64
|
||||
}
|
||||
|
||||
type SendResult struct {
|
||||
DrawID string `json:"draw_id,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
AppCode string `json:"app_code,omitempty"`
|
||||
ExternalUserID string `json:"external_user_id,omitempty"`
|
||||
GiftCount int64 `json:"gift_count,omitempty"`
|
||||
UnitAmount int64 `json:"unit_amount,omitempty"`
|
||||
TotalAmount int64 `json:"total_amount,omitempty"`
|
||||
RewardAmount int64 `json:"reward_amount,omitempty"`
|
||||
MultiplierPPM int64 `json:"multiplier_ppm,omitempty"`
|
||||
RewardStatus string `json:"reward_status,omitempty"`
|
||||
RuleVersion int64 `json:"rule_version,omitempty"`
|
||||
CreatedAtMS int64 `json:"created_at_ms,omitempty"`
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
SendLuckyGift(ctx context.Context, cmd SendCommand) (SendResult, error)
|
||||
}
|
||||
|
||||
type GRPCClient struct {
|
||||
client luckygiftv1.LuckyGiftServiceClient
|
||||
}
|
||||
|
||||
func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
||||
return &GRPCClient{client: luckygiftv1.NewLuckyGiftServiceClient(conn)}
|
||||
}
|
||||
|
||||
func (c *GRPCClient) SendLuckyGift(ctx context.Context, cmd SendCommand) (SendResult, error) {
|
||||
resp, err := c.client.ExecuteExternalGiftDraw(ctx, &luckygiftv1.ExternalGiftDrawRequest{
|
||||
Meta: &luckygiftv1.RequestMeta{
|
||||
RequestId: cmd.TraceRequestID,
|
||||
Caller: "luck-gateway",
|
||||
SentAtMs: time.Now().UTC().UnixMilli(),
|
||||
AppCode: cmd.AppCode,
|
||||
},
|
||||
AppCode: cmd.AppCode,
|
||||
ExternalUserId: cmd.ExternalUserID,
|
||||
RequestId: cmd.RequestID,
|
||||
GiftCount: cmd.GiftCount,
|
||||
UnitAmount: cmd.UnitAmount,
|
||||
TotalAmount: cmd.TotalAmount,
|
||||
Currency: cmd.Currency,
|
||||
PaidAtMs: cmd.PaidAtMS,
|
||||
MetadataJson: cmd.MetadataJSON,
|
||||
})
|
||||
if err != nil {
|
||||
return SendResult{}, err
|
||||
}
|
||||
return fromProto(resp), nil
|
||||
}
|
||||
|
||||
func fromProto(result *luckygiftv1.ExternalGiftDrawResponse) SendResult {
|
||||
if result == nil {
|
||||
return SendResult{}
|
||||
}
|
||||
return SendResult{
|
||||
DrawID: result.GetDrawId(),
|
||||
RequestID: result.GetRequestId(),
|
||||
AppCode: result.GetAppCode(),
|
||||
ExternalUserID: result.GetExternalUserId(),
|
||||
GiftCount: result.GetGiftCount(),
|
||||
UnitAmount: result.GetUnitAmount(),
|
||||
TotalAmount: result.GetTotalAmount(),
|
||||
RewardAmount: result.GetRewardAmount(),
|
||||
MultiplierPPM: result.GetMultiplierPpm(),
|
||||
RewardStatus: result.GetRewardStatus(),
|
||||
RuleVersion: result.GetRuleVersion(),
|
||||
CreatedAtMS: result.GetCreatedAtMs(),
|
||||
}
|
||||
}
|
||||
154
server/luck-gateway/internal/modules/luckygift/handler.go
Normal file
154
server/luck-gateway/internal/modules/luckygift/handler.go
Normal file
@ -0,0 +1,154 @@
|
||||
package luckygift
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp-luck-gateway/internal/integration/luckygiftclient"
|
||||
"hyapp-luck-gateway/internal/requestid"
|
||||
"hyapp-luck-gateway/internal/response"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
client luckygiftclient.Client
|
||||
allowedApps map[string]struct{}
|
||||
requestTimeout time.Duration
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func New(client luckygiftclient.Client, requestTimeout time.Duration, allowedApps map[string]struct{}) *Handler {
|
||||
if requestTimeout <= 0 {
|
||||
requestTimeout = 3 * time.Second
|
||||
}
|
||||
return &Handler{client: client, allowedApps: cloneAllowedApps(allowedApps), requestTimeout: requestTimeout, now: time.Now}
|
||||
}
|
||||
|
||||
type sendRequest struct {
|
||||
AppCode string `json:"app_code"`
|
||||
RequestID string `json:"request_id"`
|
||||
ExternalUserID string `json:"external_user_id"`
|
||||
GiftCount int64 `json:"gift_count"`
|
||||
UnitAmount int64 `json:"unit_amount"`
|
||||
Currency string `json:"currency"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
func (h *Handler) Send(w http.ResponseWriter, r *http.Request) {
|
||||
requestID := requestid.FromContext(r.Context())
|
||||
var req sendRequest
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
response.Fail(w, http.StatusBadRequest, response.CodeBadRequest, requestID, "invalid lucky gift send body")
|
||||
return
|
||||
}
|
||||
if err := req.validate(); err != nil {
|
||||
response.Fail(w, http.StatusBadRequest, response.CodeBadRequest, requestID, err.Error())
|
||||
return
|
||||
}
|
||||
appCode := normalizeAppCode(req.AppCode)
|
||||
if !h.isAllowedApp(appCode) {
|
||||
response.Fail(w, http.StatusForbidden, response.CodeForbidden, requestID, "app_code is not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), h.requestTimeout)
|
||||
defer cancel()
|
||||
result, err := h.client.SendLuckyGift(ctx, luckygiftclient.SendCommand{
|
||||
TraceRequestID: requestID,
|
||||
AppCode: appCode,
|
||||
RequestID: strings.TrimSpace(req.RequestID),
|
||||
ExternalUserID: strings.TrimSpace(req.ExternalUserID),
|
||||
GiftCount: req.GiftCount,
|
||||
UnitAmount: req.UnitAmount,
|
||||
TotalAmount: req.GiftCount * req.UnitAmount,
|
||||
Currency: strings.TrimSpace(req.Currency),
|
||||
MetadataJSON: metadataJSON(req.Metadata),
|
||||
// 外部 HTTP 契约不暴露 pool_id/paid_at_ms;pool 由 owner service 默认规则选择,支付完成时间使用网关服务端 UTC 时间,
|
||||
// 避免外部 App 通过请求体影响奖池分桶、预算日或风控窗口。
|
||||
PaidAtMS: normalizePaidAt(h.now),
|
||||
})
|
||||
if err != nil {
|
||||
response.Fail(w, http.StatusInternalServerError, response.CodeServerError, requestID, "send lucky gift failed")
|
||||
return
|
||||
}
|
||||
response.OK(w, requestID, result)
|
||||
}
|
||||
|
||||
func (r sendRequest) validate() error {
|
||||
if normalizeAppCode(r.AppCode) == "" {
|
||||
return errors.New("app_code is required")
|
||||
}
|
||||
if len(normalizeAppCode(r.AppCode)) > 32 {
|
||||
return errors.New("app_code is too long")
|
||||
}
|
||||
if strings.TrimSpace(r.RequestID) == "" {
|
||||
return errors.New("request_id is required")
|
||||
}
|
||||
if len(strings.TrimSpace(r.RequestID)) > 128 {
|
||||
return errors.New("request_id is too long")
|
||||
}
|
||||
if strings.TrimSpace(r.ExternalUserID) == "" {
|
||||
return errors.New("external_user_id is required")
|
||||
}
|
||||
if len(strings.TrimSpace(r.ExternalUserID)) > 128 {
|
||||
return errors.New("external_user_id is too long")
|
||||
}
|
||||
if r.GiftCount <= 0 || r.UnitAmount <= 0 {
|
||||
return errors.New("gift_count and unit_amount must be positive")
|
||||
}
|
||||
// 网关只接受数量和单价;total_amount 由服务端计算,避免外部 App SDK 或恶意调用方传入不一致账面金额。
|
||||
if r.GiftCount > (1<<63-1)/r.UnitAmount {
|
||||
return errors.New("gift amount is too large")
|
||||
}
|
||||
if currency := strings.ToUpper(strings.TrimSpace(r.Currency)); currency != "" && currency != "COIN" {
|
||||
// 当前 RTP、奖池和风控都以金币等价单位推进;未配置汇率前拒绝其他币种,避免污染奖池水位。
|
||||
return errors.New("currency must be COIN")
|
||||
}
|
||||
if len(strings.TrimSpace(r.Currency)) > 16 {
|
||||
return errors.New("currency is too long")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func metadataJSON(value map[string]any) string {
|
||||
if len(value) == 0 {
|
||||
return "{}"
|
||||
}
|
||||
body, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
// JSON decoder 已保证 map 可序列化;失败时返回空对象,让 owner service 不因展示元数据阻断抽奖。
|
||||
return "{}"
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func normalizePaidAt(now func() time.Time) int64 {
|
||||
return now().UTC().UnixMilli()
|
||||
}
|
||||
|
||||
func (h *Handler) isAllowedApp(appCode string) bool {
|
||||
if len(h.allowedApps) == 0 {
|
||||
return false
|
||||
}
|
||||
_, ok := h.allowedApps[normalizeAppCode(appCode)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func cloneAllowedApps(input map[string]struct{}) map[string]struct{} {
|
||||
out := make(map[string]struct{}, len(input))
|
||||
for app := range input {
|
||||
if normalized := normalizeAppCode(app); normalized != "" {
|
||||
out[normalized] = struct{}{}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeAppCode(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
139
server/luck-gateway/internal/modules/luckygift/handler_test.go
Normal file
139
server/luck-gateway/internal/modules/luckygift/handler_test.go
Normal file
@ -0,0 +1,139 @@
|
||||
package luckygift_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hyapp-luck-gateway/internal/config"
|
||||
"hyapp-luck-gateway/internal/integration/luckygiftclient"
|
||||
"hyapp-luck-gateway/internal/modules/luckygift"
|
||||
"hyapp-luck-gateway/internal/router"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
got luckygiftclient.SendCommand
|
||||
calls int
|
||||
}
|
||||
|
||||
func (f *fakeClient) SendLuckyGift(_ context.Context, cmd luckygiftclient.SendCommand) (luckygiftclient.SendResult, error) {
|
||||
f.calls++
|
||||
f.got = cmd
|
||||
return luckygiftclient.SendResult{DrawID: "draw-1", RequestID: cmd.RequestID, AppCode: cmd.AppCode, RewardStatus: "granted"}, nil
|
||||
}
|
||||
|
||||
func TestSendAcceptsAllowedAppAndComputesAmount(t *testing.T) {
|
||||
client := &fakeClient{}
|
||||
handler := testRouter(client, "aslan,yumi")
|
||||
body := []byte(`{"app_code":"Aslan","request_id":"req-1","external_user_id":"u-1001","gift_count":3,"unit_amount":7,"currency":"COIN","metadata":{"gift":"rose"}}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if client.calls != 1 {
|
||||
t.Fatalf("client calls=%d", client.calls)
|
||||
}
|
||||
if client.got.AppCode != "aslan" || client.got.RequestID != "req-1" || client.got.TotalAmount != 21 || client.got.GiftCount != 3 {
|
||||
t.Fatalf("unexpected command: %+v", client.got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRejectsPoolAndPaidAtOverrides(t *testing.T) {
|
||||
client := &fakeClient{}
|
||||
handler := testRouter(client, "aslan")
|
||||
body := []byte(`{"app_code":"aslan","request_id":"req-1","external_user_id":"u-1001","pool_id":"pool-1","paid_at_ms":1700000000000,"gift_count":1,"unit_amount":7}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if client.calls != 0 {
|
||||
t.Fatalf("client should not be called, calls=%d", client.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRejectsDisallowedAppBeforeClientCall(t *testing.T) {
|
||||
client := &fakeClient{}
|
||||
handler := testRouter(client, "aslan,yumi")
|
||||
body := []byte(`{"app_code":"hyapp","request_id":"req-1","external_user_id":"u-1001","gift_count":1,"unit_amount":9}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if client.calls != 0 {
|
||||
t.Fatalf("client should not be called, calls=%d", client.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRejectsInvalidAmount(t *testing.T) {
|
||||
client := &fakeClient{}
|
||||
handler := testRouter(client, "aslan")
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"app_code": "aslan",
|
||||
"request_id": "req-1",
|
||||
"external_user_id": "u-1",
|
||||
"gift_count": 0,
|
||||
"unit_amount": 10,
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if client.calls != 0 {
|
||||
t.Fatalf("client should not be called, calls=%d", client.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRejectsTooLongExternalFields(t *testing.T) {
|
||||
client := &fakeClient{}
|
||||
handler := testRouter(client, "aslan")
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"app_code": "aslan",
|
||||
"request_id": "req-1",
|
||||
"external_user_id": strings.Repeat("u", 129),
|
||||
"gift_count": 1,
|
||||
"unit_amount": 10,
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if client.calls != 0 {
|
||||
t.Fatalf("client should not be called, calls=%d", client.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func testRouter(client *fakeClient, allowedApps string) http.Handler {
|
||||
cfg := config.Config{AllowedApps: parseAllowedAppsForTest(allowedApps)}
|
||||
return router.New(cfg, router.Handlers{LuckyGift: luckygift.New(client, time.Second, cfg.AllowedApps)})
|
||||
}
|
||||
|
||||
func parseAllowedAppsForTest(raw string) map[string]struct{} {
|
||||
out := map[string]struct{}{}
|
||||
for _, app := range strings.Split(raw, ",") {
|
||||
app = strings.ToLower(strings.TrimSpace(app))
|
||||
if app != "" {
|
||||
out[app] = struct{}{}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
10
server/luck-gateway/internal/modules/luckygift/routes.go
Normal file
10
server/luck-gateway/internal/modules/luckygift/routes.go
Normal file
@ -0,0 +1,10 @@
|
||||
package luckygift
|
||||
|
||||
import "net/http"
|
||||
|
||||
func RegisterRoutes(mux *http.ServeMux, h *Handler) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
mux.Handle("POST /api/v1/lucky-gifts/send", http.HandlerFunc(h.Send))
|
||||
}
|
||||
34
server/luck-gateway/internal/requestid/requestid.go
Normal file
34
server/luck-gateway/internal/requestid/requestid.go
Normal file
@ -0,0 +1,34 @@
|
||||
package requestid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type contextKey struct{}
|
||||
|
||||
func Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestID := r.Header.Get("X-Request-Id")
|
||||
if requestID == "" {
|
||||
requestID = newID()
|
||||
}
|
||||
w.Header().Set("X-Request-Id", requestID)
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), contextKey{}, requestID)))
|
||||
})
|
||||
}
|
||||
|
||||
func FromContext(ctx context.Context) string {
|
||||
requestID, _ := ctx.Value(contextKey{}).(string)
|
||||
return requestID
|
||||
}
|
||||
|
||||
func newID() string {
|
||||
var buf [16]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
return "req-fallback"
|
||||
}
|
||||
return hex.EncodeToString(buf[:])
|
||||
}
|
||||
36
server/luck-gateway/internal/response/response.go
Normal file
36
server/luck-gateway/internal/response/response.go
Normal file
@ -0,0 +1,36 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
CodeOK = 0
|
||||
CodeBadRequest = 40000
|
||||
CodeUnauthorized = 40100
|
||||
CodeForbidden = 40300
|
||||
CodeConflict = 40900
|
||||
CodeServerError = 50000
|
||||
)
|
||||
|
||||
type Body struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func OK(w http.ResponseWriter, requestID string, data any) {
|
||||
write(w, http.StatusOK, Body{Code: CodeOK, Message: "ok", RequestID: requestID, Data: data})
|
||||
}
|
||||
|
||||
func Fail(w http.ResponseWriter, status int, code int, requestID string, message string) {
|
||||
write(w, status, Body{Code: code, Message: message, RequestID: requestID})
|
||||
}
|
||||
|
||||
func write(w http.ResponseWriter, status int, body Body) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
23
server/luck-gateway/internal/router/router.go
Normal file
23
server/luck-gateway/internal/router/router.go
Normal file
@ -0,0 +1,23 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"hyapp-luck-gateway/internal/config"
|
||||
"hyapp-luck-gateway/internal/modules/luckygift"
|
||||
"hyapp-luck-gateway/internal/requestid"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
LuckyGift *luckygift.Handler
|
||||
}
|
||||
|
||||
func New(cfg config.Config, h Handlers) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz/ready", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"status":"ok","service":"luck-gateway"}`))
|
||||
})
|
||||
luckygift.RegisterRoutes(mux, h.LuckyGift)
|
||||
return requestid.Middleware(mux)
|
||||
}
|
||||
@ -37,16 +37,6 @@ message_action_confirm_worker:
|
||||
outbox_lock_ttl: "30s"
|
||||
outbox_max_retry: 8
|
||||
publish_timeout: "5s"
|
||||
lucky_gift_worker:
|
||||
enabled: true
|
||||
worker_poll_interval: "1s"
|
||||
worker_batch_size: 100
|
||||
worker_concurrency: 8
|
||||
worker_lock_ttl: "30s"
|
||||
worker_max_retry: 8
|
||||
publish_timeout: "5s"
|
||||
stats_refresh_interval: "10m"
|
||||
stats_batch_size: 5000
|
||||
tencent_im:
|
||||
# Docker 本地联调全局/区域播报群;必须与 gateway/room-service 使用同一组腾讯 IM 应用配置。
|
||||
enabled: true
|
||||
|
||||
@ -37,16 +37,6 @@ message_action_confirm_worker:
|
||||
outbox_lock_ttl: "30s"
|
||||
outbox_max_retry: 8
|
||||
publish_timeout: "5s"
|
||||
lucky_gift_worker:
|
||||
enabled: true
|
||||
worker_poll_interval: "1s"
|
||||
worker_batch_size: 100
|
||||
worker_concurrency: 8
|
||||
worker_lock_ttl: "30s"
|
||||
worker_max_retry: 8
|
||||
publish_timeout: "5s"
|
||||
stats_refresh_interval: "10m"
|
||||
stats_batch_size: 5000
|
||||
tencent_im:
|
||||
# 腾讯云 IM 应用配置;必须和 gateway 的 UserSig、room-service 房间群配置属于同一个 SDKAppID。
|
||||
enabled: true
|
||||
|
||||
@ -37,16 +37,6 @@ message_action_confirm_worker:
|
||||
outbox_lock_ttl: "30s"
|
||||
outbox_max_retry: 8
|
||||
publish_timeout: "5s"
|
||||
lucky_gift_worker:
|
||||
enabled: true
|
||||
worker_poll_interval: "1s"
|
||||
worker_batch_size: 100
|
||||
worker_concurrency: 8
|
||||
worker_lock_ttl: "30s"
|
||||
worker_max_retry: 8
|
||||
publish_timeout: "5s"
|
||||
stats_refresh_interval: "10m"
|
||||
stats_batch_size: 5000
|
||||
tencent_im:
|
||||
# activity-service 只负责全局/区域播报群;必须与 gateway/room-service 使用同一组腾讯 IM 应用配置。
|
||||
enabled: true
|
||||
|
||||
@ -45,248 +45,11 @@ CREATE TABLE IF NOT EXISTS activity_outbox (
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, outbox_id),
|
||||
KEY idx_activity_outbox_status_retry (app_code, status, next_retry_at_ms),
|
||||
KEY idx_activity_outbox_lucky_retry (app_code, event_type, status, next_retry_at_ms, created_at_ms, outbox_id),
|
||||
KEY idx_activity_outbox_lucky_lock (app_code, event_type, status, lock_until_ms, created_at_ms, outbox_id),
|
||||
KEY idx_activity_outbox_event_retry (app_code, event_type, status, next_retry_at_ms, created_at_ms, outbox_id),
|
||||
KEY idx_activity_outbox_event_lock (app_code, event_type, status, lock_until_ms, created_at_ms, outbox_id),
|
||||
KEY idx_activity_outbox_retention (app_code, status, updated_at_ms, outbox_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动事件 outbox 表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_gift_rule_versions (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
rule_version BIGINT NOT NULL COMMENT '不可变规则版本',
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否启用',
|
||||
target_rtp_ppm BIGINT NOT NULL COMMENT '基础返奖目标,ppm',
|
||||
pool_rate_ppm BIGINT NOT NULL COMMENT '每笔扣费进入基础奖池比例,ppm',
|
||||
settlement_window_wager BIGINT NOT NULL COMMENT 'RTP 结算窗口流水,单位金币',
|
||||
control_band_ppm BIGINT NOT NULL COMMENT '正常波动带,ppm',
|
||||
gift_price_reference BIGINT NOT NULL COMMENT '配置参考礼物价格',
|
||||
novice_max_equivalent_draws BIGINT NOT NULL DEFAULT 2000 COMMENT '新手阶段截止等价抽数,按累计金币/参考价格折算',
|
||||
normal_max_equivalent_draws BIGINT NOT NULL DEFAULT 20000 COMMENT '正常阶段截止等价抽数,超过后进入高阶阶段',
|
||||
max_single_payout BIGINT NOT NULL DEFAULT 50000 COMMENT '单次基础返奖上限,按参考价格配置',
|
||||
user_hourly_payout_cap BIGINT NOT NULL DEFAULT 34200 COMMENT '单用户每小时基础返奖上限,按参考价格配置',
|
||||
user_daily_payout_cap BIGINT NOT NULL DEFAULT 615600 COMMENT '单用户每日基础返奖上限,按参考价格配置',
|
||||
device_daily_payout_cap BIGINT NOT NULL DEFAULT 1026000 COMMENT '单设备每日基础返奖上限,按参考价格配置',
|
||||
room_hourly_payout_cap BIGINT NOT NULL DEFAULT 684000 COMMENT '单房间每小时基础返奖上限,按参考价格配置',
|
||||
anchor_daily_payout_cap BIGINT NOT NULL DEFAULT 12312000 COMMENT '单主播每日基础返奖上限,按参考价格配置',
|
||||
effective_from_ms BIGINT NOT NULL COMMENT '生效时间,UTC epoch ms',
|
||||
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '发布人',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, pool_id, rule_version),
|
||||
KEY idx_lucky_gift_rule_versions_latest (app_code, pool_id, rule_version),
|
||||
KEY idx_lucky_gift_rule_versions_enabled (app_code, enabled, created_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物 v2 规则版本表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_gift_stage_tiers (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
rule_version BIGINT NOT NULL COMMENT '规则版本',
|
||||
stage VARCHAR(32) NOT NULL COMMENT 'novice/normal/advanced',
|
||||
tier_id VARCHAR(96) NOT NULL COMMENT '奖档 ID',
|
||||
multiplier_ppm BIGINT NOT NULL COMMENT '倍率,1x = 1000000',
|
||||
base_weight_ppm BIGINT NOT NULL COMMENT '正常模式基础概率,ppm',
|
||||
reward_source VARCHAR(32) NOT NULL COMMENT 'base_rtp/activity_subsidy/presentation_only',
|
||||
high_water_only TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否高水位才开放',
|
||||
broadcast_level VARCHAR(32) NOT NULL DEFAULT 'none' COMMENT 'none/room/region/global',
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
PRIMARY KEY (app_code, pool_id, rule_version, stage, tier_id),
|
||||
KEY idx_lucky_gift_stage_tiers_stage (app_code, pool_id, rule_version, stage)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物 v2 阶段奖档表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_rtp_windows (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
scope_type VARCHAR(32) NOT NULL COMMENT 'global 或 gift',
|
||||
scope_id VARCHAR(96) NOT NULL COMMENT 'global 或规则作用域',
|
||||
window_index BIGINT NOT NULL COMMENT 'RTP 窗口序号',
|
||||
target_rtp_ppm INT NOT NULL COMMENT '窗口目标 RTP',
|
||||
control_window_draws BIGINT NOT NULL COMMENT '窗口抽数',
|
||||
paid_draws BIGINT NOT NULL DEFAULT 0 COMMENT '窗口已抽次数',
|
||||
wager_coins BIGINT NOT NULL DEFAULT 0 COMMENT '窗口消耗金币',
|
||||
target_payout_coins BIGINT NOT NULL COMMENT '窗口目标基础返奖',
|
||||
actual_base_payout_coins BIGINT NOT NULL DEFAULT 0 COMMENT '窗口实际基础返奖',
|
||||
carry_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '整数除法余数',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'open' COMMENT 'open/closed/underpaid',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, scope_type, scope_id, window_index),
|
||||
KEY idx_lucky_rtp_windows_open (app_code, scope_type, scope_id, status, window_index)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物 RTP 控制窗口';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_pools (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
scope_type VARCHAR(32) NOT NULL COMMENT 'platform/room/gift',
|
||||
scope_id VARCHAR(96) NOT NULL COMMENT 'global/room_id/规则作用域',
|
||||
balance BIGINT NOT NULL DEFAULT 0 COMMENT '当前可用水位',
|
||||
reserve_floor BIGINT NOT NULL DEFAULT 0 COMMENT '安全水位',
|
||||
total_in BIGINT NOT NULL DEFAULT 0 COMMENT '累计入池',
|
||||
total_out BIGINT NOT NULL DEFAULT 0 COMMENT '累计基础返奖支出',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, scope_type, scope_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物三层基础奖池';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_room_atmosphere_pools (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
room_id VARCHAR(96) NOT NULL COMMENT '房间 ID',
|
||||
balance BIGINT NOT NULL DEFAULT 0 COMMENT '房间气氛可用预算',
|
||||
reserve_floor BIGINT NOT NULL DEFAULT 0 COMMENT '安全水位',
|
||||
total_in BIGINT NOT NULL DEFAULT 0 COMMENT '累计入池',
|
||||
total_out BIGINT NOT NULL DEFAULT 0 COMMENT '累计气氛支出',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, room_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物房间气氛池';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_activity_budgets (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
gift_id VARCHAR(96) NOT NULL COMMENT '奖池 ID;当前列名沿用旧内部作用域名',
|
||||
budget_day VARCHAR(32) NOT NULL COMMENT 'UTC 预算日;__total__ 表示活动总预算',
|
||||
budget_coins BIGINT NOT NULL DEFAULT 0 COMMENT '总补贴预算',
|
||||
spent_coins BIGINT NOT NULL DEFAULT 0 COMMENT '已支出补贴',
|
||||
daily_limit BIGINT NOT NULL DEFAULT 0 COMMENT '日支出上限',
|
||||
daily_spent_coins BIGINT NOT NULL DEFAULT 0 COMMENT '当日已支出',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT 'active/paused/exhausted',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, gift_id, budget_day)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物活动补贴预算';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_user_states (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
||||
gift_id VARCHAR(96) NOT NULL COMMENT '奖池 ID;当前列名沿用旧内部作用域名',
|
||||
paid_draws BIGINT NOT NULL DEFAULT 0 COMMENT '该用户幸运礼物累计付费抽数',
|
||||
cumulative_wager_coins BIGINT NOT NULL DEFAULT 0 COMMENT '该用户在当前奖池累计消耗金币',
|
||||
equivalent_draws BIGINT NOT NULL DEFAULT 0 COMMENT '累计金币按规则参考价格折算后的等价抽数',
|
||||
loss_streak BIGINT NOT NULL DEFAULT 0 COMMENT '连续未获得可见奖励次数,中奖后清零',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, user_id, gift_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物用户体验池状态';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_risk_counters (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
scope_type VARCHAR(32) NOT NULL COMMENT 'user/device/room/anchor',
|
||||
scope_id VARCHAR(128) NOT NULL COMMENT '风控主体 ID',
|
||||
window_type VARCHAR(32) NOT NULL COMMENT 'hour/day',
|
||||
bucket_key VARCHAR(32) NOT NULL COMMENT 'UTC 窗口键',
|
||||
payout_coins BIGINT NOT NULL DEFAULT 0 COMMENT '窗口内已支出可见奖励',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, scope_type, scope_id, window_type, bucket_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物风控窗口计数';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_draw_records (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
draw_id VARCHAR(128) NOT NULL COMMENT '抽奖 ID',
|
||||
command_id VARCHAR(128) NOT NULL COMMENT '幂等命令 ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
||||
device_id VARCHAR(128) NOT NULL COMMENT '设备 ID',
|
||||
room_id VARCHAR(96) NOT NULL COMMENT '房间 ID',
|
||||
anchor_id VARCHAR(96) NOT NULL COMMENT '主播关联 ID',
|
||||
visible_region_id BIGINT NOT NULL DEFAULT 0 COMMENT '房间可见区域 ID',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
gift_id VARCHAR(96) NOT NULL COMMENT '礼物 ID',
|
||||
coin_spent BIGINT NOT NULL COMMENT '消耗金币',
|
||||
rule_version BIGINT NOT NULL COMMENT '规则版本',
|
||||
experience_pool VARCHAR(32) NOT NULL COMMENT '体验池',
|
||||
rtp_window_index BIGINT NOT NULL COMMENT '全站 RTP 窗口',
|
||||
gift_rtp_window_index BIGINT NOT NULL COMMENT '礼物 RTP 窗口',
|
||||
selected_tier_id VARCHAR(96) NOT NULL COMMENT '命中奖档',
|
||||
base_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '基础 RTP 返奖',
|
||||
room_atmosphere_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '房间气氛支出',
|
||||
activity_subsidy_coins BIGINT NOT NULL DEFAULT 0 COMMENT '活动补贴支出',
|
||||
effective_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '用户可见总奖励',
|
||||
budget_sources_json JSON NOT NULL COMMENT '预算来源',
|
||||
stage_feedback TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否阶段反馈',
|
||||
high_multiplier TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否高倍命中',
|
||||
candidate_tiers_json JSON NOT NULL COMMENT '候选过滤快照',
|
||||
pool_snapshot_json JSON NOT NULL COMMENT '奖池快照',
|
||||
rtp_snapshot_json JSON NOT NULL COMMENT 'RTP 快照',
|
||||
reward_status VARCHAR(32) NOT NULL COMMENT 'pending/granted/failed',
|
||||
reward_transaction_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '返奖钱包交易 ID',
|
||||
reward_failure_reason VARCHAR(255) NOT NULL DEFAULT '' COMMENT '返奖失败原因',
|
||||
paid_at_ms BIGINT NOT NULL COMMENT '扣费时间,UTC epoch ms',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, draw_id),
|
||||
UNIQUE KEY uk_lucky_draw_command (app_code, command_id),
|
||||
KEY idx_lucky_draw_pool (app_code, pool_id, created_at_ms),
|
||||
KEY idx_lucky_draw_user (app_code, user_id, created_at_ms),
|
||||
KEY idx_lucky_draw_gift (app_code, gift_id, created_at_ms),
|
||||
KEY idx_lucky_draw_room (app_code, room_id, created_at_ms),
|
||||
KEY idx_lucky_draw_region_created (app_code, visible_region_id, created_at_ms, draw_id),
|
||||
KEY idx_lucky_draw_status (app_code, reward_status, updated_at_ms),
|
||||
KEY idx_lucky_draw_created (app_code, created_at_ms, draw_id),
|
||||
KEY idx_lucky_draw_pool_status_summary (app_code, pool_id, reward_status, created_at_ms, draw_id),
|
||||
KEY idx_lucky_draw_gift_status_summary (app_code, pool_id, gift_id, reward_status, created_at_ms, draw_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物抽奖事实';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_draw_pool_stats (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
gift_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '礼物 ID,空值表示奖池汇总',
|
||||
total_draws BIGINT NOT NULL DEFAULT 0 COMMENT '抽奖次数',
|
||||
unique_users BIGINT NOT NULL DEFAULT 0 COMMENT '参与用户数',
|
||||
unique_rooms BIGINT NOT NULL DEFAULT 0 COMMENT '参与房间数',
|
||||
total_spent_coins BIGINT NOT NULL DEFAULT 0 COMMENT '消耗金币',
|
||||
total_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '返还金币',
|
||||
base_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '基础 RTP 返奖',
|
||||
room_atmosphere_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '房间气氛支出',
|
||||
activity_subsidy_coins BIGINT NOT NULL DEFAULT 0 COMMENT '活动补贴支出',
|
||||
pending_draws BIGINT NOT NULL DEFAULT 0 COMMENT '待发放数量',
|
||||
granted_draws BIGINT NOT NULL DEFAULT 0 COMMENT '已发放数量',
|
||||
failed_draws BIGINT NOT NULL DEFAULT 0 COMMENT '发放失败数量',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, pool_id, gift_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物池级汇总统计';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_draw_pool_day_stats (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
stat_day DATE NOT NULL COMMENT 'UTC 统计日',
|
||||
visible_region_id BIGINT NOT NULL DEFAULT 0 COMMENT '房间可见区域 ID',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
gift_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '礼物 ID,空值表示奖池汇总',
|
||||
draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '抽奖次数',
|
||||
turnover_coin BIGINT NOT NULL DEFAULT 0 COMMENT '消耗金币',
|
||||
payout_coin BIGINT NOT NULL DEFAULT 0 COMMENT '用户可见返奖金币',
|
||||
base_reward_coin BIGINT NOT NULL DEFAULT 0 COMMENT '基础 RTP 返奖',
|
||||
room_atmosphere_reward_coin BIGINT NOT NULL DEFAULT 0 COMMENT '房间气氛支出',
|
||||
activity_subsidy_coin BIGINT NOT NULL DEFAULT 0 COMMENT '活动补贴支出',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, stat_day, visible_region_id, pool_id, gift_id),
|
||||
KEY idx_lucky_draw_pool_day_overview (app_code, stat_day, pool_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物 Databi 日维度汇总';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_draw_pool_stat_users (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
gift_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '礼物 ID,空值表示奖池汇总',
|
||||
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, pool_id, gift_id, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物汇总用户去重';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_draw_pool_stat_rooms (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
gift_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '礼物 ID,空值表示奖池汇总',
|
||||
room_id VARCHAR(96) NOT NULL COMMENT '房间 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, pool_id, gift_id, room_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物汇总房间去重';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lucky_draw_pool_stat_cursors (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
cursor_name VARCHAR(64) NOT NULL COMMENT '游标名称',
|
||||
last_draw_created_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '最后已聚合 draw 创建时间',
|
||||
last_draw_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '同一毫秒内最后已聚合 draw ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, cursor_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物后台统计快照游标';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wheel_rule_versions (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
wheel_id VARCHAR(96) NOT NULL COMMENT '转盘 ID',
|
||||
@ -433,6 +196,22 @@ CREATE TABLE IF NOT EXISTS im_broadcast_outbox (
|
||||
KEY idx_im_broadcast_outbox_retention (app_code, status, updated_at_ms, event_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM 广播发件箱表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_reward_policy_instances (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
instance_code VARCHAR(96) NOT NULL COMMENT '后台策略实例编码',
|
||||
template_code VARCHAR(96) NOT NULL COMMENT '模板编码快照',
|
||||
template_version VARCHAR(64) NOT NULL COMMENT '模板版本快照',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
|
||||
reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '任务默认奖励资产:COIN/POINT',
|
||||
rule_json JSON NOT NULL COMMENT '完整策略快照',
|
||||
published_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '发布管理员 ID',
|
||||
published_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '发布时间,UTC epoch ms',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, instance_code),
|
||||
KEY idx_task_reward_policy_active (app_code, status, published_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务奖励运行侧策略实例表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_definitions (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
task_id VARCHAR(96) NOT NULL COMMENT '任务 ID',
|
||||
@ -451,6 +230,7 @@ CREATE TABLE IF NOT EXISTS task_definitions (
|
||||
target_value BIGINT NOT NULL COMMENT '目标值',
|
||||
target_unit VARCHAR(32) NOT NULL COMMENT '目标单位',
|
||||
reward_coin_amount BIGINT NOT NULL COMMENT '奖励金币数量',
|
||||
reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型:COIN/POINT',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
version BIGINT NOT NULL COMMENT '版本号',
|
||||
@ -487,6 +267,7 @@ CREATE TABLE IF NOT EXISTS user_task_progress (
|
||||
progress_value BIGINT NOT NULL COMMENT '进度值',
|
||||
target_value BIGINT NOT NULL COMMENT '目标值',
|
||||
reward_coin_amount BIGINT NOT NULL COMMENT '奖励金币数量',
|
||||
reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型快照:COIN/POINT',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
completed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '完成时间,UTC epoch ms',
|
||||
claimed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '领取时间,UTC epoch ms',
|
||||
@ -524,6 +305,7 @@ CREATE TABLE IF NOT EXISTS task_reward_claims (
|
||||
task_type VARCHAR(32) NOT NULL COMMENT '任务类型',
|
||||
cycle_key VARCHAR(32) NOT NULL COMMENT '周期键',
|
||||
reward_coin_amount BIGINT NOT NULL COMMENT '奖励金币数量',
|
||||
reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型快照:COIN/POINT',
|
||||
wallet_command_id VARCHAR(128) NOT NULL COMMENT '钱包命令 ID',
|
||||
wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
@ -1250,47 +1032,6 @@ CREATE TABLE IF NOT EXISTS message_action_outbox (
|
||||
KEY idx_message_action_outbox_retention (app_code, status, updated_at_ms, event_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='确认消息 IM 投递 outbox';
|
||||
|
||||
-- 本地开发必须开箱即可验证幸运礼物 v2 链路;INSERT IGNORE 只补缺省奖池,不覆盖后台已发布版本。
|
||||
SET @lucky_seed_now_ms := 1779259000000;
|
||||
INSERT IGNORE INTO lucky_gift_rule_versions (
|
||||
app_code, pool_id, rule_version, enabled, target_rtp_ppm, pool_rate_ppm,
|
||||
settlement_window_wager, control_band_ppm, gift_price_reference,
|
||||
novice_max_equivalent_draws, normal_max_equivalent_draws,
|
||||
max_single_payout, user_hourly_payout_cap, user_daily_payout_cap, device_daily_payout_cap,
|
||||
room_hourly_payout_cap, anchor_daily_payout_cap, effective_from_ms, created_by_admin_id, created_at_ms
|
||||
) VALUES
|
||||
('lalu', 'lucky', 1, true, 950000, 960000, 1000000, 10000, 100, 2000, 20000, 50000, 34200, 615600, 1026000, 684000, 12312000, @lucky_seed_now_ms, 0, @lucky_seed_now_ms),
|
||||
('lalu', 'super_lucky', 1, true, 950000, 960000, 1000000, 10000, 100, 2000, 20000, 50000, 34200, 615600, 1026000, 684000, 12312000, @lucky_seed_now_ms, 0, @lucky_seed_now_ms);
|
||||
|
||||
INSERT IGNORE INTO lucky_gift_stage_tiers (
|
||||
app_code, pool_id, rule_version, stage, tier_id, multiplier_ppm, base_weight_ppm,
|
||||
reward_source, high_water_only, broadcast_level, enabled
|
||||
) VALUES
|
||||
('lalu', 'lucky', 1, 'novice', 'novice_none', 0, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'novice', 'novice_0_5x', 500000, 200000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'novice', 'novice_1x', 1000000, 550000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'novice', 'novice_2x', 2000000, 150000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'normal', 'normal_none', 0, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'normal', 'normal_0_5x', 500000, 200000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'normal', 'normal_1x', 1000000, 550000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'normal', 'normal_2x', 2000000, 150000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'advanced', 'advanced_none', 0, 270000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'advanced', 'advanced_1x', 1000000, 600000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'advanced', 'advanced_2x', 2000000, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'advanced', 'advanced_5x', 5000000, 30000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'novice', 'novice_none', 0, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'novice', 'novice_0_5x', 500000, 200000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'novice', 'novice_1x', 1000000, 550000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'novice', 'novice_2x', 2000000, 150000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'normal', 'normal_none', 0, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'normal', 'normal_0_5x', 500000, 200000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'normal', 'normal_1x', 1000000, 550000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'normal', 'normal_2x', 2000000, 150000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'advanced', 'advanced_none', 0, 270000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'advanced', 'advanced_1x', 1000000, 600000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'advanced', 'advanced_2x', 2000000, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'advanced', 'advanced_5x', 5000000, 30000, 'base_rtp', false, 'none', true);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS room_turnover_reward_configs (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否启用',
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
ALTER TABLE task_definitions
|
||||
ADD COLUMN reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型:COIN/POINT' AFTER reward_coin_amount;
|
||||
|
||||
ALTER TABLE user_task_progress
|
||||
ADD COLUMN reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型快照:COIN/POINT' AFTER reward_coin_amount;
|
||||
|
||||
ALTER TABLE task_reward_claims
|
||||
ADD COLUMN reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型快照:COIN/POINT' AFTER reward_coin_amount;
|
||||
@ -0,0 +1,17 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_reward_policy_instances (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
instance_code VARCHAR(96) NOT NULL COMMENT '后台策略实例编码',
|
||||
template_code VARCHAR(96) NOT NULL COMMENT '模板编码快照',
|
||||
template_version VARCHAR(64) NOT NULL COMMENT '模板版本快照',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
|
||||
reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '任务默认奖励资产:COIN/POINT',
|
||||
rule_json JSON NOT NULL COMMENT '完整策略快照',
|
||||
published_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '发布管理员 ID',
|
||||
published_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '发布时间,UTC epoch ms',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, instance_code),
|
||||
KEY idx_task_reward_policy_active (app_code, status, published_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务奖励运行侧策略实例表';
|
||||
@ -20,7 +20,6 @@ import (
|
||||
cumulativerechargeservice "hyapp/services/activity-service/internal/service/cumulativerecharge"
|
||||
firstrechargeservice "hyapp/services/activity-service/internal/service/firstrecharge"
|
||||
inviteactivityservice "hyapp/services/activity-service/internal/service/inviteactivity"
|
||||
luckygiftservice "hyapp/services/activity-service/internal/service/luckygift"
|
||||
messageservice "hyapp/services/activity-service/internal/service/message"
|
||||
mysqlstorage "hyapp/services/activity-service/internal/storage/mysql"
|
||||
)
|
||||
@ -34,16 +33,13 @@ type App struct {
|
||||
healthHTTP *healthhttp.Server
|
||||
mysqlRepo *mysqlstorage.Repository
|
||||
broadcast *broadcastservice.Service
|
||||
luckyGift *luckygiftservice.Service
|
||||
firstRechargeReward *firstrechargeservice.Service
|
||||
cumulativeRecharge *cumulativerechargeservice.Service
|
||||
inviteActivityReward *inviteactivityservice.Service
|
||||
actionConfirm *messageservice.ActionConfirmService
|
||||
broadcastWorkerEnabled bool
|
||||
luckyGiftWorkerEnabled bool
|
||||
messageActionWorkerEnabled bool
|
||||
workerNodeID string
|
||||
luckyGiftWorkerOptions luckygiftservice.WorkerOptions
|
||||
messageActionWorkerOptions config.MessageActionConfirmWorkerConfig
|
||||
messageActionOutboxTopic string
|
||||
userLeaderboardRedisClose func() error
|
||||
@ -133,16 +129,13 @@ func New(cfg config.Config) (*App, error) {
|
||||
healthHTTP: healthHTTP,
|
||||
mysqlRepo: repository,
|
||||
broadcast: services.broadcast,
|
||||
luckyGift: services.luckyGift,
|
||||
firstRechargeReward: services.firstRechargeReward,
|
||||
cumulativeRecharge: services.cumulativeRecharge,
|
||||
inviteActivityReward: services.inviteActivityReward,
|
||||
actionConfirm: services.actionConfirm,
|
||||
broadcastWorkerEnabled: cfg.Broadcast.Enabled,
|
||||
luckyGiftWorkerEnabled: cfg.LuckyGiftWorker.Enabled,
|
||||
messageActionWorkerEnabled: cfg.MessageActionConfirmWorker.Enabled,
|
||||
workerNodeID: cfg.NodeID,
|
||||
luckyGiftWorkerOptions: luckyGiftWorkerOptions(cfg.NodeID, cfg.LuckyGiftWorker),
|
||||
messageActionWorkerOptions: cfg.MessageActionConfirmWorker,
|
||||
messageActionOutboxTopic: cfg.RocketMQ.MessageActionOutbox.Topic,
|
||||
userLeaderboardRedisClose: userLeaderboardRedisClose,
|
||||
@ -168,11 +161,6 @@ func (a *App) Run() error {
|
||||
a.broadcast.RunWorker(ctx, broadcastservice.WorkerOptions{})
|
||||
})
|
||||
}
|
||||
if a.luckyGiftWorkerEnabled && a.luckyGift != nil && a.workers != nil {
|
||||
a.workers.Go(func(ctx context.Context) {
|
||||
a.luckyGift.RunWorker(ctx, a.luckyGiftWorkerOptions)
|
||||
})
|
||||
}
|
||||
if a.messageActionWorkerEnabled && a.actionConfirm != nil && a.messageActionProducer != nil && a.workers != nil {
|
||||
a.workers.Go(func(ctx context.Context) {
|
||||
a.runMessageActionOutboxWorker(ctx)
|
||||
|
||||
@ -19,8 +19,6 @@ func registerGRPCServers(server *grpc.Server, services *serviceBundle) {
|
||||
activityv1.RegisterAdminGrowthLevelServiceServer(server, grpcserver.NewAdminGrowthLevelServer(services.growth))
|
||||
activityv1.RegisterAchievementServiceServer(server, grpcserver.NewAchievementServer(services.achievement))
|
||||
activityv1.RegisterAdminAchievementServiceServer(server, grpcserver.NewAdminAchievementServer(services.achievement))
|
||||
activityv1.RegisterLuckyGiftServiceServer(server, grpcserver.NewLuckyGiftServer(services.luckyGift))
|
||||
activityv1.RegisterAdminLuckyGiftServiceServer(server, grpcserver.NewAdminLuckyGiftServer(services.luckyGift))
|
||||
activityv1.RegisterWheelServiceServer(server, grpcserver.NewWheelServer(services.wheel))
|
||||
activityv1.RegisterAdminWheelServiceServer(server, grpcserver.NewAdminWheelServer(services.wheel))
|
||||
activityv1.RegisterRegistrationRewardServiceServer(server, grpcserver.NewRegistrationRewardServer(services.registrationReward))
|
||||
|
||||
@ -18,7 +18,6 @@ import (
|
||||
firstrechargeservice "hyapp/services/activity-service/internal/service/firstrecharge"
|
||||
growthservice "hyapp/services/activity-service/internal/service/growth"
|
||||
inviteactivityservice "hyapp/services/activity-service/internal/service/inviteactivity"
|
||||
luckygiftservice "hyapp/services/activity-service/internal/service/luckygift"
|
||||
messageservice "hyapp/services/activity-service/internal/service/message"
|
||||
registrationrewardservice "hyapp/services/activity-service/internal/service/registrationreward"
|
||||
roomturnoverrewardservice "hyapp/services/activity-service/internal/service/roomturnoverreward"
|
||||
@ -43,7 +42,6 @@ type serviceBundle struct {
|
||||
cpWeeklyRank *cpweeklyrankservice.Service
|
||||
roomTurnoverReward *roomturnoverrewardservice.Service
|
||||
broadcast *broadcastservice.Service
|
||||
luckyGift *luckygiftservice.Service
|
||||
wheel *wheelservice.Service
|
||||
firstRechargeReward *firstrechargeservice.Service
|
||||
cumulativeRecharge *cumulativerechargeservice.Service
|
||||
@ -71,14 +69,12 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
|
||||
|
||||
// Tencent IM 是区域广播、房间礼物播报和红包播报的外部发布器。
|
||||
// 如果配置要求启动广播 worker,就必须先确认 publisher 可用,避免 worker 启动后只写 outbox 不发消息。
|
||||
var tencentClient *tencentim.RESTClient
|
||||
var broadcastPublisher broadcastservice.Publisher
|
||||
if cfg.TencentIM.Enabled {
|
||||
client, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tencentClient = client
|
||||
broadcastPublisher = client
|
||||
}
|
||||
if cfg.Broadcast.Enabled && broadcastPublisher == nil {
|
||||
@ -98,19 +94,6 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
|
||||
}, repository, broadcastPublisher, activityclient.NewGRPCRegionSource(clients.userConn))
|
||||
userProfileSource := activityclient.NewGRPCUserProfileSource(clients.userConn)
|
||||
broadcastSvc.SetSenderProfileSource(userProfileSource)
|
||||
|
||||
luckyGiftOptions := []luckygiftservice.Option{
|
||||
luckygiftservice.WithWallet(walletClient),
|
||||
luckygiftservice.WithRegionBroadcaster(broadcastSvc),
|
||||
luckygiftservice.WithSenderProfileSource(userProfileSource),
|
||||
}
|
||||
if tencentClient != nil {
|
||||
luckyGiftOptions = append(luckyGiftOptions,
|
||||
luckygiftservice.WithRoomPublisher(tencentClient),
|
||||
luckygiftservice.WithUserPublisher(tencentClient),
|
||||
)
|
||||
}
|
||||
luckyGiftSvc := luckygiftservice.New(repository, luckyGiftOptions...)
|
||||
wheelSvc := wheelservice.New(repository, wheelservice.WithWallet(walletClient))
|
||||
firstRechargeRewardSvc := firstrechargeservice.New(repository, walletClient)
|
||||
cumulativeRechargeSvc := cumulativerechargeservice.New(repository, walletClient)
|
||||
@ -130,7 +113,6 @@ func buildServiceBundle(cfg config.Config, repository *mysqlstorage.Repository,
|
||||
cpWeeklyRank: cpWeeklyRankSvc,
|
||||
roomTurnoverReward: roomTurnoverRewardSvc,
|
||||
broadcast: broadcastSvc,
|
||||
luckyGift: luckyGiftSvc,
|
||||
wheel: wheelSvc,
|
||||
firstRechargeReward: firstRechargeRewardSvc,
|
||||
cumulativeRecharge: cumulativeRechargeSvc,
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"hyapp/services/activity-service/internal/config"
|
||||
luckygiftservice "hyapp/services/activity-service/internal/service/luckygift"
|
||||
)
|
||||
|
||||
func luckyGiftWorkerOptions(nodeID string, cfg config.LuckyGiftWorkerConfig) luckygiftservice.WorkerOptions {
|
||||
return luckygiftservice.WorkerOptions{
|
||||
WorkerID: nodeID + "-lucky-gift",
|
||||
PollInterval: cfg.WorkerPollInterval,
|
||||
BatchSize: cfg.WorkerBatchSize,
|
||||
Concurrency: cfg.WorkerConcurrency,
|
||||
LockTTL: cfg.WorkerLockTTL,
|
||||
MaxRetry: cfg.WorkerMaxRetry,
|
||||
PublishTimeout: cfg.PublishTimeout,
|
||||
StatsInterval: cfg.StatsRefreshInterval,
|
||||
StatsBatchSize: cfg.StatsBatchSize,
|
||||
}
|
||||
}
|
||||
@ -39,8 +39,6 @@ type Config struct {
|
||||
TaskEventWorker TaskEventWorkerConfig `yaml:"task_event_worker"`
|
||||
// UserLeaderboardWorker 控制用户送礼榜单对 wallet_outbox 送礼事实的 Redis 投影。
|
||||
UserLeaderboardWorker UserLeaderboardWorkerConfig `yaml:"user_leaderboard_worker"`
|
||||
// LuckyGiftWorker 控制幸运礼物 draw outbox 的返奖和房间 IM 补偿。
|
||||
LuckyGiftWorker LuckyGiftWorkerConfig `yaml:"lucky_gift_worker"`
|
||||
// TencentIM 是 activity-service 发送全局/区域播报群消息的 REST 配置。
|
||||
TencentIM TencentIMConfig `yaml:"tencent_im"`
|
||||
// Broadcast 控制 IM 播报群 reconciler、outbox worker 和贵重礼物阈值。
|
||||
@ -121,19 +119,6 @@ type UserLeaderboardWorkerConfig struct {
|
||||
KeyPrefix string `yaml:"key_prefix"`
|
||||
}
|
||||
|
||||
// LuckyGiftWorkerConfig 保存幸运礼物抽奖副作用补偿策略。
|
||||
type LuckyGiftWorkerConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
WorkerPollInterval time.Duration `yaml:"worker_poll_interval"`
|
||||
WorkerBatchSize int `yaml:"worker_batch_size"`
|
||||
WorkerConcurrency int `yaml:"worker_concurrency"`
|
||||
WorkerLockTTL time.Duration `yaml:"worker_lock_ttl"`
|
||||
WorkerMaxRetry int `yaml:"worker_max_retry"`
|
||||
PublishTimeout time.Duration `yaml:"publish_timeout"`
|
||||
StatsRefreshInterval time.Duration `yaml:"stats_refresh_interval"`
|
||||
StatsBatchSize int `yaml:"stats_batch_size"`
|
||||
}
|
||||
|
||||
// TencentIMConfig 描述 activity-service 调用腾讯云 IM REST API 的配置。
|
||||
type TencentIMConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
@ -274,17 +259,6 @@ func Default() Config {
|
||||
OutboxMaxRetry: 8,
|
||||
PublishTimeout: 5 * time.Second,
|
||||
},
|
||||
LuckyGiftWorker: LuckyGiftWorkerConfig{
|
||||
Enabled: false,
|
||||
WorkerPollInterval: time.Second,
|
||||
WorkerBatchSize: 100,
|
||||
WorkerConcurrency: 4,
|
||||
WorkerLockTTL: 30 * time.Second,
|
||||
WorkerMaxRetry: 8,
|
||||
PublishTimeout: 5 * time.Second,
|
||||
StatsRefreshInterval: 10 * time.Minute,
|
||||
StatsBatchSize: 5000,
|
||||
},
|
||||
TencentIM: TencentIMConfig{
|
||||
Enabled: false,
|
||||
AdminIdentifier: "administrator",
|
||||
@ -429,27 +403,6 @@ func Load(path string) (Config, error) {
|
||||
if cfg.Broadcast.WorkerMaxRetry <= 0 {
|
||||
cfg.Broadcast.WorkerMaxRetry = 8
|
||||
}
|
||||
if cfg.LuckyGiftWorker.WorkerPollInterval <= 0 {
|
||||
cfg.LuckyGiftWorker.WorkerPollInterval = time.Second
|
||||
}
|
||||
if cfg.LuckyGiftWorker.WorkerBatchSize <= 0 {
|
||||
cfg.LuckyGiftWorker.WorkerBatchSize = 100
|
||||
}
|
||||
if cfg.LuckyGiftWorker.WorkerConcurrency <= 0 {
|
||||
cfg.LuckyGiftWorker.WorkerConcurrency = 4
|
||||
}
|
||||
if cfg.LuckyGiftWorker.WorkerConcurrency > cfg.LuckyGiftWorker.WorkerBatchSize {
|
||||
cfg.LuckyGiftWorker.WorkerConcurrency = cfg.LuckyGiftWorker.WorkerBatchSize
|
||||
}
|
||||
if cfg.LuckyGiftWorker.WorkerLockTTL <= 0 {
|
||||
cfg.LuckyGiftWorker.WorkerLockTTL = 30 * time.Second
|
||||
}
|
||||
if cfg.LuckyGiftWorker.WorkerMaxRetry <= 0 {
|
||||
cfg.LuckyGiftWorker.WorkerMaxRetry = 8
|
||||
}
|
||||
if cfg.LuckyGiftWorker.PublishTimeout <= 0 {
|
||||
cfg.LuckyGiftWorker.PublishTimeout = 5 * time.Second
|
||||
}
|
||||
if cfg.MessageActionConfirmWorker.OutboxPollInterval <= 0 {
|
||||
cfg.MessageActionConfirmWorker.OutboxPollInterval = time.Second
|
||||
}
|
||||
@ -484,10 +437,6 @@ func Load(path string) (Config, error) {
|
||||
if cfg.MessageActionConfirmWorker.Enabled && (!cfg.RocketMQ.UserOutbox.Enabled || !cfg.RocketMQ.MessageActionOutbox.Enabled) {
|
||||
return Config{}, errors.New("message action confirm worker requires rocketmq.user_outbox.enabled and rocketmq.message_action_outbox.enabled")
|
||||
}
|
||||
if cfg.LuckyGiftWorker.Enabled && !cfg.TencentIM.Enabled {
|
||||
return Config{}, errors.New("lucky gift worker requires tencent_im.enabled")
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
|
||||
@ -52,6 +52,9 @@ const (
|
||||
EventStatusConsumed = "consumed"
|
||||
EventStatusSkipped = "skipped"
|
||||
EventStatusFailed = "failed"
|
||||
|
||||
RewardAssetCoin = "COIN"
|
||||
RewardAssetPoint = "POINT"
|
||||
)
|
||||
|
||||
// Definition 是 task_definitions 的当前版本读模型;App 查询只读这张表和用户进度表。
|
||||
@ -73,6 +76,7 @@ type Definition struct {
|
||||
TargetValue int64
|
||||
TargetUnit string
|
||||
RewardCoinAmount int64
|
||||
RewardAssetType string
|
||||
Status string
|
||||
SortOrder int32
|
||||
Version int64
|
||||
@ -95,6 +99,7 @@ type Progress struct {
|
||||
ProgressValue int64
|
||||
TargetValue int64
|
||||
RewardCoinAmount int64
|
||||
RewardAssetType string
|
||||
Status string
|
||||
CompletedAtMS int64
|
||||
ClaimedAtMS int64
|
||||
@ -155,6 +160,7 @@ type DefinitionCommand struct {
|
||||
TargetValue int64
|
||||
TargetUnit string
|
||||
RewardCoinAmount int64
|
||||
RewardAssetType string
|
||||
Status string
|
||||
SortOrder int32
|
||||
EffectiveFromMS int64
|
||||
@ -162,6 +168,18 @@ type DefinitionCommand struct {
|
||||
OperatorAdminID int64
|
||||
}
|
||||
|
||||
// RewardPolicyCommand 是后台 policy instance 发布到 activity 运行侧的任务奖励默认资产快照。
|
||||
type RewardPolicyCommand struct {
|
||||
InstanceCode string
|
||||
TemplateCode string
|
||||
TemplateVersion string
|
||||
Status string
|
||||
RewardAssetType string
|
||||
RuleJSON string
|
||||
OperatorAdminID int64
|
||||
PublishedAtMS int64
|
||||
}
|
||||
|
||||
// Event 是由 wallet/user/room outbox 派生的任务进度事实。
|
||||
type Event struct {
|
||||
EventID string
|
||||
@ -199,6 +217,7 @@ type Claim struct {
|
||||
TaskType string
|
||||
CycleKey string
|
||||
RewardCoinAmount int64
|
||||
RewardAssetType string
|
||||
WalletCommandID string
|
||||
WalletTransactionID string
|
||||
Status string
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
package luckygift
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultRuleConfigPassesValidation(t *testing.T) {
|
||||
config := DefaultRuleConfig("hyapp", "pool_v2")
|
||||
config.Enabled = true
|
||||
|
||||
if err := validateRuleConfig(config); err != nil {
|
||||
t.Fatalf("default v2 lucky gift config should be publishable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRuleConfigRejectsPercentSubmittedAsPPM(t *testing.T) {
|
||||
config := DefaultRuleConfig("hyapp", "pool_bad_rtp")
|
||||
config.TargetRTPPPM = 95
|
||||
|
||||
if err := validateRuleConfig(config); err == nil {
|
||||
t.Fatalf("expected raw percent value in ppm field to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRuleConfigRejectsStageWeightMismatch(t *testing.T) {
|
||||
config := DefaultRuleConfig("hyapp", "pool_bad_weight")
|
||||
config.Stages[0].Tiers[0].BaseWeightPPM = 299_999
|
||||
|
||||
if err := validateRuleConfig(config); err == nil {
|
||||
t.Fatalf("expected stage probability sum mismatch to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRuleConfigRejectsMissingRiskCaps(t *testing.T) {
|
||||
config := DefaultRuleConfig("hyapp", "pool_bad_cap")
|
||||
config.RoomHourlyPayoutCap = 0
|
||||
|
||||
if err := validateRuleConfig(config); err == nil {
|
||||
t.Fatalf("expected missing v2 risk cap to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRuleConfigRejectsInvalidEquivalentDrawThresholds(t *testing.T) {
|
||||
config := DefaultRuleConfig("hyapp", "pool_bad_stage_threshold")
|
||||
config.NormalMaxEquivalentDraws = config.NoviceMaxEquivalentDraws - 1
|
||||
|
||||
if err := validateRuleConfig(config); err == nil {
|
||||
t.Fatalf("expected invalid equivalent draw thresholds to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRuleConfigRequiresExplicitZeroTier(t *testing.T) {
|
||||
config := DefaultRuleConfig("hyapp", "pool_no_zero")
|
||||
config.Stages[1].Tiers = config.Stages[1].Tiers[1:]
|
||||
|
||||
if err := validateRuleConfig(config); err == nil {
|
||||
t.Fatalf("expected stage without 0x tier to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRuleConfigRejectsDuplicatedStageTierID(t *testing.T) {
|
||||
config := DefaultRuleConfig("hyapp", "pool_duplicate_tier")
|
||||
advanced := &config.Stages[2]
|
||||
duplicate := advanced.Tiers[len(advanced.Tiers)-1]
|
||||
duplicate.MultiplierPPM = 10_000_000
|
||||
duplicate.BaseWeightPPM = 0
|
||||
advanced.Tiers = append(advanced.Tiers, duplicate)
|
||||
|
||||
if err := validateRuleConfig(config); err == nil {
|
||||
t.Fatalf("expected duplicate tier id inside one stage to be rejected")
|
||||
}
|
||||
}
|
||||
@ -1,783 +0,0 @@
|
||||
package luckygift
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/tencentim"
|
||||
broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
|
||||
domain "hyapp/services/activity-service/internal/domain/luckygift"
|
||||
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
||||
)
|
||||
|
||||
func TestGetConfigReturnsPoolDefault(t *testing.T) {
|
||||
repository := &fakeLuckyGiftRepository{}
|
||||
service := New(repository)
|
||||
|
||||
config, err := service.GetConfig(appcode.WithContext(context.Background(), "hyapp"), "pool_95")
|
||||
if err != nil {
|
||||
t.Fatalf("GetConfig returned error: %v", err)
|
||||
}
|
||||
if repository.getConfigCalls != 1 {
|
||||
t.Fatalf("expected repository to be called once, got %d", repository.getConfigCalls)
|
||||
}
|
||||
if config.PoolID != "pool_95" {
|
||||
t.Fatalf("expected pool config scope, got pool=%q", config.PoolID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertConfigPreservesPoolScope(t *testing.T) {
|
||||
repository := &fakeLuckyGiftRepository{}
|
||||
service := New(repository)
|
||||
config := DefaultRuleConfig("hyapp", "pool_98")
|
||||
config.Enabled = true
|
||||
|
||||
if _, err := service.UpsertConfig(appcode.WithContext(context.Background(), "hyapp"), config); err != nil {
|
||||
t.Fatalf("UpsertConfig returned error: %v", err)
|
||||
}
|
||||
if repository.upserted.PoolID != "pool_98" {
|
||||
t.Fatalf("expected pool scope, got pool=%q", repository.upserted.PoolID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrawCreditsLuckyGiftRewardFastPath(t *testing.T) {
|
||||
repository := &fakeLuckyGiftRepository{executeResult: domain.DrawResult{
|
||||
DrawID: "lucky_draw_fast_1",
|
||||
DrawIDs: []string{"lucky_draw_fast_1", "lucky_draw_fast_2"},
|
||||
CommandID: "cmd-gift-fast",
|
||||
PoolID: "super_lucky",
|
||||
GiftID: "rose",
|
||||
RuleVersion: 12,
|
||||
ExperiencePool: "novice",
|
||||
SelectedTierID: "batch",
|
||||
MultiplierPPM: 3_000_000,
|
||||
EffectiveRewardCoins: 300,
|
||||
RewardStatus: domain.StatusPending,
|
||||
CreatedAtMS: 1760000000000,
|
||||
}}
|
||||
wallet := &fakeLuckyGiftWallet{balanceAfter: 1300}
|
||||
userPublisher := &fakeLuckyGiftUserPublisher{}
|
||||
service := New(repository, WithWallet(wallet), WithUserPublisher(userPublisher))
|
||||
service.SetClock(func() time.Time { return time.UnixMilli(1760000001000).UTC() })
|
||||
|
||||
result, err := service.Draw(appcode.WithContext(context.Background(), "lalu"), domain.DrawCommand{
|
||||
CommandID: "cmd-gift-fast",
|
||||
PoolID: "super_lucky",
|
||||
UserID: 42,
|
||||
TargetUserID: 99,
|
||||
CountryID: 15,
|
||||
DeviceID: "device-1",
|
||||
RoomID: "room-1",
|
||||
AnchorID: "99",
|
||||
GiftID: "rose",
|
||||
GiftCount: 2,
|
||||
CoinSpent: 200,
|
||||
PaidAtMS: 1760000000000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Draw failed: %v", err)
|
||||
}
|
||||
if wallet.last == nil || wallet.last.GetCommandId() != "lucky_reward:lucky_draw_fast_1" || wallet.last.GetAmount() != 300 || wallet.last.GetTargetUserId() != 42 || wallet.last.GetCountryId() != 15 {
|
||||
t.Fatalf("wallet fast path request mismatch: %+v", wallet.last)
|
||||
}
|
||||
if result.RewardStatus != domain.StatusGranted || result.WalletTransactionID != "wallet_tx_lucky" || result.CoinBalanceAfter != 0 {
|
||||
t.Fatalf("fast path result mismatch: %+v", result)
|
||||
}
|
||||
if got := strings.Join(repository.grantedDrawIDs, ","); got != "lucky_draw_fast_1,lucky_draw_fast_2" {
|
||||
t.Fatalf("fast path should mark all draw ids granted, got %s", got)
|
||||
}
|
||||
assertLuckyGiftWalletNotice(t, userPublisher.last, "42", "wallet_tx_lucky", 300, 1300)
|
||||
}
|
||||
|
||||
func TestDrawBatchLeavesRewardSettlementToWorker(t *testing.T) {
|
||||
repository := &fakeLuckyGiftRepository{executeBatchResults: []domain.DrawResult{{
|
||||
DrawID: "lucky_draw_batch_async_1",
|
||||
CommandID: "cmd-gift-batch:target:42",
|
||||
PoolID: "super_lucky",
|
||||
GiftID: "rose",
|
||||
EffectiveRewardCoins: 300,
|
||||
RewardStatus: domain.StatusPending,
|
||||
}}}
|
||||
wallet := &fakeLuckyGiftWallet{}
|
||||
service := New(repository, WithWallet(wallet))
|
||||
|
||||
results, err := service.DrawBatch(appcode.WithContext(context.Background(), "lalu"), []domain.DrawCommand{{
|
||||
CommandID: "cmd-gift-batch:target:42",
|
||||
PoolID: "super_lucky",
|
||||
UserID: 42,
|
||||
TargetUserID: 99,
|
||||
DeviceID: "device-1",
|
||||
RoomID: "room-1",
|
||||
AnchorID: "42",
|
||||
GiftID: "rose",
|
||||
GiftCount: 1,
|
||||
CoinSpent: 100,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("DrawBatch failed: %v", err)
|
||||
}
|
||||
if len(results) != 1 || results[0].DrawID != "lucky_draw_batch_async_1" {
|
||||
t.Fatalf("DrawBatch result mismatch: %+v", results)
|
||||
}
|
||||
if wallet.last != nil || repository.grantedDrawID != "" || len(repository.grantedDrawIDs) != 0 {
|
||||
t.Fatalf("batch draw must not run synchronous wallet fast path: wallet=%+v granted=%s granted_ids=%v", wallet.last, repository.grantedDrawID, repository.grantedDrawIDs)
|
||||
}
|
||||
if len(repository.executeBatchCmds) != 1 || repository.executeBatchCmds[0].CommandID != "cmd-gift-batch:target:42" {
|
||||
t.Fatalf("batch draw must pass normalized commands to repository: %+v", repository.executeBatchCmds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrawKeepsPendingWhenFastPathWalletFails(t *testing.T) {
|
||||
repository := &fakeLuckyGiftRepository{executeResult: domain.DrawResult{
|
||||
DrawID: "lucky_draw_retry",
|
||||
DrawIDs: []string{"lucky_draw_retry"},
|
||||
CommandID: "cmd-gift-retry",
|
||||
PoolID: "super_lucky",
|
||||
GiftID: "rose",
|
||||
EffectiveRewardCoins: 300,
|
||||
RewardStatus: domain.StatusPending,
|
||||
}}
|
||||
wallet := &fakeLuckyGiftWallet{err: errors.New("wallet timeout")}
|
||||
service := New(repository, WithWallet(wallet))
|
||||
|
||||
result, err := service.Draw(appcode.WithContext(context.Background(), "lalu"), domain.DrawCommand{
|
||||
CommandID: "cmd-gift-retry",
|
||||
PoolID: "super_lucky",
|
||||
UserID: 42,
|
||||
DeviceID: "device-1",
|
||||
RoomID: "room-1",
|
||||
AnchorID: "99",
|
||||
GiftID: "rose",
|
||||
GiftCount: 1,
|
||||
CoinSpent: 100,
|
||||
PaidAtMS: 1760000000000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Draw should keep the committed draw fact: %v", err)
|
||||
}
|
||||
if result.RewardStatus != domain.StatusPending || result.WalletTransactionID != "" || len(repository.grantedDrawIDs) != 0 {
|
||||
t.Fatalf("wallet failure must leave draw pending for outbox compensation: result=%+v granted=%v", result, repository.grantedDrawIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPendingDrawOutboxCreditsWalletAndSkipsRoomDisplayBelowTenTimes(t *testing.T) {
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"event_type": "lucky_gift_drawn",
|
||||
"event_id": "lucky_gift_drawn:lucky_draw_test",
|
||||
"app_code": "lalu",
|
||||
"draw_id": "lucky_draw_test",
|
||||
"command_id": "cmd-gift",
|
||||
"pool_id": "super_lucky",
|
||||
"user_id": 42,
|
||||
"sender_user_id": 42,
|
||||
"target_user_id": 99,
|
||||
"country_id": 15,
|
||||
"visible_region_id": 210,
|
||||
"room_id": "room-1",
|
||||
"gift_id": "rose",
|
||||
"gift_count": 1,
|
||||
"coin_spent": 100,
|
||||
"effective_reward_coins": 300,
|
||||
"multiplier_ppm": 3000000,
|
||||
"stage_feedback": true,
|
||||
"created_at_ms": 1760000000000,
|
||||
})
|
||||
repository := &fakeLuckyGiftRepository{
|
||||
outbox: []domain.DrawOutbox{{
|
||||
AppCode: "lalu",
|
||||
OutboxID: "lucky_lucky_draw_test",
|
||||
EventType: "LuckyGiftDrawn",
|
||||
PayloadJSON: string(payload),
|
||||
}},
|
||||
}
|
||||
wallet := &fakeLuckyGiftWallet{}
|
||||
publisher := &fakeLuckyGiftPublisher{}
|
||||
userPublisher := &fakeLuckyGiftUserPublisher{}
|
||||
broadcaster := &fakeLuckyGiftRegionBroadcaster{}
|
||||
service := New(repository, WithWallet(wallet), WithRoomPublisher(publisher), WithUserPublisher(userPublisher), WithRegionBroadcaster(broadcaster))
|
||||
service.SetClock(func() time.Time { return time.UnixMilli(1760000001000).UTC() })
|
||||
|
||||
processed, err := service.ProcessPendingDrawOutbox(context.Background(), WorkerOptions{WorkerID: "worker-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessPendingDrawOutbox failed: %v", err)
|
||||
}
|
||||
if processed != 1 {
|
||||
t.Fatalf("processed count mismatch: %d", processed)
|
||||
}
|
||||
if wallet.last == nil || wallet.last.GetDrawId() != "lucky_draw_test" || wallet.last.GetTargetUserId() != 42 || wallet.last.GetAmount() != 300 || wallet.last.GetCountryId() != 15 {
|
||||
t.Fatalf("wallet reward request mismatch: %+v", wallet.last)
|
||||
}
|
||||
if publisher.last.EventID != "" {
|
||||
t.Fatalf("3x lucky gift must not publish room display: %+v", publisher.last)
|
||||
}
|
||||
if broadcaster.last.EventID != "" {
|
||||
t.Fatalf("3x lucky gift must not enter region broadcast: %+v", broadcaster.last)
|
||||
}
|
||||
assertLuckyGiftWalletNotice(t, userPublisher.last, "42", "wallet_tx_lucky", 300, 0)
|
||||
if repository.grantedDrawID != "lucky_draw_test" || repository.deliveredOutboxID != "lucky_lucky_draw_test" {
|
||||
t.Fatalf("repository settlement mismatch: granted=%s delivered=%s", repository.grantedDrawID, repository.deliveredOutboxID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPendingDrawOutboxPublishesTenTimesRegionBroadcastWithSenderProfile(t *testing.T) {
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"event_type": "lucky_gift_drawn",
|
||||
"event_id": "lucky_gift_drawn:lucky_draw_10x",
|
||||
"app_code": "lalu",
|
||||
"draw_id": "lucky_draw_10x",
|
||||
"command_id": "cmd-gift-10x",
|
||||
"pool_id": "super_lucky",
|
||||
"user_id": 42,
|
||||
"sender_user_id": 42,
|
||||
"target_user_id": 99,
|
||||
"country_id": 15,
|
||||
"visible_region_id": 210,
|
||||
"room_id": "room-1",
|
||||
"gift_id": "rose",
|
||||
"gift_count": 1,
|
||||
"coin_spent": 100,
|
||||
"effective_reward_coins": 1000,
|
||||
"multiplier_ppm": luckyGiftBroadcastMinMultiplierPPM,
|
||||
"created_at_ms": 1760000000000,
|
||||
})
|
||||
repository := &fakeLuckyGiftRepository{
|
||||
outbox: []domain.DrawOutbox{{
|
||||
AppCode: "lalu",
|
||||
OutboxID: "lucky_lucky_draw_10x",
|
||||
EventType: "LuckyGiftDrawn",
|
||||
PayloadJSON: string(payload),
|
||||
}},
|
||||
}
|
||||
wallet := &fakeLuckyGiftWallet{}
|
||||
publisher := &fakeLuckyGiftPublisher{}
|
||||
userPublisher := &fakeLuckyGiftUserPublisher{}
|
||||
broadcaster := &fakeLuckyGiftRegionBroadcaster{}
|
||||
service := New(repository,
|
||||
WithWallet(wallet),
|
||||
WithRoomPublisher(publisher),
|
||||
WithUserPublisher(userPublisher),
|
||||
WithRegionBroadcaster(broadcaster),
|
||||
WithSenderProfileSource(fakeLuckyGiftSenderProfileSource{profiles: map[int64]broadcastservice.SenderProfile{
|
||||
42: {UserID: 42, Account: "160042", Nickname: "Real Sender", Avatar: "https://cdn.example/sender.png"},
|
||||
}}),
|
||||
)
|
||||
service.SetClock(func() time.Time { return time.UnixMilli(1760000001000).UTC() })
|
||||
|
||||
processed, err := service.ProcessPendingDrawOutbox(appcode.WithContext(context.Background(), "lalu"), WorkerOptions{WorkerID: "worker-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessPendingDrawOutbox failed: %v", err)
|
||||
}
|
||||
if processed != 1 {
|
||||
t.Fatalf("processed count mismatch: %d", processed)
|
||||
}
|
||||
assertLuckyGiftRoomMessage(t, publisher.last, "room-1", "lucky_gift_drawn:lucky_draw_10x", "lucky_draw_10x", 1000)
|
||||
var roomPayload map[string]any
|
||||
if err := json.Unmarshal(publisher.last.PayloadJSON, &roomPayload); err != nil {
|
||||
t.Fatalf("room payload is invalid: %v", err)
|
||||
}
|
||||
if roomPayload["sender_name"] != "Real Sender" || roomPayload["sender_display_user_id"] != "160042" {
|
||||
t.Fatalf("room payload must include sender display snapshot: %+v", roomPayload)
|
||||
}
|
||||
if broadcaster.last.EventID != "lucky_gift_big_win:lucky_draw_10x" || broadcaster.last.RegionID != 210 {
|
||||
t.Fatalf("10x lucky gift region broadcast mismatch: %+v", broadcaster.last)
|
||||
}
|
||||
var regionPayload map[string]any
|
||||
if err := json.Unmarshal([]byte(broadcaster.last.PayloadJSON), ®ionPayload); err != nil {
|
||||
t.Fatalf("region payload is invalid: %v", err)
|
||||
}
|
||||
if regionPayload["sender_name"] != "Real Sender" || regionPayload["sender_display_user_id"] != "160042" || regionPayload["sender_avatar"] != "https://cdn.example/sender.png" {
|
||||
t.Fatalf("region payload must include sender display snapshot: %+v", regionPayload)
|
||||
}
|
||||
sender, _ := regionPayload["sender"].(map[string]any)
|
||||
if sender["nickname"] != "Real Sender" || sender["display_user_id"] != "160042" {
|
||||
t.Fatalf("nested sender profile mismatch: %+v", sender)
|
||||
}
|
||||
if _, exists := regionPayload["gift_name"]; exists {
|
||||
t.Fatalf("lucky gift region broadcast should not send a gift-name template field: %+v", regionPayload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPendingBatchDrawOutboxCreditsWalletOnceAndGrantsAllDraws(t *testing.T) {
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"event_type": "lucky_gift_drawn",
|
||||
"event_id": "lucky_gift_drawn:lucky_draw_batch_1",
|
||||
"app_code": "lalu",
|
||||
"draw_id": "lucky_draw_batch_1",
|
||||
"draw_ids": []string{"lucky_draw_batch_1", "lucky_draw_batch_2", "lucky_draw_batch_3"},
|
||||
"command_id": "cmd-gift-batch",
|
||||
"pool_id": "lucky_100",
|
||||
"user_id": 42,
|
||||
"sender_user_id": 42,
|
||||
"target_user_id": 99,
|
||||
"country_id": 15,
|
||||
"room_id": "room-1",
|
||||
"gift_id": "rose",
|
||||
"gift_count": 3,
|
||||
"visible_region_id": 210,
|
||||
"coin_spent": 300,
|
||||
"effective_reward_coins": 3600,
|
||||
"multiplier_ppm": 12000000,
|
||||
"created_at_ms": 1760000000000,
|
||||
})
|
||||
repository := &fakeLuckyGiftRepository{
|
||||
outbox: []domain.DrawOutbox{{
|
||||
AppCode: "lalu",
|
||||
OutboxID: "lucky_lucky_draw_batch_1",
|
||||
EventType: "LuckyGiftDrawn",
|
||||
PayloadJSON: string(payload),
|
||||
}},
|
||||
}
|
||||
wallet := &fakeLuckyGiftWallet{}
|
||||
publisher := &fakeLuckyGiftPublisher{}
|
||||
userPublisher := &fakeLuckyGiftUserPublisher{}
|
||||
broadcaster := &fakeLuckyGiftRegionBroadcaster{}
|
||||
service := New(repository,
|
||||
WithWallet(wallet),
|
||||
WithRoomPublisher(publisher),
|
||||
WithUserPublisher(userPublisher),
|
||||
WithRegionBroadcaster(broadcaster),
|
||||
WithSenderProfileSource(fakeLuckyGiftSenderProfileSource{profiles: map[int64]broadcastservice.SenderProfile{
|
||||
42: {UserID: 42, Account: "160042", Nickname: "Batch Sender"},
|
||||
}}),
|
||||
)
|
||||
service.SetClock(func() time.Time { return time.UnixMilli(1760000001000).UTC() })
|
||||
|
||||
processed, err := service.ProcessPendingDrawOutbox(context.Background(), WorkerOptions{WorkerID: "worker-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessPendingDrawOutbox failed: %v", err)
|
||||
}
|
||||
if processed != 1 {
|
||||
t.Fatalf("processed count mismatch: %d", processed)
|
||||
}
|
||||
if wallet.last == nil || wallet.last.GetAmount() != 3600 || wallet.last.GetDrawId() != "lucky_draw_batch_1" || wallet.last.GetCommandId() != "lucky_reward:lucky_draw_batch_1" || wallet.last.GetCountryId() != 15 || wallet.last.GetVisibleRegionId() != 210 {
|
||||
t.Fatalf("wallet should receive one aggregate reward request: %+v", wallet.last)
|
||||
}
|
||||
if got := strings.Join(repository.grantedDrawIDs, ","); got != "lucky_draw_batch_1,lucky_draw_batch_2,lucky_draw_batch_3" {
|
||||
t.Fatalf("all sub draws should be granted together, got %s", got)
|
||||
}
|
||||
assertLuckyGiftWalletNotice(t, userPublisher.last, "42", "wallet_tx_lucky", 3600, 0)
|
||||
assertLuckyGiftRoomMessage(t, publisher.last, "room-1", "lucky_gift_drawn:lucky_draw_batch_1", "lucky_draw_batch_1", 3600)
|
||||
if broadcaster.last.EventID != "lucky_gift_big_win:lucky_draw_batch_1" || broadcaster.last.RegionID != 210 {
|
||||
t.Fatalf("batch big win region broadcast mismatch: %+v", broadcaster.last)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessRewardSettlementOutboxPublishesRoomDisplayAfterWalletSettlement(t *testing.T) {
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"event_type": "lucky_gift_drawn",
|
||||
"event_id": "lucky_gift_drawn:lucky_draw_settle",
|
||||
"app_code": "lalu",
|
||||
"draw_id": "lucky_draw_settle",
|
||||
"command_id": "cmd-gift-settle",
|
||||
"pool_id": "super_lucky",
|
||||
"user_id": 42,
|
||||
"visible_region_id": 210,
|
||||
"room_id": "room-1",
|
||||
"gift_id": "rose",
|
||||
"gift_count": 1,
|
||||
"coin_spent": 100,
|
||||
"effective_reward_coins": 1200,
|
||||
"multiplier_ppm": 12000000,
|
||||
"created_at_ms": 1760000000000,
|
||||
})
|
||||
repository := &fakeLuckyGiftRepository{
|
||||
outbox: []domain.DrawOutbox{{
|
||||
AppCode: "lalu",
|
||||
OutboxID: "lucky_reward_lucky_draw_settle",
|
||||
EventType: domain.EventTypeLuckyGiftRewardSettlement,
|
||||
PayloadJSON: string(payload),
|
||||
}},
|
||||
}
|
||||
wallet := &fakeLuckyGiftWallet{}
|
||||
publisher := &fakeLuckyGiftPublisher{}
|
||||
userPublisher := &fakeLuckyGiftUserPublisher{}
|
||||
broadcaster := &fakeLuckyGiftRegionBroadcaster{}
|
||||
service := New(repository, WithWallet(wallet), WithRoomPublisher(publisher), WithUserPublisher(userPublisher), WithRegionBroadcaster(broadcaster))
|
||||
|
||||
processed, err := service.ProcessPendingDrawOutbox(context.Background(), WorkerOptions{WorkerID: "worker-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessPendingDrawOutbox failed: %v", err)
|
||||
}
|
||||
if processed != 1 || repository.deliveredOutboxID != "lucky_reward_lucky_draw_settle" {
|
||||
t.Fatalf("settlement outbox should be delivered: processed=%d delivered=%s", processed, repository.deliveredOutboxID)
|
||||
}
|
||||
if wallet.last == nil || repository.grantedDrawID != "lucky_draw_settle" {
|
||||
t.Fatalf("settlement should credit wallet and mark draw granted: wallet=%+v granted=%s", wallet.last, repository.grantedDrawID)
|
||||
}
|
||||
assertLuckyGiftRoomMessage(t, publisher.last, "room-1", "lucky_gift_drawn:lucky_draw_settle", "lucky_draw_settle", 1200)
|
||||
if broadcaster.last.EventID != "" {
|
||||
t.Fatalf("settlement without sender profile must not enter region broadcast: %+v", broadcaster.last)
|
||||
}
|
||||
assertLuckyGiftWalletNotice(t, userPublisher.last, "42", "wallet_tx_lucky", 1200, 0)
|
||||
}
|
||||
|
||||
func TestPublishLuckyGiftDrawnOmitsBatchDrawIDsFromRoomIM(t *testing.T) {
|
||||
drawIDs := make([]string, 0, 999)
|
||||
for index := 0; index < 999; index++ {
|
||||
drawIDs = append(drawIDs, fmt.Sprintf("lucky_draw_1780217057721_%016d", index))
|
||||
}
|
||||
publisher := &fakeLuckyGiftPublisher{}
|
||||
service := New(&fakeLuckyGiftRepository{}, WithRoomPublisher(publisher))
|
||||
payload := luckyGiftDrawnPayload{
|
||||
EventType: "lucky_gift_drawn",
|
||||
EventID: "lucky_gift_drawn:lucky_draw_1780217057721_0000000000000000",
|
||||
AppCode: "lalu",
|
||||
DrawID: drawIDs[0],
|
||||
DrawIDs: drawIDs,
|
||||
CommandID: "cmd-gift-batch",
|
||||
PoolID: "lucky",
|
||||
RoomID: "lalu_9aca4db6-35f0-4490-9cbc-1a452b7fa67d",
|
||||
GiftID: "31",
|
||||
GiftCount: 999,
|
||||
UserID: 42,
|
||||
SenderUserID: 42,
|
||||
TargetUserID: 99,
|
||||
VisibleRegionID: 210,
|
||||
CoinSpent: 99900,
|
||||
RuleVersion: 2,
|
||||
ExperiencePool: "advanced",
|
||||
SelectedTierID: "batch",
|
||||
MultiplierPPM: 12000000,
|
||||
BaseRewardCoins: 73983,
|
||||
RoomAtmosphereRewardCoins: 0,
|
||||
ActivitySubsidyCoins: 0,
|
||||
EffectiveRewardCoins: 73983,
|
||||
CreatedAtMS: 1780217057721,
|
||||
}
|
||||
|
||||
if err := service.publishLuckyGiftDrawn(context.Background(), payload, WorkerOptions{PublishTimeout: time.Second}); err != nil {
|
||||
t.Fatalf("publishLuckyGiftDrawn failed: %v", err)
|
||||
}
|
||||
if len(publisher.last.PayloadJSON) >= 12*1024 {
|
||||
t.Fatalf("room IM payload should stay below Tencent 12KB limit, got %d bytes", len(publisher.last.PayloadJSON))
|
||||
}
|
||||
var body map[string]any
|
||||
if err := json.Unmarshal(publisher.last.PayloadJSON, &body); err != nil {
|
||||
t.Fatalf("decode room IM payload failed: %v", err)
|
||||
}
|
||||
if _, exists := body["draw_ids"]; exists {
|
||||
t.Fatalf("room IM payload must not include internal draw_ids")
|
||||
}
|
||||
if body["draw_id"] != drawIDs[0] || body["event_id"] != payload.EventID || body["gift_count"].(float64) != 999 {
|
||||
t.Fatalf("room IM payload lost display fields: %+v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPendingDrawOutboxIgnoresRoomIMPublisherFailure(t *testing.T) {
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"event_type": "lucky_gift_drawn",
|
||||
"event_id": "lucky_gift_drawn:lucky_draw_im_fail",
|
||||
"app_code": "lalu",
|
||||
"draw_id": "lucky_draw_im_fail",
|
||||
"command_id": "cmd-gift-im-fail",
|
||||
"pool_id": "super_lucky",
|
||||
"user_id": 42,
|
||||
"room_id": "room-1",
|
||||
"gift_id": "rose",
|
||||
"gift_count": 1,
|
||||
"coin_spent": 100,
|
||||
"effective_reward_coins": 1200,
|
||||
"multiplier_ppm": 12000000,
|
||||
"created_at_ms": 1760000000000,
|
||||
})
|
||||
repository := &fakeLuckyGiftRepository{
|
||||
outbox: []domain.DrawOutbox{{
|
||||
AppCode: "lalu",
|
||||
OutboxID: "lucky_lucky_draw_im_fail",
|
||||
EventType: "LuckyGiftDrawn",
|
||||
PayloadJSON: string(payload),
|
||||
}},
|
||||
}
|
||||
wallet := &fakeLuckyGiftWallet{}
|
||||
publisher := &fakeLuckyGiftPublisher{err: errors.New("im unavailable")}
|
||||
userPublisher := &fakeLuckyGiftUserPublisher{}
|
||||
service := New(repository, WithWallet(wallet), WithRoomPublisher(publisher), WithUserPublisher(userPublisher), WithRegionBroadcaster(&fakeLuckyGiftRegionBroadcaster{}))
|
||||
service.SetClock(func() time.Time { return time.UnixMilli(1760000001000).UTC() })
|
||||
|
||||
processed, err := service.ProcessPendingDrawOutbox(context.Background(), WorkerOptions{WorkerID: "worker-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessPendingDrawOutbox failed: %v", err)
|
||||
}
|
||||
if processed != 1 {
|
||||
t.Fatalf("processed count mismatch: %d", processed)
|
||||
}
|
||||
if repository.grantedDrawID != "lucky_draw_im_fail" {
|
||||
t.Fatalf("draw should be settled through wallet, got %q", repository.grantedDrawID)
|
||||
}
|
||||
if repository.deliveredOutboxID != "lucky_lucky_draw_im_fail" || repository.retryableOutboxID != "" {
|
||||
t.Fatalf("IM publisher must not affect settlement outbox: delivered=%s retryable=%s", repository.deliveredOutboxID, repository.retryableOutboxID)
|
||||
}
|
||||
if publisher.last.EventID != "lucky_gift_drawn:lucky_draw_im_fail" {
|
||||
t.Fatalf("room IM should still be attempted before ignoring failure: %+v", publisher.last)
|
||||
}
|
||||
assertLuckyGiftWalletNotice(t, userPublisher.last, "42", "wallet_tx_lucky", 1200, 0)
|
||||
}
|
||||
|
||||
func TestProcessPendingDrawOutboxSkipsWalletWhenDrawAlreadyGranted(t *testing.T) {
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"event_type": "lucky_gift_drawn",
|
||||
"event_id": "lucky_gift_drawn:lucky_draw_granted",
|
||||
"app_code": "lalu",
|
||||
"draw_id": "lucky_draw_granted",
|
||||
"command_id": "cmd-gift-granted",
|
||||
"pool_id": "super_lucky",
|
||||
"user_id": 42,
|
||||
"room_id": "room-1",
|
||||
"gift_id": "rose",
|
||||
"gift_count": 1,
|
||||
"coin_spent": 100,
|
||||
"effective_reward_coins": 1200,
|
||||
"multiplier_ppm": 12_000_000,
|
||||
"created_at_ms": 1760000000000,
|
||||
})
|
||||
repository := &fakeLuckyGiftRepository{
|
||||
rewardState: domain.DrawRewardState{AllGranted: true, WalletTransactionID: "wallet_tx_existing"},
|
||||
outbox: []domain.DrawOutbox{{
|
||||
AppCode: "lalu",
|
||||
OutboxID: "lucky_lucky_draw_granted",
|
||||
EventType: "LuckyGiftDrawn",
|
||||
PayloadJSON: string(payload),
|
||||
}},
|
||||
}
|
||||
wallet := &fakeLuckyGiftWallet{}
|
||||
publisher := &fakeLuckyGiftPublisher{}
|
||||
userPublisher := &fakeLuckyGiftUserPublisher{}
|
||||
service := New(repository, WithWallet(wallet), WithRoomPublisher(publisher), WithUserPublisher(userPublisher), WithRegionBroadcaster(&fakeLuckyGiftRegionBroadcaster{}))
|
||||
|
||||
processed, err := service.ProcessPendingDrawOutbox(context.Background(), WorkerOptions{WorkerID: "worker-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessPendingDrawOutbox failed: %v", err)
|
||||
}
|
||||
if processed != 1 {
|
||||
t.Fatalf("processed count mismatch: %d", processed)
|
||||
}
|
||||
if wallet.last != nil {
|
||||
t.Fatalf("already granted draw must not call wallet again: %+v", wallet.last)
|
||||
}
|
||||
if repository.grantedDrawID != "" {
|
||||
t.Fatalf("already granted draw should not be marked again, got %q", repository.grantedDrawID)
|
||||
}
|
||||
if userPublisher.last.EventID != "" || repository.deliveredOutboxID != "lucky_lucky_draw_granted" {
|
||||
t.Fatalf("already granted draw should not resend wallet notice and must deliver outbox: user_publisher=%+v delivered=%s", userPublisher.last, repository.deliveredOutboxID)
|
||||
}
|
||||
assertLuckyGiftRoomMessage(t, publisher.last, "room-1", "lucky_gift_drawn:lucky_draw_granted", "lucky_draw_granted", 1200)
|
||||
}
|
||||
|
||||
type fakeLuckyGiftRepository struct {
|
||||
getConfigCalls int
|
||||
upserted domain.RuleConfig
|
||||
executeResult domain.DrawResult
|
||||
executeCmd domain.DrawCommand
|
||||
executeBatchResults []domain.DrawResult
|
||||
executeBatchCmds []domain.DrawCommand
|
||||
rewardState domain.DrawRewardState
|
||||
outbox []domain.DrawOutbox
|
||||
|
||||
grantedDrawID string
|
||||
grantedDrawIDs []string
|
||||
deliveredOutboxID string
|
||||
retryableOutboxID string
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) GetLuckyGiftRuleConfig(context.Context, string) (domain.RuleConfig, bool, error) {
|
||||
r.getConfigCalls++
|
||||
return domain.RuleConfig{}, false, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) PublishLuckyGiftRuleConfig(_ context.Context, config domain.RuleConfig, _ int64) (domain.RuleConfig, error) {
|
||||
r.upserted = config
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) ListLuckyGiftRuleConfigs(context.Context) ([]domain.RuleConfig, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) CheckLuckyGift(context.Context, domain.CheckCommand) (domain.CheckResult, error) {
|
||||
return domain.CheckResult{}, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) ExecuteLuckyGiftDraw(_ context.Context, cmd domain.DrawCommand, _ int64) (domain.DrawResult, error) {
|
||||
r.executeCmd = cmd
|
||||
return r.executeResult, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) ExecuteLuckyGiftDrawBatch(_ context.Context, cmds []domain.DrawCommand, _ int64) ([]domain.DrawResult, error) {
|
||||
r.executeBatchCmds = append([]domain.DrawCommand(nil), cmds...)
|
||||
if len(r.executeBatchResults) > 0 {
|
||||
return append([]domain.DrawResult(nil), r.executeBatchResults...), nil
|
||||
}
|
||||
results := make([]domain.DrawResult, 0, len(cmds))
|
||||
for _, cmd := range cmds {
|
||||
result := r.executeResult
|
||||
result.CommandID = cmd.CommandID
|
||||
results = append(results, result)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) ListLuckyGiftDraws(context.Context, domain.DrawQuery) ([]domain.DrawResult, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) GetLuckyGiftDrawSummary(context.Context, domain.DrawQuery) (domain.DrawSummary, error) {
|
||||
return domain.DrawSummary{}, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) GetLuckyGiftDrawRewardState(context.Context, string, []string) (domain.DrawRewardState, error) {
|
||||
return r.rewardState, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) ClaimPendingLuckyGiftOutbox(context.Context, string, int64, time.Duration, int) ([]domain.DrawOutbox, error) {
|
||||
return r.outbox, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) MarkLuckyGiftOutboxDelivered(_ context.Context, event domain.DrawOutbox, _ int64) error {
|
||||
r.deliveredOutboxID = event.OutboxID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) MarkLuckyGiftOutboxRetryable(_ context.Context, event domain.DrawOutbox, _ int, _ int64, _ string, _ int64) error {
|
||||
r.retryableOutboxID = event.OutboxID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) MarkLuckyGiftOutboxFailed(context.Context, domain.DrawOutbox, int, string, int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) RefreshLuckyGiftAdminStatsSnapshots(context.Context, int64, int) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) RefreshLuckyGiftDatabiStatsSnapshots(context.Context, int64, int) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) MarkLuckyGiftDrawGranted(_ context.Context, _ string, drawID string, _ string, _ int64) error {
|
||||
r.grantedDrawID = drawID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) MarkLuckyGiftDrawFailed(context.Context, string, string, string, int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) MarkLuckyGiftDrawsGranted(_ context.Context, _ string, drawIDs []string, _ string, _ int64) error {
|
||||
r.grantedDrawIDs = append([]string(nil), drawIDs...)
|
||||
if len(drawIDs) > 0 {
|
||||
r.grantedDrawID = drawIDs[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) MarkLuckyGiftDrawsFailed(context.Context, string, []string, string, int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeLuckyGiftWallet struct {
|
||||
last *walletv1.CreditLuckyGiftRewardRequest
|
||||
err error
|
||||
balanceAfter int64
|
||||
}
|
||||
|
||||
func (w *fakeLuckyGiftWallet) CreditLuckyGiftReward(_ context.Context, req *walletv1.CreditLuckyGiftRewardRequest, _ ...grpc.CallOption) (*walletv1.CreditLuckyGiftRewardResponse, error) {
|
||||
w.last = req
|
||||
if w.err != nil {
|
||||
return nil, w.err
|
||||
}
|
||||
return &walletv1.CreditLuckyGiftRewardResponse{
|
||||
TransactionId: "wallet_tx_lucky",
|
||||
Amount: req.GetAmount(),
|
||||
Balance: &walletv1.AssetBalance{AvailableAmount: w.balanceAfter},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type fakeLuckyGiftPublisher struct {
|
||||
last tencentim.CustomGroupMessage
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *fakeLuckyGiftPublisher) PublishGroupCustomMessage(_ context.Context, message tencentim.CustomGroupMessage) error {
|
||||
p.last = message
|
||||
return p.err
|
||||
}
|
||||
|
||||
type fakeLuckyGiftUserPublisher struct {
|
||||
last tencentim.CustomUserMessage
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *fakeLuckyGiftUserPublisher) PublishUserCustomMessage(_ context.Context, message tencentim.CustomUserMessage) error {
|
||||
p.last = message
|
||||
return p.err
|
||||
}
|
||||
|
||||
type fakeLuckyGiftRegionBroadcaster struct {
|
||||
last broadcastservice.PublishInput
|
||||
}
|
||||
|
||||
func (b *fakeLuckyGiftRegionBroadcaster) PublishRegionBroadcast(_ context.Context, input broadcastservice.PublishInput) (broadcastdomain.PublishResult, error) {
|
||||
b.last = input
|
||||
return broadcastdomain.PublishResult{EventID: input.EventID, Status: broadcastdomain.StatusPending, Created: true}, nil
|
||||
}
|
||||
|
||||
type fakeLuckyGiftSenderProfileSource struct {
|
||||
profiles map[int64]broadcastservice.SenderProfile
|
||||
err error
|
||||
}
|
||||
|
||||
func (s fakeLuckyGiftSenderProfileSource) GetSenderProfile(_ context.Context, userID int64) (broadcastservice.SenderProfile, error) {
|
||||
if s.err != nil {
|
||||
return broadcastservice.SenderProfile{}, s.err
|
||||
}
|
||||
if profile, ok := s.profiles[userID]; ok {
|
||||
return profile, nil
|
||||
}
|
||||
return broadcastservice.SenderProfile{UserID: userID}, nil
|
||||
}
|
||||
|
||||
func assertLuckyGiftWalletNotice(t *testing.T, message tencentim.CustomUserMessage, toAccount string, transactionID string, availableDelta int64, availableAfter int64) {
|
||||
t.Helper()
|
||||
if message.ToAccount != toAccount || message.Desc != "WalletBalanceChanged" || message.Ext != "wallet_notice" {
|
||||
t.Fatalf("wallet notice envelope mismatch: %+v", message)
|
||||
}
|
||||
if message.EventID != walletBalanceChangedEventID(transactionID, 42, "COIN") {
|
||||
t.Fatalf("wallet notice event id mismatch: %s", message.EventID)
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(message.PayloadJSON, &payload); err != nil {
|
||||
t.Fatalf("wallet notice payload is invalid: %v", err)
|
||||
}
|
||||
if payload["event_type"] != "WalletBalanceChanged" || payload["transaction_id"] != transactionID || payload["user_id"] != "42" || payload["asset_type"] != "COIN" {
|
||||
t.Fatalf("wallet notice identity fields mismatch: %+v", payload)
|
||||
}
|
||||
if int64(payload["available_delta"].(float64)) != availableDelta || int64(payload["available_after"].(float64)) != availableAfter {
|
||||
t.Fatalf("wallet notice balance fields mismatch: %+v", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func assertLuckyGiftRoomMessage(t *testing.T, message tencentim.CustomGroupMessage, groupID string, eventID string, drawID string, rewardCoins int64) {
|
||||
t.Helper()
|
||||
if message.GroupID != groupID || message.EventID != eventID || message.Desc != "lucky_gift_drawn" || message.Ext != "room_system_message" {
|
||||
t.Fatalf("room lucky gift envelope mismatch: %+v", message)
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(message.PayloadJSON, &payload); err != nil {
|
||||
t.Fatalf("room lucky gift payload is invalid: %v", err)
|
||||
}
|
||||
if payload["event_type"] != "lucky_gift_drawn" || payload["event_id"] != eventID || payload["draw_id"] != drawID {
|
||||
t.Fatalf("room lucky gift identity fields mismatch: %+v", payload)
|
||||
}
|
||||
if int64(payload["effective_reward_coins"].(float64)) != rewardCoins {
|
||||
t.Fatalf("room lucky gift reward mismatch: %+v", payload)
|
||||
}
|
||||
}
|
||||
@ -16,10 +16,6 @@ import (
|
||||
taskdomain "hyapp/services/activity-service/internal/domain/task"
|
||||
)
|
||||
|
||||
const (
|
||||
taskRewardAsset = "COIN"
|
||||
)
|
||||
|
||||
// Repository 是任务系统的唯一持久化边界;进度、领奖和事件幂等都必须落 activity MySQL。
|
||||
type Repository interface {
|
||||
ListVisibleDefinitions(ctx context.Context, nowMS int64) ([]taskdomain.Definition, error)
|
||||
@ -33,6 +29,11 @@ type Repository interface {
|
||||
MarkTaskClaimFailed(ctx context.Context, claimID string, failureReason string, nowMS int64) error
|
||||
}
|
||||
|
||||
type taskRewardPolicyStore interface {
|
||||
GetActiveTaskRewardAssetType(ctx context.Context, nowMS int64) (string, bool, error)
|
||||
PublishTaskRewardPolicy(ctx context.Context, command taskdomain.RewardPolicyCommand, nowMS int64) error
|
||||
}
|
||||
|
||||
// WalletClient 是 activity-service 发放金币奖励时依赖的钱包入账接口。
|
||||
type WalletClient interface {
|
||||
CreditTaskReward(ctx context.Context, req *walletv1.CreditTaskRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditTaskRewardResponse, error)
|
||||
@ -103,7 +104,7 @@ func (s *Service) ListUserTasks(ctx context.Context, userID int64) (taskdomain.L
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ClaimTaskReward 校验 activity 侧完成状态后,使用 wallet-service 幂等命令发放 COIN。
|
||||
// ClaimTaskReward 校验 activity 侧完成状态后,使用 wallet-service 幂等命令发放任务奖励资产。
|
||||
func (s *Service) ClaimTaskReward(ctx context.Context, command taskdomain.ClaimCommand) (taskdomain.Claim, error) {
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return taskdomain.Claim{}, err
|
||||
@ -148,14 +149,15 @@ func (s *Service) ClaimTaskReward(ctx context.Context, command taskdomain.ClaimC
|
||||
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,
|
||||
CommandId: claim.WalletCommandID,
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
TargetUserId: claim.UserID,
|
||||
Amount: claim.RewardCoinAmount,
|
||||
RewardAssetType: claim.RewardAssetType,
|
||||
TaskType: claim.TaskType,
|
||||
TaskId: claim.TaskID,
|
||||
CycleKey: claim.CycleKey,
|
||||
Reason: reason,
|
||||
})
|
||||
if err != nil {
|
||||
// 钱包失败时不能把任务进度改成 claimed;同一 command_id 重试会复用 claim 和 wallet_command_id。
|
||||
@ -311,13 +313,54 @@ func (s *Service) UpsertTaskDefinition(ctx context.Context, command taskdomain.D
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return taskdomain.Definition{}, false, err
|
||||
}
|
||||
if strings.TrimSpace(command.RewardAssetType) == "" {
|
||||
if policyStore, ok := s.repository.(taskRewardPolicyStore); ok {
|
||||
// policy instance 只提供“未显式配置时”的默认奖励资产;任务定义一旦保存仍会按定义和进度快照发放。
|
||||
if assetType, found, err := policyStore.GetActiveTaskRewardAssetType(ctx, s.now().UnixMilli()); err != nil {
|
||||
return taskdomain.Definition{}, false, err
|
||||
} else if found {
|
||||
command.RewardAssetType = assetType
|
||||
}
|
||||
}
|
||||
}
|
||||
command = normalizeDefinitionCommand(command)
|
||||
if err := validateDefinitionCommand(command); err != nil {
|
||||
if err := validateDefinitionCommand(ctx, command); err != nil {
|
||||
return taskdomain.Definition{}, false, err
|
||||
}
|
||||
return s.repository.UpsertTaskDefinition(ctx, command, s.now().UnixMilli())
|
||||
}
|
||||
|
||||
// PublishTaskRewardPolicy 写入 activity 运行侧默认任务奖励资产;它不改历史任务,只影响后续未显式传 reward_asset_type 的配置。
|
||||
func (s *Service) PublishTaskRewardPolicy(ctx context.Context, command taskdomain.RewardPolicyCommand) error {
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return err
|
||||
}
|
||||
policyStore, ok := s.repository.(taskRewardPolicyStore)
|
||||
if !ok {
|
||||
return xerr.New(xerr.Unavailable, "task reward policy store is not configured")
|
||||
}
|
||||
command.InstanceCode = strings.TrimSpace(command.InstanceCode)
|
||||
command.TemplateCode = strings.TrimSpace(command.TemplateCode)
|
||||
command.TemplateVersion = strings.TrimSpace(command.TemplateVersion)
|
||||
command.Status = strings.TrimSpace(command.Status)
|
||||
command.RewardAssetType = normalizeRewardAssetType(command.RewardAssetType)
|
||||
command.RuleJSON = normalizeJSONForCommand(command.RuleJSON)
|
||||
if command.InstanceCode == "" || command.TemplateCode == "" || command.TemplateVersion == "" || command.OperatorAdminID <= 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "task reward policy command is incomplete")
|
||||
}
|
||||
if command.Status != "active" && command.Status != "disabled" {
|
||||
return xerr.New(xerr.InvalidArgument, "task reward policy status is invalid")
|
||||
}
|
||||
if !validRewardAssetType(appcode.FromContext(ctx), command.RewardAssetType) {
|
||||
return xerr.New(xerr.InvalidArgument, "reward_asset_type is invalid")
|
||||
}
|
||||
nowMS := s.now().UnixMilli()
|
||||
if command.PublishedAtMS <= 0 {
|
||||
command.PublishedAtMS = nowMS
|
||||
}
|
||||
return policyStore.PublishTaskRewardPolicy(ctx, command, nowMS)
|
||||
}
|
||||
|
||||
// SetTaskDefinitionStatus 只做状态流转,不删除历史进度和领奖事实。
|
||||
func (s *Service) SetTaskDefinitionStatus(ctx context.Context, taskID string, status string, operatorAdminID int64) (taskdomain.Definition, error) {
|
||||
if err := s.requireRepository(); err != nil {
|
||||
@ -344,10 +387,14 @@ func (s *Service) itemFromDefinition(def taskdomain.Definition, progress taskdom
|
||||
if progress.TaskID != "" {
|
||||
status = progress.Status
|
||||
progressValue = progress.ProgressValue
|
||||
def.TargetValue = progress.TargetValue
|
||||
def.RewardCoinAmount = progress.RewardCoinAmount
|
||||
def.RewardAssetType = normalizeRewardAssetType(progress.RewardAssetType)
|
||||
}
|
||||
if status == "" {
|
||||
status = taskdomain.StatusInProgress
|
||||
}
|
||||
def.RewardAssetType = normalizeRewardAssetType(def.RewardAssetType)
|
||||
return taskdomain.Item{
|
||||
Definition: def,
|
||||
ProgressValue: progressValue,
|
||||
@ -420,6 +467,7 @@ func normalizeDefinitionCommand(command taskdomain.DefinitionCommand) taskdomain
|
||||
command.ActionPayloadJSON = normalizeJSONForCommand(command.ActionPayloadJSON)
|
||||
command.DimensionFilterJSON = normalizeJSONForCommand(command.DimensionFilterJSON)
|
||||
command.TargetUnit = strings.ToLower(strings.TrimSpace(command.TargetUnit))
|
||||
command.RewardAssetType = normalizeRewardAssetType(command.RewardAssetType)
|
||||
command.Status = strings.TrimSpace(command.Status)
|
||||
if command.Status == "" {
|
||||
command.Status = taskdomain.StatusDraft
|
||||
@ -438,7 +486,7 @@ func normalizeTaskType(value string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func validateDefinitionCommand(command taskdomain.DefinitionCommand) error {
|
||||
func validateDefinitionCommand(ctx context.Context, command taskdomain.DefinitionCommand) error {
|
||||
// daily 按 UTC 日周期生成进度,exclusive 使用 lifetime 周期;新增“新人专属+每日”用 audience_type 表达,不新增 task_type。
|
||||
if command.TaskType != taskdomain.TypeDaily && command.TaskType != taskdomain.TypeExclusive {
|
||||
return xerr.New(xerr.InvalidArgument, "task_type is invalid")
|
||||
@ -463,6 +511,9 @@ func validateDefinitionCommand(command taskdomain.DefinitionCommand) error {
|
||||
if command.TargetValue <= 0 || command.RewardCoinAmount <= 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "target_value and reward_coin_amount must be positive")
|
||||
}
|
||||
if !validRewardAssetType(appcode.FromContext(ctx), command.RewardAssetType) {
|
||||
return xerr.New(xerr.InvalidArgument, "reward_asset_type is invalid")
|
||||
}
|
||||
if !validTargetUnit(command.TargetUnit) {
|
||||
return xerr.New(xerr.InvalidArgument, "target_unit is invalid")
|
||||
}
|
||||
@ -478,6 +529,26 @@ func validateDefinitionCommand(command taskdomain.DefinitionCommand) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeRewardAssetType(value string) string {
|
||||
value = strings.ToUpper(strings.TrimSpace(value))
|
||||
if value == "" {
|
||||
return taskdomain.RewardAssetCoin
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func validRewardAssetType(appCode string, value string) bool {
|
||||
switch normalizeRewardAssetType(value) {
|
||||
case taskdomain.RewardAssetCoin:
|
||||
return true
|
||||
case taskdomain.RewardAssetPoint:
|
||||
// POINT 是 Huwaa 第一套政策的真实收益资产;其它 App 继续只能发 COIN,避免配置误伤旧任务。
|
||||
return appcode.Normalize(appCode) == "huwaa"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func roomGiftTaskDimensions(envelope *roomeventsv1.EventEnvelope, gift *roomeventsv1.RoomGiftSent) (string, error) {
|
||||
// 维度只记录任务匹配和排障需要的事实字段;后台 dimension_filter_json 可以按 gift_id、pool_id、room_id 等字段精确限定任务。
|
||||
payload := map[string]any{
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"google.golang.org/grpc"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
taskdomain "hyapp/services/activity-service/internal/domain/task"
|
||||
taskservice "hyapp/services/activity-service/internal/service/task"
|
||||
"hyapp/services/activity-service/internal/testutil/mysqltest"
|
||||
@ -166,16 +167,220 @@ func TestTaskEventDimensionFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type fakeWalletClient struct {
|
||||
calls int
|
||||
func TestHuwaaTaskRewardPolicyDefaultsNewTasksToPointAndClaimsPoint(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
wallet := &fakeWalletClient{}
|
||||
svc := taskservice.New(repository, wallet)
|
||||
now := time.Date(2026, 5, 9, 10, 0, 0, 0, time.UTC)
|
||||
svc.SetClock(func() time.Time { return now })
|
||||
ctx := appcode.WithContext(context.Background(), "huwaa")
|
||||
|
||||
if err := svc.PublishTaskRewardPolicy(ctx, taskdomain.RewardPolicyCommand{
|
||||
InstanceCode: "huwaa-point-live",
|
||||
TemplateCode: "huwaa_point_policy",
|
||||
TemplateVersion: "v2",
|
||||
Status: "active",
|
||||
RewardAssetType: taskdomain.RewardAssetPoint,
|
||||
RuleJSON: `{"tasks":{"reward_asset_type":"POINT"}}`,
|
||||
OperatorAdminID: 90001,
|
||||
PublishedAtMS: now.UnixMilli(),
|
||||
}); err != nil {
|
||||
t.Fatalf("PublishTaskRewardPolicy failed: %v", err)
|
||||
}
|
||||
definition, created, err := svc.UpsertTaskDefinition(ctx, taskdomain.DefinitionCommand{
|
||||
TaskType: taskdomain.TypeDaily,
|
||||
Category: "gift",
|
||||
MetricType: taskdomain.MetricGiftSpendCoin,
|
||||
Title: "send gift for point",
|
||||
TargetValue: 100,
|
||||
TargetUnit: "coin",
|
||||
RewardCoinAmount: 12,
|
||||
Status: taskdomain.StatusActive,
|
||||
OperatorAdminID: 90001,
|
||||
})
|
||||
if err != nil || !created {
|
||||
t.Fatalf("create Huwaa task definition failed: created=%v err=%v", created, err)
|
||||
}
|
||||
if definition.RewardAssetType != taskdomain.RewardAssetPoint {
|
||||
t.Fatalf("policy default reward asset mismatch: %+v", definition)
|
||||
}
|
||||
|
||||
if _, err := svc.ConsumeTaskEvent(ctx, taskdomain.Event{
|
||||
EventID: "evt-huwaa-point-task",
|
||||
EventType: "RoomGiftSent",
|
||||
SourceService: "room-service",
|
||||
UserID: 41001,
|
||||
MetricType: taskdomain.MetricGiftSpendCoin,
|
||||
Value: 100,
|
||||
OccurredAtMS: now.UnixMilli(),
|
||||
}); err != nil {
|
||||
t.Fatalf("ConsumeTaskEvent failed: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimTaskReward(ctx, taskdomain.ClaimCommand{
|
||||
UserID: 41001,
|
||||
TaskID: definition.TaskID,
|
||||
TaskType: taskdomain.TypeDaily,
|
||||
CycleKey: "2026-05-09",
|
||||
CommandID: "cmd-huwaa-point-task",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimTaskReward failed: %v", err)
|
||||
}
|
||||
if claim.RewardAssetType != taskdomain.RewardAssetPoint {
|
||||
t.Fatalf("claim reward asset mismatch: %+v", claim)
|
||||
}
|
||||
if wallet.last == nil ||
|
||||
wallet.last.GetAppCode() != "huwaa" ||
|
||||
wallet.last.GetRewardAssetType() != taskdomain.RewardAssetPoint ||
|
||||
wallet.last.GetAmount() != 12 {
|
||||
t.Fatalf("wallet POINT reward request mismatch: %+v", wallet.last)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeWalletClient) CreditTaskReward(context.Context, *walletv1.CreditTaskRewardRequest, ...grpc.CallOption) (*walletv1.CreditTaskRewardResponse, error) {
|
||||
func TestPointTaskRewardPolicyIsRejectedOutsideHuwaa(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
svc := taskservice.New(repository, &fakeWalletClient{})
|
||||
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||
|
||||
if err := svc.PublishTaskRewardPolicy(ctx, taskdomain.RewardPolicyCommand{
|
||||
InstanceCode: "lalu-point-denied",
|
||||
TemplateCode: "huwaa_point_policy",
|
||||
TemplateVersion: "v2",
|
||||
Status: "active",
|
||||
RewardAssetType: taskdomain.RewardAssetPoint,
|
||||
RuleJSON: `{}`,
|
||||
OperatorAdminID: 90001,
|
||||
}); err == nil {
|
||||
t.Fatal("Lalu POINT policy publish must be rejected")
|
||||
}
|
||||
if _, _, err := svc.UpsertTaskDefinition(ctx, taskdomain.DefinitionCommand{
|
||||
TaskType: taskdomain.TypeDaily,
|
||||
Category: "gift",
|
||||
MetricType: taskdomain.MetricGiftSpendCoin,
|
||||
Title: "bad point task",
|
||||
TargetValue: 100,
|
||||
TargetUnit: "coin",
|
||||
RewardCoinAmount: 12,
|
||||
RewardAssetType: taskdomain.RewardAssetPoint,
|
||||
Status: taskdomain.StatusActive,
|
||||
OperatorAdminID: 90001,
|
||||
}); err == nil {
|
||||
t.Fatal("Lalu explicit POINT task definition must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskRewardPointPolicyAppGateWithoutMySQL(t *testing.T) {
|
||||
repository := &fakeTaskPolicyRepository{activeAsset: taskdomain.RewardAssetPoint, activeFound: true}
|
||||
svc := taskservice.New(repository, &fakeWalletClient{})
|
||||
|
||||
if err := svc.PublishTaskRewardPolicy(appcode.WithContext(context.Background(), "lalu"), taskdomain.RewardPolicyCommand{
|
||||
InstanceCode: "lalu-point-denied",
|
||||
TemplateCode: "huwaa_point_policy",
|
||||
TemplateVersion: "v2",
|
||||
Status: "active",
|
||||
RewardAssetType: taskdomain.RewardAssetPoint,
|
||||
RuleJSON: `{}`,
|
||||
OperatorAdminID: 90001,
|
||||
}); err == nil {
|
||||
t.Fatal("Lalu POINT task reward policy must be rejected before repository call")
|
||||
}
|
||||
if repository.publishCalls != 0 {
|
||||
t.Fatalf("rejected Lalu POINT policy must not call repository, got %d calls", repository.publishCalls)
|
||||
}
|
||||
|
||||
if err := svc.PublishTaskRewardPolicy(appcode.WithContext(context.Background(), "huwaa"), taskdomain.RewardPolicyCommand{
|
||||
InstanceCode: "huwaa-point-live",
|
||||
TemplateCode: "huwaa_point_policy",
|
||||
TemplateVersion: "v2",
|
||||
Status: "active",
|
||||
RewardAssetType: taskdomain.RewardAssetPoint,
|
||||
RuleJSON: `{}`,
|
||||
OperatorAdminID: 90001,
|
||||
}); err != nil {
|
||||
t.Fatalf("Huwaa POINT task reward policy should reach repository: %v", err)
|
||||
}
|
||||
if repository.publishCalls != 1 || repository.lastPublishApp != "huwaa" || repository.lastPublish.RewardAssetType != taskdomain.RewardAssetPoint {
|
||||
t.Fatalf("Huwaa POINT policy repository payload mismatch: repo=%+v", repository)
|
||||
}
|
||||
|
||||
definition, created, err := svc.UpsertTaskDefinition(appcode.WithContext(context.Background(), "huwaa"), taskdomain.DefinitionCommand{
|
||||
TaskType: taskdomain.TypeDaily,
|
||||
Category: "gift",
|
||||
MetricType: taskdomain.MetricGiftSpendCoin,
|
||||
Title: "fake point task",
|
||||
TargetValue: 100,
|
||||
TargetUnit: "coin",
|
||||
RewardCoinAmount: 12,
|
||||
Status: taskdomain.StatusActive,
|
||||
OperatorAdminID: 90001,
|
||||
})
|
||||
if err != nil || !created {
|
||||
t.Fatalf("Huwaa policy default task definition failed: created=%v err=%v", created, err)
|
||||
}
|
||||
if definition.RewardAssetType != taskdomain.RewardAssetPoint ||
|
||||
repository.activeLookupApp != "huwaa" ||
|
||||
repository.lastUpsert.RewardAssetType != taskdomain.RewardAssetPoint {
|
||||
t.Fatalf("Huwaa policy default task definition mismatch: definition=%+v repo=%+v", definition, repository)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeTaskPolicyRepository struct {
|
||||
taskservice.Repository
|
||||
|
||||
activeAsset string
|
||||
activeFound bool
|
||||
activeLookupApp string
|
||||
publishCalls int
|
||||
lastPublish taskdomain.RewardPolicyCommand
|
||||
lastPublishApp string
|
||||
lastUpsert taskdomain.DefinitionCommand
|
||||
}
|
||||
|
||||
func (f *fakeTaskPolicyRepository) GetActiveTaskRewardAssetType(ctx context.Context, _ int64) (string, bool, error) {
|
||||
f.activeLookupApp = appcode.FromContext(ctx)
|
||||
return f.activeAsset, f.activeFound, nil
|
||||
}
|
||||
|
||||
func (f *fakeTaskPolicyRepository) PublishTaskRewardPolicy(ctx context.Context, command taskdomain.RewardPolicyCommand, _ int64) error {
|
||||
f.publishCalls++
|
||||
f.lastPublish = command
|
||||
f.lastPublishApp = appcode.FromContext(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeTaskPolicyRepository) UpsertTaskDefinition(_ context.Context, command taskdomain.DefinitionCommand, _ int64) (taskdomain.Definition, bool, error) {
|
||||
f.lastUpsert = command
|
||||
return taskdomain.Definition{
|
||||
AppCode: "huwaa",
|
||||
TaskID: "fake-point-task",
|
||||
TaskType: command.TaskType,
|
||||
Category: command.Category,
|
||||
MetricType: command.MetricType,
|
||||
Title: command.Title,
|
||||
TargetValue: command.TargetValue,
|
||||
TargetUnit: command.TargetUnit,
|
||||
RewardCoinAmount: command.RewardCoinAmount,
|
||||
RewardAssetType: command.RewardAssetType,
|
||||
Status: command.Status,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
type fakeWalletClient struct {
|
||||
calls int
|
||||
last *walletv1.CreditTaskRewardRequest
|
||||
}
|
||||
|
||||
func (f *fakeWalletClient) CreditTaskReward(_ context.Context, req *walletv1.CreditTaskRewardRequest, _ ...grpc.CallOption) (*walletv1.CreditTaskRewardResponse, error) {
|
||||
f.calls++
|
||||
f.last = req
|
||||
assetType := req.GetRewardAssetType()
|
||||
if assetType == "" {
|
||||
assetType = taskdomain.RewardAssetCoin
|
||||
}
|
||||
return &walletv1.CreditTaskRewardResponse{
|
||||
TransactionId: "wtx-task-1",
|
||||
Amount: 10,
|
||||
Amount: req.GetAmount(),
|
||||
GrantedAtMs: 1778292000000,
|
||||
Balance: &walletv1.AssetBalance{AssetType: "COIN", AvailableAmount: 10},
|
||||
Balance: &walletv1.AssetBalance{AssetType: assetType, AvailableAmount: req.GetAmount()},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -1,624 +0,0 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hyapp/internal/testutil/mysqlschema"
|
||||
"hyapp/pkg/appcode"
|
||||
domain "hyapp/services/activity-service/internal/domain/luckygift"
|
||||
)
|
||||
|
||||
func TestLuckyRuntimeConfigScalesTierRewardsByActualSpendButKeepsRiskCapsAbsolute(t *testing.T) {
|
||||
config := domain.Config{
|
||||
GiftID: domain.DefaultPoolID,
|
||||
PoolID: domain.DefaultPoolID,
|
||||
GiftPrice: 500,
|
||||
MaxSinglePayout: 250_000,
|
||||
UserHourlyPayoutCap: 500_000,
|
||||
Tiers: []domain.Tier{
|
||||
{Pool: domain.PoolAdvanced, TierID: "adv_large_100x", RewardCoins: 50_000, Weight: 1, Enabled: true},
|
||||
{Pool: domain.PoolAdvanced, TierID: "none", RewardCoins: 0, Weight: 1, Enabled: true},
|
||||
},
|
||||
}
|
||||
|
||||
runtimeConfig := luckyRuntimeConfig(config, 1_000)
|
||||
|
||||
if runtimeConfig.GiftPrice != 1_000 {
|
||||
t.Fatalf("expected runtime gift price to follow actual spend, got %d", runtimeConfig.GiftPrice)
|
||||
}
|
||||
if runtimeConfig.Tiers[0].RewardCoins != 100_000 {
|
||||
t.Fatalf("expected tier reward to scale by spend, got %d", runtimeConfig.Tiers[0].RewardCoins)
|
||||
}
|
||||
if runtimeConfig.MaxSinglePayout != 250_000 || runtimeConfig.UserHourlyPayoutCap != 500_000 {
|
||||
t.Fatalf("expected risk caps to stay absolute, got single=%d hourly=%d", runtimeConfig.MaxSinglePayout, runtimeConfig.UserHourlyPayoutCap)
|
||||
}
|
||||
if config.Tiers[0].RewardCoins != 50_000 {
|
||||
t.Fatalf("runtime scaling mutated source config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimPendingLuckyGiftOutboxPrioritizesRewardSettlement(t *testing.T) {
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hyapp_activity_lucky_outbox_test",
|
||||
})
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
|
||||
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||
nowMS := int64(1_700_000_000_000)
|
||||
_, err = repository.db.ExecContext(ctx, `
|
||||
INSERT INTO activity_outbox (
|
||||
app_code, outbox_id, event_type, payload, status, retry_count, next_retry_at_ms,
|
||||
locked_by, lock_until_ms, last_error, created_at_ms, updated_at_ms
|
||||
) VALUES
|
||||
('lalu', 'lucky_legacy_draw_1', ?, '{}', 'pending', 0, ?, '', 0, '', ?, ?),
|
||||
('lalu', 'lucky_reward_draw_1', ?, '{}', 'pending', 0, ?, '', 0, '', ?, ?)`,
|
||||
domain.EventTypeLuckyGiftDrawn, nowMS-100, nowMS-200, nowMS-200,
|
||||
domain.EventTypeLuckyGiftRewardSettlement, nowMS-100, nowMS-100, nowMS-100,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("seed activity_outbox: %v", err)
|
||||
}
|
||||
|
||||
events, err := repository.ClaimPendingLuckyGiftOutbox(ctx, "worker-priority", nowMS, 30*time.Second, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ClaimPendingLuckyGiftOutbox failed: %v", err)
|
||||
}
|
||||
if len(events) != 1 || events[0].EventType != domain.EventTypeLuckyGiftRewardSettlement || events[0].OutboxID != "lucky_reward_draw_1" {
|
||||
t.Fatalf("claim should only return reward settlement first, got %+v", events)
|
||||
}
|
||||
var legacyStatus string
|
||||
if err := repository.db.QueryRowContext(ctx, `
|
||||
SELECT status FROM activity_outbox WHERE app_code = 'lalu' AND outbox_id = 'lucky_legacy_draw_1'`).Scan(&legacyStatus); err != nil {
|
||||
t.Fatalf("read legacy outbox status: %v", err)
|
||||
}
|
||||
if legacyStatus != "pending" {
|
||||
t.Fatalf("legacy outbox should remain pending while reward settlement exists, got %s", legacyStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshLuckyGiftAdminStatsSnapshotsAdvancesCursorByBatch(t *testing.T) {
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hyapp_activity_lucky_stats_test",
|
||||
})
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
|
||||
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||
if _, err := repository.db.ExecContext(ctx, `
|
||||
INSERT INTO lucky_draw_pool_stat_cursors (
|
||||
app_code, cursor_name, last_draw_created_at_ms, last_draw_id, created_at_ms, updated_at_ms
|
||||
) VALUES ('lalu', 'draw_records', 0, '', 1, 1)`); err != nil {
|
||||
t.Fatalf("seed stats cursor: %v", err)
|
||||
}
|
||||
seedLuckyDrawRecordForStats(t, repository, "draw_001", "cmd_001", domain.StatusGranted, 100, 100, 300)
|
||||
seedLuckyDrawRecordForStats(t, repository, "draw_002", "cmd_002", domain.StatusPending, 200, 150, 0)
|
||||
|
||||
processed, err := repository.RefreshLuckyGiftAdminStatsSnapshots(ctx, 300, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("first refresh failed: %v", err)
|
||||
}
|
||||
if processed != 1 {
|
||||
t.Fatalf("first refresh should process one row, got %d", processed)
|
||||
}
|
||||
assertLuckyStatsTotal(t, repository, "", 1, 100, 300)
|
||||
assertLuckyStatsStatus(t, repository, "", 0, 1, 0)
|
||||
assertLuckyStatsCursor(t, repository, 100, "draw_001")
|
||||
processed, err = repository.RefreshLuckyGiftDatabiStatsSnapshots(ctx, 310, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("first databi refresh failed: %v", err)
|
||||
}
|
||||
if processed != 1 {
|
||||
t.Fatalf("first databi refresh should process one row, got %d", processed)
|
||||
}
|
||||
assertLuckyDatabiDayStatsTotal(t, repository, "1970-01-01", "", 1, 100, 300)
|
||||
|
||||
processed, err = repository.RefreshLuckyGiftAdminStatsSnapshots(ctx, 400, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("second refresh failed: %v", err)
|
||||
}
|
||||
if processed != 1 {
|
||||
t.Fatalf("second refresh should process remaining row, got %d", processed)
|
||||
}
|
||||
assertLuckyStatsTotal(t, repository, "", 2, 250, 300)
|
||||
assertLuckyStatsStatus(t, repository, "", 1, 1, 0)
|
||||
assertLuckyStatsCursor(t, repository, 200, "draw_002")
|
||||
processed, err = repository.RefreshLuckyGiftDatabiStatsSnapshots(ctx, 410, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("second databi refresh failed: %v", err)
|
||||
}
|
||||
if processed != 1 {
|
||||
t.Fatalf("second databi refresh should process remaining row, got %d", processed)
|
||||
}
|
||||
assertLuckyDatabiDayStatsTotal(t, repository, "1970-01-01", "", 2, 250, 300)
|
||||
}
|
||||
|
||||
func seedLuckyDrawRecordForStats(t *testing.T, repository *Repository, drawID string, commandID string, status string, createdAtMS int64, spent int64, reward int64) {
|
||||
t.Helper()
|
||||
_, err := repository.db.ExecContext(context.Background(), `
|
||||
INSERT INTO lucky_draw_records (
|
||||
app_code, draw_id, command_id, user_id, device_id, room_id, anchor_id, pool_id, gift_id,
|
||||
coin_spent, rule_version, experience_pool, rtp_window_index, gift_rtp_window_index,
|
||||
selected_tier_id, base_reward_coins, room_atmosphere_reward_coins, activity_subsidy_coins,
|
||||
effective_reward_coins, budget_sources_json, stage_feedback, high_multiplier,
|
||||
candidate_tiers_json, pool_snapshot_json, rtp_snapshot_json, reward_status,
|
||||
reward_transaction_id, reward_failure_reason, paid_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES (
|
||||
'lalu', ?, ?, 42, 'device-1', 'room-1', 'anchor-1', 'lucky', 'rose',
|
||||
?, 1, 'novice', 1, 1,
|
||||
'tier', ?, 0, 0,
|
||||
?, '{}', 0, 0,
|
||||
'{}', '{}', '{}', ?,
|
||||
'', '', ?, ?, ?
|
||||
)`, drawID, commandID, spent, reward, reward, status, createdAtMS, createdAtMS, createdAtMS)
|
||||
if err != nil {
|
||||
t.Fatalf("seed draw record %s: %v", drawID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertLuckyStatsTotal(t *testing.T, repository *Repository, giftID string, wantDraws int64, wantSpent int64, wantReward int64) {
|
||||
t.Helper()
|
||||
var draws, spent, reward int64
|
||||
if err := repository.db.QueryRowContext(context.Background(), `
|
||||
SELECT total_draws, total_spent_coins, total_reward_coins
|
||||
FROM lucky_draw_pool_stats
|
||||
WHERE app_code = 'lalu' AND pool_id = 'lucky' AND gift_id = ?`, giftID,
|
||||
).Scan(&draws, &spent, &reward); err != nil {
|
||||
t.Fatalf("read stats total: %v", err)
|
||||
}
|
||||
if draws != wantDraws || spent != wantSpent || reward != wantReward {
|
||||
t.Fatalf("stats mismatch: draws=%d spent=%d reward=%d want draws=%d spent=%d reward=%d", draws, spent, reward, wantDraws, wantSpent, wantReward)
|
||||
}
|
||||
}
|
||||
|
||||
func assertLuckyStatsStatus(t *testing.T, repository *Repository, giftID string, wantPending int64, wantGranted int64, wantFailed int64) {
|
||||
t.Helper()
|
||||
var pending, granted, failed int64
|
||||
if err := repository.db.QueryRowContext(context.Background(), `
|
||||
SELECT pending_draws, granted_draws, failed_draws
|
||||
FROM lucky_draw_pool_stats
|
||||
WHERE app_code = 'lalu' AND pool_id = 'lucky' AND gift_id = ?`, giftID,
|
||||
).Scan(&pending, &granted, &failed); err != nil {
|
||||
t.Fatalf("read stats status: %v", err)
|
||||
}
|
||||
if pending != wantPending || granted != wantGranted || failed != wantFailed {
|
||||
t.Fatalf("stats status mismatch: pending=%d granted=%d failed=%d want pending=%d granted=%d failed=%d", pending, granted, failed, wantPending, wantGranted, wantFailed)
|
||||
}
|
||||
}
|
||||
|
||||
func assertLuckyDatabiDayStatsTotal(t *testing.T, repository *Repository, day string, giftID string, wantDraws int64, wantSpent int64, wantReward int64) {
|
||||
t.Helper()
|
||||
var draws, spent, reward int64
|
||||
if err := repository.db.QueryRowContext(context.Background(), `
|
||||
SELECT draw_count, turnover_coin, payout_coin
|
||||
FROM lucky_draw_pool_day_stats
|
||||
WHERE app_code = 'lalu' AND stat_day = ? AND visible_region_id = 0 AND pool_id = 'lucky' AND gift_id = ?`, day, giftID,
|
||||
).Scan(&draws, &spent, &reward); err != nil {
|
||||
t.Fatalf("read databi day stats total: %v", err)
|
||||
}
|
||||
if draws != wantDraws || spent != wantSpent || reward != wantReward {
|
||||
t.Fatalf("databi day stats mismatch: draws=%d spent=%d reward=%d want draws=%d spent=%d reward=%d", draws, spent, reward, wantDraws, wantSpent, wantReward)
|
||||
}
|
||||
}
|
||||
|
||||
func assertLuckyStatsCursor(t *testing.T, repository *Repository, wantCreatedAtMS int64, wantDrawID string) {
|
||||
t.Helper()
|
||||
var createdAtMS int64
|
||||
var drawID string
|
||||
if err := repository.db.QueryRowContext(context.Background(), `
|
||||
SELECT last_draw_created_at_ms, last_draw_id
|
||||
FROM lucky_draw_pool_stat_cursors
|
||||
WHERE app_code = 'lalu' AND cursor_name = 'draw_records'`,
|
||||
).Scan(&createdAtMS, &drawID); err != nil {
|
||||
t.Fatalf("read stats cursor: %v", err)
|
||||
}
|
||||
if createdAtMS != wantCreatedAtMS || drawID != wantDrawID {
|
||||
t.Fatalf("cursor mismatch: created_at=%d draw_id=%s want created_at=%d draw_id=%s", createdAtMS, drawID, wantCreatedAtMS, wantDrawID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuckyRuntimeConfigFromRuleConfigUsesV2PoolAndStageTiers(t *testing.T) {
|
||||
ruleConfig := domain.RuleConfig{
|
||||
AppCode: "lalu",
|
||||
PoolID: "lucky",
|
||||
RuleVersion: 7,
|
||||
Enabled: true,
|
||||
TargetRTPPPM: 950_000,
|
||||
PoolRatePPM: 960_000,
|
||||
SettlementWindowWager: 1_000_001,
|
||||
GiftPriceReference: 100,
|
||||
MaxSinglePayout: 9_999,
|
||||
UserHourlyPayoutCap: 10_001,
|
||||
UserDailyPayoutCap: 10_002,
|
||||
DeviceDailyPayoutCap: 10_003,
|
||||
RoomHourlyPayoutCap: 10_004,
|
||||
AnchorDailyPayoutCap: 10_005,
|
||||
CreatedByAdminID: 42,
|
||||
CreatedAtMS: 123456,
|
||||
Stages: []domain.RuleStage{
|
||||
{Stage: domain.StageNovice, Tiers: []domain.RuleTier{
|
||||
{Stage: domain.StageNovice, TierID: "novice_none", MultiplierPPM: 0, BaseWeightPPM: 300_000, RewardSource: domain.SourceBaseRTP, Enabled: true},
|
||||
{Stage: domain.StageNovice, TierID: "novice_0_3x", MultiplierPPM: 300_000, BaseWeightPPM: 220_000, RewardSource: domain.SourceBaseRTP, Enabled: true},
|
||||
}},
|
||||
{Stage: domain.StageNormal, Tiers: []domain.RuleTier{
|
||||
{Stage: domain.StageNormal, TierID: "normal_2x", MultiplierPPM: 2_000_000, BaseWeightPPM: 100_000, RewardSource: domain.SourceBaseRTP, Enabled: true},
|
||||
}},
|
||||
{Stage: domain.StageAdvanced, Tiers: []domain.RuleTier{
|
||||
{Stage: domain.StageAdvanced, TierID: "advanced_5x", MultiplierPPM: 5_000_000, BaseWeightPPM: 50_000, RewardSource: domain.SourceBaseRTP, HighWaterOnly: true, Enabled: true},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
config, err := luckyRuntimeConfigFromRuleConfig(ruleConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("convert v2 rule config failed: %v", err)
|
||||
}
|
||||
if config.GiftID != "lucky" || config.PoolID != "lucky" || config.RuleVersion != 7 {
|
||||
t.Fatalf("runtime identity should come from pool version, got pool=%s gift=%s version=%d", config.PoolID, config.GiftID, config.RuleVersion)
|
||||
}
|
||||
if config.GlobalWindowDraws != 10_001 || config.GiftWindowDraws != 10_001 {
|
||||
t.Fatalf("expected wager window to be converted to equivalent draw window, got global=%d gift=%d", config.GlobalWindowDraws, config.GiftWindowDraws)
|
||||
}
|
||||
if config.UserHourlyPayoutCap != 10_001 || config.RoomHourlyPayoutCap != 10_004 {
|
||||
t.Fatalf("expected runtime risk caps to come from v2 config, got user_hour=%d room_hour=%d", config.UserHourlyPayoutCap, config.RoomHourlyPayoutCap)
|
||||
}
|
||||
expected := map[string]struct {
|
||||
pool string
|
||||
rewardCoins int64
|
||||
weight int64
|
||||
highWaterOnly bool
|
||||
}{
|
||||
"novice_0_3x": {pool: domain.PoolNovice, rewardCoins: 30, weight: 220_000},
|
||||
"normal_2x": {pool: domain.PoolIntermediate, rewardCoins: 200, weight: 100_000},
|
||||
"advanced_5x": {pool: domain.PoolAdvanced, rewardCoins: 500, weight: 50_000, highWaterOnly: true},
|
||||
}
|
||||
for _, tier := range config.Tiers {
|
||||
want, exists := expected[tier.TierID]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
if tier.Pool != want.pool || tier.RewardCoins != want.rewardCoins || tier.Weight != want.weight || tier.HighWaterOnly != want.highWaterOnly {
|
||||
t.Fatalf("tier %s mismatch: got %#v want %#v", tier.TierID, tier, want)
|
||||
}
|
||||
delete(expected, tier.TierID)
|
||||
}
|
||||
if len(expected) != 0 {
|
||||
t.Fatalf("missing converted v2 tiers: %#v", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuckyExperiencePoolUsesEquivalentDrawsFromCumulativeWager(t *testing.T) {
|
||||
config := domain.Config{
|
||||
NoviceMaxEquivalentDraws: 2_000,
|
||||
NormalMaxEquivalentDraws: 20_000,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
cumulativeWager int64
|
||||
referencePrice int64
|
||||
wantDraws int64
|
||||
wantPool string
|
||||
}{
|
||||
{name: "small gift advances fractionally by wager", cumulativeWager: 199_900, referencePrice: 100, wantDraws: 1_999, wantPool: domain.PoolNovice},
|
||||
{name: "novice boundary includes exact threshold", cumulativeWager: 200_000, referencePrice: 100, wantDraws: 2_000, wantPool: domain.PoolNovice},
|
||||
{name: "large gift exits novice by value not send count", cumulativeWager: 200_100, referencePrice: 100, wantDraws: 2_001, wantPool: domain.PoolIntermediate},
|
||||
{name: "advanced starts after normal threshold", cumulativeWager: 2_000_100, referencePrice: 100, wantDraws: 20_001, wantPool: domain.PoolAdvanced},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
draws := luckyEquivalentDraws(tt.cumulativeWager, tt.referencePrice)
|
||||
if draws != tt.wantDraws {
|
||||
t.Fatalf("equivalent draws = %d, want %d", draws, tt.wantDraws)
|
||||
}
|
||||
if pool := luckyExperiencePool(draws, config); pool != tt.wantPool {
|
||||
t.Fatalf("experience pool = %s, want %s", pool, tt.wantPool)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuckyDrawUnitSpendSplitsBatchSpendByGiftCount(t *testing.T) {
|
||||
var total int64
|
||||
for index := int32(1); index <= 10; index++ {
|
||||
spend := luckyDrawUnitSpend(1_001, 10, index)
|
||||
total += spend
|
||||
if index == 1 && spend != 101 {
|
||||
t.Fatalf("expected remainder coin to be assigned to first draw, got %d", spend)
|
||||
}
|
||||
if index > 1 && spend != 100 {
|
||||
t.Fatalf("expected normal unit spend to be 100, got index=%d spend=%d", index, spend)
|
||||
}
|
||||
}
|
||||
if total != 1_001 {
|
||||
t.Fatalf("unit spends must preserve total spend, got %d", total)
|
||||
}
|
||||
if spend := luckyDrawUnitSpend(1_000, 10, 7); spend != 100 {
|
||||
t.Fatalf("expected 1000 coins / 10 draws to use 100 per draw, got %d", spend)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuckyDrawSubCommandIDIsStableAndBounded(t *testing.T) {
|
||||
if got := luckyDrawSubCommandID("gift_cmd", 3, 10); got != "gift_cmd#000003" {
|
||||
t.Fatalf("short sub command id mismatch: %s", got)
|
||||
}
|
||||
longCommand := strings.Repeat("x", 160)
|
||||
first := luckyDrawSubCommandID(longCommand, 12, 20)
|
||||
second := luckyDrawSubCommandID(longCommand, 12, 20)
|
||||
if first != second {
|
||||
t.Fatalf("sub command id must be stable, got %s and %s", first, second)
|
||||
}
|
||||
if len(first) > 128 || !strings.HasSuffix(first, "#000012") {
|
||||
t.Fatalf("sub command id must fit db column and keep draw index, got len=%d id=%s", len(first), first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuckyAggregateDrawResultsSummarizesBatchReward(t *testing.T) {
|
||||
cmd := domain.DrawCommand{CommandID: "gift_cmd", CoinSpent: 1_000}
|
||||
results := []domain.DrawResult{
|
||||
{DrawID: "draw-1", CommandID: "gift_cmd#000001", PoolID: "lucky", GiftID: "gift-1", RuleVersion: 7, ExperiencePool: domain.PoolNovice, SelectedTierID: "none", BudgetSourcesJSON: `{"sources":["base_rtp"]}`, RewardStatus: domain.StatusGranted},
|
||||
{DrawID: "draw-2", CommandID: "gift_cmd#000002", PoolID: "lucky", GiftID: "gift-1", RuleVersion: 7, ExperiencePool: domain.PoolNovice, SelectedTierID: "five", BaseRewardCoins: 500, EffectiveRewardCoins: 500, MultiplierPPM: 5_000_000, BudgetSourcesJSON: `{"sources":["base_rtp"]}`, RewardStatus: domain.StatusPending, StageFeedback: true, HighMultiplier: true, CreatedAtMS: 123},
|
||||
{DrawID: "draw-3", CommandID: "gift_cmd#000003", PoolID: "lucky", GiftID: "gift-1", RuleVersion: 7, ExperiencePool: domain.PoolNovice, SelectedTierID: "two", BaseRewardCoins: 200, EffectiveRewardCoins: 200, MultiplierPPM: 2_000_000, BudgetSourcesJSON: `{"sources":["base_rtp"]}`, RewardStatus: domain.StatusPending, CreatedAtMS: 123},
|
||||
}
|
||||
for index := 0; index < 4; index++ {
|
||||
results = append(results, domain.DrawResult{
|
||||
DrawID: "draw-one",
|
||||
CommandID: "gift_cmd#00000x",
|
||||
PoolID: "lucky",
|
||||
GiftID: "gift-1",
|
||||
RuleVersion: 7,
|
||||
ExperiencePool: domain.PoolNovice,
|
||||
SelectedTierID: "one",
|
||||
BaseRewardCoins: 100,
|
||||
EffectiveRewardCoins: 100,
|
||||
MultiplierPPM: 1_000_000,
|
||||
BudgetSourcesJSON: `{"sources":["base_rtp"]}`,
|
||||
RewardStatus: domain.StatusPending,
|
||||
CreatedAtMS: 123,
|
||||
})
|
||||
}
|
||||
for len(results) < 10 {
|
||||
results = append(results, domain.DrawResult{DrawID: "draw-none", CommandID: "gift_cmd#00000n", PoolID: "lucky", GiftID: "gift-1", RuleVersion: 7, ExperiencePool: domain.PoolNovice, SelectedTierID: "none", BudgetSourcesJSON: `{"sources":["base_rtp"]}`, RewardStatus: domain.StatusGranted})
|
||||
}
|
||||
|
||||
aggregate := luckyAggregateDrawResults(cmd, results)
|
||||
|
||||
if aggregate.CommandID != "gift_cmd" || aggregate.SelectedTierID != "batch" {
|
||||
t.Fatalf("aggregate identity mismatch: %#v", aggregate)
|
||||
}
|
||||
if aggregate.EffectiveRewardCoins != 1_100 || aggregate.BaseRewardCoins != 1_100 || aggregate.MultiplierPPM != 11_000_000 {
|
||||
t.Fatalf("aggregate reward mismatch: reward=%d base=%d multiplier=%d", aggregate.EffectiveRewardCoins, aggregate.BaseRewardCoins, aggregate.MultiplierPPM)
|
||||
}
|
||||
if aggregate.RewardStatus != domain.StatusPending || !aggregate.StageFeedback || !aggregate.HighMultiplier {
|
||||
t.Fatalf("aggregate flags mismatch: %#v", aggregate)
|
||||
}
|
||||
if !strings.Contains(aggregate.BudgetSourcesJSON, `"draw_count":10`) || !strings.Contains(aggregate.BudgetSourcesJSON, domain.SourceBaseRTP) {
|
||||
t.Fatalf("aggregate budget json mismatch: %s", aggregate.BudgetSourcesJSON)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuckyRTPWindowAddsCurrentSpendToTarget(t *testing.T) {
|
||||
window := luckyRTPWindow{
|
||||
ControlDraws: 100,
|
||||
TargetRTPPPM: 950_000,
|
||||
WagerCoins: 500,
|
||||
CarryPPM: 250_000,
|
||||
PaidDraws: 1,
|
||||
TargetPayoutCoins: 475,
|
||||
}
|
||||
|
||||
next := window.withCurrentWager(1_000)
|
||||
|
||||
if next.WagerCoins != 1_500 {
|
||||
t.Fatalf("expected wager to include current spend, got %d", next.WagerCoins)
|
||||
}
|
||||
if next.TargetPayoutCoins != 1_425 {
|
||||
t.Fatalf("expected target payout to include current spend, got %d", next.TargetPayoutCoins)
|
||||
}
|
||||
if next.CarryPPM != 250_000 {
|
||||
t.Fatalf("expected carry to keep fractional target, got %d", next.CarryPPM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuckyConfiguredWeightCandidatesUsesConfiguredWeightsWithoutForcedSettlement(t *testing.T) {
|
||||
candidates := []luckyCandidate{
|
||||
{TierID: "none", BaseReward: 0, Weight: 300_000},
|
||||
{TierID: "one", BaseReward: 100, Weight: 600_000},
|
||||
{TierID: "two", BaseReward: 200, Weight: 100_000},
|
||||
}
|
||||
|
||||
weighted := luckyConfiguredWeightCandidates(candidates)
|
||||
|
||||
if len(weighted) != 3 {
|
||||
t.Fatalf("expected configured candidates inside control band, got %#v", weighted)
|
||||
}
|
||||
if weighted[0].TierID != "none" || weighted[0].Weight != 300_000 || weighted[2].TierID != "two" || weighted[2].Weight != 100_000 {
|
||||
t.Fatalf("expected configured weights to be preserved, got %#v", weighted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectLuckyCandidateDoesNotInjectNoneTier(t *testing.T) {
|
||||
config := domain.Config{
|
||||
GiftID: domain.DefaultPoolID,
|
||||
PoolID: domain.DefaultPoolID,
|
||||
GiftPrice: 10,
|
||||
TargetRTPPPM: 950_000,
|
||||
HighMultiplier: 100,
|
||||
LargeTierEnabled: true,
|
||||
PlatformPoolWeightPPM: 200_000,
|
||||
RoomPoolWeightPPM: 300_000,
|
||||
GiftPoolWeightPPM: 500_000,
|
||||
MaxSinglePayout: 10_000,
|
||||
UserHourlyPayoutCap: 10_000,
|
||||
UserDailyPayoutCap: 10_000,
|
||||
DeviceDailyPayoutCap: 10_000,
|
||||
RoomHourlyPayoutCap: 10_000,
|
||||
AnchorDailyPayoutCap: 10_000,
|
||||
Tiers: []domain.Tier{
|
||||
{Pool: domain.PoolAdvanced, TierID: "advanced_1x", RewardCoins: 10, MultiplierPPM: 1_000_000, Enabled: true},
|
||||
{Pool: domain.PoolAdvanced, TierID: "advanced_2x", RewardCoins: 20, MultiplierPPM: 2_000_000, Enabled: true},
|
||||
},
|
||||
}
|
||||
pool := luckyPool{Balance: 1_000_000}
|
||||
counters := map[string]luckyCounter{
|
||||
luckyCounterKey("user", "hour"): {},
|
||||
luckyCounterKey("user", "day"): {},
|
||||
luckyCounterKey("device", "day"): {},
|
||||
luckyCounterKey("room", "hour"): {},
|
||||
luckyCounterKey("anchor", "day"): {},
|
||||
}
|
||||
|
||||
candidate, _, err := (&Repository{}).selectLuckyCandidate(
|
||||
config,
|
||||
domain.PoolAdvanced,
|
||||
pool,
|
||||
pool,
|
||||
pool,
|
||||
counters,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("select positive-only candidate failed: %v", err)
|
||||
}
|
||||
if candidate.BaseReward <= 0 || candidate.MultiplierPPM <= 0 {
|
||||
t.Fatalf("positive-only config should not draw implicit none tier: %#v", candidate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectLuckyCandidateKeepsSmallGiftEligibleAfterLargeWagerWhenAbsoluteCapAllows(t *testing.T) {
|
||||
config := domain.Config{
|
||||
GiftID: domain.DefaultPoolID,
|
||||
PoolID: domain.DefaultPoolID,
|
||||
GiftPrice: 10,
|
||||
TargetRTPPPM: 950_000,
|
||||
HighMultiplier: 100,
|
||||
LargeTierEnabled: true,
|
||||
PlatformPoolWeightPPM: 200_000,
|
||||
RoomPoolWeightPPM: 300_000,
|
||||
GiftPoolWeightPPM: 500_000,
|
||||
MaxSinglePayout: 1_000_000,
|
||||
UserHourlyPayoutCap: 1_000_000,
|
||||
UserDailyPayoutCap: 1_000_000,
|
||||
DeviceDailyPayoutCap: 1_000_000,
|
||||
RoomHourlyPayoutCap: 1_000_000,
|
||||
AnchorDailyPayoutCap: 1_000_000,
|
||||
Tiers: []domain.Tier{
|
||||
{Pool: domain.PoolAdvanced, TierID: "advanced_none", RewardCoins: 0, MultiplierPPM: 0, Weight: 500_000, Enabled: true},
|
||||
{Pool: domain.PoolAdvanced, TierID: "advanced_1x", RewardCoins: 10, MultiplierPPM: 1_000_000, Weight: 300_000, Enabled: true},
|
||||
{Pool: domain.PoolAdvanced, TierID: "advanced_2x", RewardCoins: 20, MultiplierPPM: 2_000_000, Weight: 200_000, Enabled: true},
|
||||
},
|
||||
}
|
||||
pool := luckyPool{Balance: 1_000_000}
|
||||
counters := map[string]luckyCounter{
|
||||
luckyCounterKey("user", "hour"): {Payout: 999_970},
|
||||
luckyCounterKey("user", "day"): {Payout: 999_970},
|
||||
luckyCounterKey("device", "day"): {Payout: 999_970},
|
||||
luckyCounterKey("room", "hour"): {Payout: 999_970},
|
||||
luckyCounterKey("anchor", "day"): {Payout: 999_970},
|
||||
}
|
||||
|
||||
seenPositive := false
|
||||
for i := 0; i < 100; i++ {
|
||||
candidate, limited, err := (&Repository{}).selectLuckyCandidate(config, domain.PoolAdvanced, pool, pool, pool, counters)
|
||||
if err != nil {
|
||||
t.Fatalf("select small gift candidate failed: %v", err)
|
||||
}
|
||||
if limited["risk_cap"] {
|
||||
t.Fatalf("small payable tiers should not be risk-filtered when absolute cap still has room")
|
||||
}
|
||||
if candidate.BaseReward > 0 {
|
||||
seenPositive = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seenPositive {
|
||||
t.Fatalf("expected positive small-gift tier to remain in random candidate set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectLuckyCandidateFallsBackToZeroWhenAllPositiveTiersAreUnpayable(t *testing.T) {
|
||||
config := domain.Config{
|
||||
GiftID: domain.DefaultPoolID,
|
||||
PoolID: domain.DefaultPoolID,
|
||||
GiftPrice: 10,
|
||||
HighMultiplier: 100,
|
||||
LargeTierEnabled: true,
|
||||
PlatformPoolWeightPPM: 200_000,
|
||||
RoomPoolWeightPPM: 300_000,
|
||||
GiftPoolWeightPPM: 500_000,
|
||||
MaxSinglePayout: 10,
|
||||
UserHourlyPayoutCap: 10,
|
||||
UserDailyPayoutCap: 10,
|
||||
DeviceDailyPayoutCap: 10,
|
||||
RoomHourlyPayoutCap: 10,
|
||||
AnchorDailyPayoutCap: 10,
|
||||
Tiers: []domain.Tier{
|
||||
{Pool: domain.PoolAdvanced, TierID: "advanced_2x", RewardCoins: 20, MultiplierPPM: 2_000_000, Weight: 1_000_000, Enabled: true},
|
||||
},
|
||||
}
|
||||
pool := luckyPool{Balance: 1_000_000}
|
||||
counters := map[string]luckyCounter{
|
||||
luckyCounterKey("user", "hour"): {},
|
||||
luckyCounterKey("user", "day"): {},
|
||||
luckyCounterKey("device", "day"): {},
|
||||
luckyCounterKey("room", "hour"): {},
|
||||
luckyCounterKey("anchor", "day"): {},
|
||||
}
|
||||
|
||||
candidate, limited, err := (&Repository{}).selectLuckyCandidate(config, domain.PoolAdvanced, pool, pool, pool, counters)
|
||||
if err != nil {
|
||||
t.Fatalf("select unpayable candidate failed: %v", err)
|
||||
}
|
||||
if candidate.TierID != "no_payable_tier" || candidate.effectiveReward() != 0 {
|
||||
t.Fatalf("expected no-payable fallback, got %#v", candidate)
|
||||
}
|
||||
if !limited["risk_cap"] {
|
||||
t.Fatalf("expected risk_cap to explain filtered positive tier")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuckyInitialRewardStatusGrantsZeroRewardWithoutOutbox(t *testing.T) {
|
||||
if status := luckyInitialRewardStatus(luckyCandidate{TierID: "none"}, false); status != domain.StatusGranted {
|
||||
t.Fatalf("zero reward without presentation should be granted, got %s", status)
|
||||
}
|
||||
if luckyDrawNeedsRewardSettlementOutbox(luckyCandidate{TierID: "none"}) {
|
||||
t.Fatalf("zero reward without presentation must not emit outbox")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuckyInitialRewardStatusOnlyWaitsForWalletSettlement(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
candidate luckyCandidate
|
||||
stageFeedback bool
|
||||
wantStatus string
|
||||
wantReward bool
|
||||
}{
|
||||
{name: "wallet credit", candidate: luckyCandidate{TierID: "hit", BaseReward: 10}, wantStatus: domain.StatusPending, wantReward: true},
|
||||
{name: "presentation candidate", candidate: luckyCandidate{TierID: "stage", Presentation: true}, wantStatus: domain.StatusGranted},
|
||||
{name: "stage feedback", candidate: luckyCandidate{TierID: "none"}, stageFeedback: true, wantStatus: domain.StatusGranted},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if status := luckyInitialRewardStatus(test.candidate, test.stageFeedback); status != test.wantStatus {
|
||||
t.Fatalf("reward status mismatch: got %s want %s", status, test.wantStatus)
|
||||
}
|
||||
if got := luckyDrawNeedsRewardSettlementOutbox(test.candidate); got != test.wantReward {
|
||||
t.Fatalf("reward settlement outbox mismatch: got %v want %v", got, test.wantReward)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,196 +0,0 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hyapp/internal/testutil/mysqlschema"
|
||||
"hyapp/pkg/appcode"
|
||||
domain "hyapp/services/activity-service/internal/domain/luckygift"
|
||||
luckygiftservice "hyapp/services/activity-service/internal/service/luckygift"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
)
|
||||
|
||||
func TestLuckyGiftRewardSettlementOutboxPressureLocal(t *testing.T) {
|
||||
if os.Getenv("RUN_LUCKY_GIFT_SETTLEMENT_PRESSURE") != "1" {
|
||||
t.Skip("set RUN_LUCKY_GIFT_SETTLEMENT_PRESSURE=1 to run local settlement outbox pressure test")
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
total int
|
||||
batchSize int
|
||||
concurrency int
|
||||
}{
|
||||
{name: "1k_batch100_c4", total: 1000, batchSize: 100, concurrency: 4},
|
||||
{name: "5k_batch100_c4", total: 5000, batchSize: 100, concurrency: 4},
|
||||
{name: "10k_batch200_c8", total: 10000, batchSize: 200, concurrency: 8},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runLuckyGiftSettlementPressureCase(t, tc.total, tc.batchSize, tc.concurrency)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runLuckyGiftSettlementPressureCase(t *testing.T, total int, batchSize int, concurrency int) {
|
||||
t.Helper()
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hy_lucky_press",
|
||||
})
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
|
||||
seedStart := time.Now()
|
||||
seedLuckyGiftSettlementPressureRows(t, repository, total)
|
||||
seedElapsed := time.Since(seedStart)
|
||||
|
||||
wallet := &pressureLuckyGiftWallet{}
|
||||
service := luckygiftservice.New(repository, luckygiftservice.WithWallet(wallet))
|
||||
ctx := appcode.WithContext(context.Background(), "lalu")
|
||||
options := luckygiftservice.WorkerOptions{
|
||||
WorkerID: "pressure-worker",
|
||||
BatchSize: batchSize,
|
||||
Concurrency: concurrency,
|
||||
LockTTL: time.Minute,
|
||||
MaxRetry: 3,
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
processedTotal := 0
|
||||
loops := 0
|
||||
for processedTotal < total {
|
||||
processed, err := service.ProcessPendingDrawOutbox(ctx, options)
|
||||
if err != nil {
|
||||
t.Fatalf("process settlement outbox: %v", err)
|
||||
}
|
||||
if processed == 0 {
|
||||
break
|
||||
}
|
||||
processedTotal += processed
|
||||
loops++
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
granted := countLuckyGiftPressureRows(t, repository, "lucky_draw_records", "reward_status = 'granted'")
|
||||
delivered := countLuckyGiftPressureRows(t, repository, "activity_outbox", "status = 'delivered'")
|
||||
if processedTotal != total || granted != int64(total) || delivered != int64(total) || wallet.calls.Load() != int64(total) {
|
||||
t.Fatalf("pressure result mismatch: processed=%d granted=%d delivered=%d wallet_calls=%d total=%d",
|
||||
processedTotal, granted, delivered, wallet.calls.Load(), total)
|
||||
}
|
||||
t.Logf("settlement_pressure total=%d batch=%d concurrency=%d loops=%d seed_ms=%d process_ms=%d throughput=%.2f events/s avg_event_ms=%.4f",
|
||||
total, batchSize, concurrency, loops, seedElapsed.Milliseconds(), elapsed.Milliseconds(),
|
||||
float64(total)/elapsed.Seconds(), float64(elapsed.Microseconds())/1000.0/float64(total))
|
||||
}
|
||||
|
||||
func seedLuckyGiftSettlementPressureRows(t *testing.T, repository *Repository, total int) {
|
||||
t.Helper()
|
||||
const chunkSize = 500
|
||||
for start := 0; start < total; start += chunkSize {
|
||||
end := start + chunkSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
if err := seedLuckyGiftSettlementPressureChunk(repository, start, end); err != nil {
|
||||
t.Fatalf("seed pressure rows %d-%d: %v", start, end, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func seedLuckyGiftSettlementPressureChunk(repository *Repository, start int, end int) error {
|
||||
nowMS := time.Now().UTC().UnixMilli()
|
||||
drawPlaceholders := make([]string, 0, end-start)
|
||||
drawArgs := make([]any, 0, (end-start)*31)
|
||||
outboxPlaceholders := make([]string, 0, end-start)
|
||||
outboxArgs := make([]any, 0, (end-start)*12)
|
||||
for index := start; index < end; index++ {
|
||||
drawID := fmt.Sprintf("pressure_draw_%08d", index)
|
||||
commandID := fmt.Sprintf("pressure_cmd_%08d", index)
|
||||
userID := int64(100000 + index%1000)
|
||||
roomID := fmt.Sprintf("room_%03d", index%100)
|
||||
createdAtMS := nowMS + int64(index)
|
||||
drawPlaceholders = append(drawPlaceholders, "("+luckySQLPlaceholders(31)+")")
|
||||
drawArgs = append(drawArgs,
|
||||
"lalu", drawID, commandID, userID, "pressure-device", roomID, "pressure-anchor", "lucky", "rose",
|
||||
int64(100), int64(1), domain.PoolNovice, int64(1), int64(1),
|
||||
"hit", int64(10), int64(0), int64(0),
|
||||
int64(10), "{}", false, false,
|
||||
"{}", "{}", "{}", domain.StatusPending, "", "",
|
||||
createdAtMS, createdAtMS, createdAtMS,
|
||||
)
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"event_type": "lucky_gift_drawn",
|
||||
"event_id": "lucky_gift_drawn:" + drawID,
|
||||
"app_code": "lalu",
|
||||
"draw_id": drawID,
|
||||
"command_id": commandID,
|
||||
"pool_id": "lucky",
|
||||
"user_id": userID,
|
||||
"room_id": roomID,
|
||||
"gift_id": "rose",
|
||||
"gift_count": 1,
|
||||
"coin_spent": 100,
|
||||
"effective_reward_coins": 10,
|
||||
"created_at_ms": createdAtMS,
|
||||
})
|
||||
outboxPlaceholders = append(outboxPlaceholders, "("+luckySQLPlaceholders(12)+")")
|
||||
outboxArgs = append(outboxArgs,
|
||||
"lalu", "lucky_reward_"+drawID, domain.EventTypeLuckyGiftRewardSettlement, payload,
|
||||
"pending", 0, createdAtMS, "", int64(0), "", createdAtMS, createdAtMS,
|
||||
)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if _, err := repository.db.ExecContext(ctx, `
|
||||
INSERT INTO lucky_draw_records (
|
||||
app_code, draw_id, command_id, user_id, device_id, room_id, anchor_id, pool_id, gift_id,
|
||||
coin_spent, rule_version, experience_pool, rtp_window_index, gift_rtp_window_index,
|
||||
selected_tier_id, base_reward_coins, room_atmosphere_reward_coins, activity_subsidy_coins,
|
||||
effective_reward_coins, budget_sources_json, stage_feedback, high_multiplier,
|
||||
candidate_tiers_json, pool_snapshot_json, rtp_snapshot_json, reward_status,
|
||||
reward_transaction_id, reward_failure_reason, paid_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES `+strings.Join(drawPlaceholders, ","), drawArgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := repository.db.ExecContext(ctx, `
|
||||
INSERT INTO activity_outbox (
|
||||
app_code, outbox_id, event_type, payload, status, retry_count, next_retry_at_ms,
|
||||
locked_by, lock_until_ms, last_error, created_at_ms, updated_at_ms
|
||||
) VALUES `+strings.Join(outboxPlaceholders, ","), outboxArgs...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func countLuckyGiftPressureRows(t *testing.T, repository *Repository, table string, where string) int64 {
|
||||
t.Helper()
|
||||
var count int64
|
||||
if err := repository.db.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM "+table+" WHERE app_code = 'lalu' AND "+where).Scan(&count); err != nil {
|
||||
t.Fatalf("count %s: %v", table, err)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
type pressureLuckyGiftWallet struct {
|
||||
calls atomic.Int64
|
||||
}
|
||||
|
||||
func (w *pressureLuckyGiftWallet) CreditLuckyGiftReward(_ context.Context, req *walletv1.CreditLuckyGiftRewardRequest, _ ...grpc.CallOption) (*walletv1.CreditLuckyGiftRewardResponse, error) {
|
||||
w.calls.Add(1)
|
||||
return &walletv1.CreditLuckyGiftRewardResponse{
|
||||
TransactionId: "pressure_tx_" + req.GetDrawId(),
|
||||
Balance: &walletv1.AssetBalance{AvailableAmount: 1_000_000},
|
||||
Amount: req.GetAmount(),
|
||||
GrantedAtMs: time.Now().UTC().UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
@ -48,42 +48,21 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
||||
if r == nil || r.db == nil {
|
||||
return xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
if err := r.ensureLuckyDrawPoolID(ctx); err != nil {
|
||||
if err := r.ensureActivityOutboxDeliveryColumns(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureLuckyDrawVisibleRegion(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureLuckyGiftOutboxColumns(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureLuckyGiftOutboxIndexes(ctx); err != nil {
|
||||
if err := r.ensureActivityOutboxDeliveryIndexes(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureBroadcastOutboxIndexes(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureLuckyDrawRewardSettlementColumns(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureLuckyDrawPoolStatsTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureLuckyGiftRuleVersionTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureWeeklyStarTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureAgencyOpeningTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureLuckyUserStateFlowColumns(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureDefaultLuckyGiftRules(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureWheelTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -102,9 +81,32 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
||||
if err := r.ensureTaskDefinitionMetadataColumns(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureTaskRewardPolicyTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureTaskRewardPolicyTables(ctx context.Context) error {
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS task_reward_policy_instances (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
instance_code VARCHAR(96) NOT NULL COMMENT '后台策略实例编码',
|
||||
template_code VARCHAR(96) NOT NULL COMMENT '模板编码快照',
|
||||
template_version VARCHAR(64) NOT NULL COMMENT '模板版本快照',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
|
||||
reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '任务默认奖励资产:COIN/POINT',
|
||||
rule_json JSON NOT NULL COMMENT '完整策略快照',
|
||||
published_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '发布管理员 ID',
|
||||
published_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '发布时间,UTC epoch ms',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, instance_code),
|
||||
KEY idx_task_reward_policy_active (app_code, status, published_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务奖励运行侧策略实例表'`)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) ensureTaskDefinitionMetadataColumns(ctx context.Context) error {
|
||||
// 本地和测试环境会走自动迁移,逐列检查避免旧库重复启动时报 duplicate column;线上仍可用 deploy/mysql/migrations 的显式 SQL。
|
||||
additions := []struct {
|
||||
@ -118,6 +120,7 @@ func (r *Repository) ensureTaskDefinitionMetadataColumns(ctx context.Context) er
|
||||
{"action_param", `ALTER TABLE task_definitions ADD COLUMN action_param VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'App 跳转参数' AFTER action_type`},
|
||||
{"action_payload_json", `ALTER TABLE task_definitions ADD COLUMN action_payload_json JSON NULL COMMENT 'App 跳转扩展 JSON' AFTER action_param`},
|
||||
{"dimension_filter_json", `ALTER TABLE task_definitions ADD COLUMN dimension_filter_json JSON NULL COMMENT '任务事件维度过滤 JSON' AFTER action_payload_json`},
|
||||
{"reward_asset_type", `ALTER TABLE task_definitions ADD COLUMN reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型:COIN/POINT' AFTER reward_coin_amount`},
|
||||
}
|
||||
for _, addition := range additions {
|
||||
exists, err := r.columnExists(ctx, "task_definitions", addition.column)
|
||||
@ -131,6 +134,40 @@ func (r *Repository) ensureTaskDefinitionMetadataColumns(ctx context.Context) er
|
||||
}
|
||||
}
|
||||
}
|
||||
progressAdditions := []struct {
|
||||
column string
|
||||
sql string
|
||||
}{
|
||||
{"reward_asset_type", `ALTER TABLE user_task_progress ADD COLUMN reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型快照:COIN/POINT' AFTER reward_coin_amount`},
|
||||
}
|
||||
for _, addition := range progressAdditions {
|
||||
exists, err := r.columnExists(ctx, "user_task_progress", addition.column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
claimAdditions := []struct {
|
||||
column string
|
||||
sql string
|
||||
}{
|
||||
{"reward_asset_type", `ALTER TABLE task_reward_claims ADD COLUMN reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型快照:COIN/POINT' AFTER reward_coin_amount`},
|
||||
}
|
||||
for _, addition := range claimAdditions {
|
||||
exists, err := r.columnExists(ctx, "task_reward_claims", addition.column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
eventAdditions := []struct {
|
||||
column string
|
||||
sql string
|
||||
@ -154,102 +191,6 @@ func (r *Repository) ensureTaskDefinitionMetadataColumns(ctx context.Context) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureLuckyDrawPoolStatsTables(ctx context.Context) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE IF NOT EXISTS lucky_draw_pool_stats (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
gift_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '礼物 ID,空值表示奖池汇总',
|
||||
total_draws BIGINT NOT NULL DEFAULT 0 COMMENT '抽奖次数',
|
||||
unique_users BIGINT NOT NULL DEFAULT 0 COMMENT '参与用户数',
|
||||
unique_rooms BIGINT NOT NULL DEFAULT 0 COMMENT '参与房间数',
|
||||
total_spent_coins BIGINT NOT NULL DEFAULT 0 COMMENT '消耗金币',
|
||||
total_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '返还金币',
|
||||
base_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '基础 RTP 返奖',
|
||||
room_atmosphere_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '房间气氛支出',
|
||||
activity_subsidy_coins BIGINT NOT NULL DEFAULT 0 COMMENT '活动补贴支出',
|
||||
pending_draws BIGINT NOT NULL DEFAULT 0 COMMENT '待发放数量',
|
||||
granted_draws BIGINT NOT NULL DEFAULT 0 COMMENT '已发放数量',
|
||||
failed_draws BIGINT NOT NULL DEFAULT 0 COMMENT '发放失败数量',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, pool_id, gift_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物池级汇总统计'`,
|
||||
`CREATE TABLE IF NOT EXISTS lucky_draw_pool_day_stats (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
stat_day DATE NOT NULL COMMENT 'UTC 统计日',
|
||||
visible_region_id BIGINT NOT NULL DEFAULT 0 COMMENT '房间可见区域 ID',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
gift_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '礼物 ID,空值表示奖池汇总',
|
||||
draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '抽奖次数',
|
||||
turnover_coin BIGINT NOT NULL DEFAULT 0 COMMENT '消耗金币',
|
||||
payout_coin BIGINT NOT NULL DEFAULT 0 COMMENT '用户可见返奖金币',
|
||||
base_reward_coin BIGINT NOT NULL DEFAULT 0 COMMENT '基础 RTP 返奖',
|
||||
room_atmosphere_reward_coin BIGINT NOT NULL DEFAULT 0 COMMENT '房间气氛支出',
|
||||
activity_subsidy_coin BIGINT NOT NULL DEFAULT 0 COMMENT '活动补贴支出',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, stat_day, visible_region_id, pool_id, gift_id),
|
||||
KEY idx_lucky_draw_pool_day_overview (app_code, stat_day, pool_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物 Databi 日维度汇总'`,
|
||||
`CREATE TABLE IF NOT EXISTS lucky_draw_pool_stat_users (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
gift_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '礼物 ID,空值表示奖池汇总',
|
||||
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, pool_id, gift_id, user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物汇总用户去重'`,
|
||||
`CREATE TABLE IF NOT EXISTS lucky_draw_pool_stat_rooms (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
gift_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '礼物 ID,空值表示奖池汇总',
|
||||
room_id VARCHAR(96) NOT NULL COMMENT '房间 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, pool_id, gift_id, room_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物汇总房间去重'`,
|
||||
`CREATE TABLE IF NOT EXISTS lucky_draw_pool_stat_cursors (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
cursor_name VARCHAR(64) NOT NULL COMMENT '游标名称',
|
||||
last_draw_created_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '最后已聚合 draw 创建时间',
|
||||
last_draw_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '同一毫秒内最后已聚合 draw ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, cursor_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物后台统计快照游标'`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := r.db.ExecContext(ctx, statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureLuckyUserStateFlowColumns(ctx context.Context) error {
|
||||
// 用户阶段改按金币流水折算等价抽数,状态表必须持久保存累计流水和连续未中奖次数,不能再只依赖 paid_draws。
|
||||
additions := []struct {
|
||||
name string
|
||||
sql string
|
||||
}{
|
||||
{"cumulative_wager_coins", `ALTER TABLE lucky_user_states ADD COLUMN cumulative_wager_coins BIGINT NOT NULL DEFAULT 0 COMMENT '该用户在当前奖池累计消耗金币' AFTER paid_draws`},
|
||||
{"equivalent_draws", `ALTER TABLE lucky_user_states ADD COLUMN equivalent_draws BIGINT NOT NULL DEFAULT 0 COMMENT '累计金币按规则参考价格折算后的等价抽数' AFTER cumulative_wager_coins`},
|
||||
{"loss_streak", `ALTER TABLE lucky_user_states ADD COLUMN loss_streak BIGINT NOT NULL DEFAULT 0 COMMENT '连续未获得可见奖励次数,中奖后清零' AFTER equivalent_draws`},
|
||||
}
|
||||
for _, addition := range additions {
|
||||
exists, err := r.columnExists(ctx, "lucky_user_states", addition.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 释放 MySQL 连接池。
|
||||
func (r *Repository) Close() error {
|
||||
if r == nil || r.db == nil {
|
||||
@ -268,79 +209,7 @@ func (r *Repository) Ping(ctx context.Context) error {
|
||||
return r.db.PingContext(ctx)
|
||||
}
|
||||
|
||||
func (r *Repository) ensureLuckyDrawPoolID(ctx context.Context) error {
|
||||
const table = "lucky_draw_records"
|
||||
hasPoolID, err := r.columnExists(ctx, table, "pool_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasPoolID {
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
ALTER TABLE lucky_draw_records
|
||||
ADD COLUMN pool_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '幸运礼物奖池 ID' AFTER anchor_id`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := r.db.ExecContext(ctx, `UPDATE lucky_draw_records SET pool_id = gift_id WHERE pool_id = ''`); err != nil {
|
||||
return err
|
||||
}
|
||||
hasPoolIndex, err := r.indexExists(ctx, table, "idx_lucky_draw_pool")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasPoolIndex {
|
||||
if _, err := r.db.ExecContext(ctx, `CREATE INDEX idx_lucky_draw_pool ON lucky_draw_records (app_code, pool_id, created_at_ms)`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
indexes := []struct {
|
||||
name string
|
||||
sql string
|
||||
}{
|
||||
{"idx_lucky_draw_created", `CREATE INDEX idx_lucky_draw_created ON lucky_draw_records (app_code, created_at_ms, draw_id)`},
|
||||
{"idx_lucky_draw_pool_status_summary", `CREATE INDEX idx_lucky_draw_pool_status_summary ON lucky_draw_records (app_code, pool_id, reward_status, created_at_ms, draw_id)`},
|
||||
{"idx_lucky_draw_gift_status_summary", `CREATE INDEX idx_lucky_draw_gift_status_summary ON lucky_draw_records (app_code, pool_id, gift_id, reward_status, created_at_ms, draw_id)`},
|
||||
}
|
||||
for _, addition := range indexes {
|
||||
exists, err := r.indexExists(ctx, table, addition.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureLuckyDrawVisibleRegion(ctx context.Context) error {
|
||||
const table = "lucky_draw_records"
|
||||
hasVisibleRegion, err := r.columnExists(ctx, table, "visible_region_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasVisibleRegion {
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
ALTER TABLE lucky_draw_records
|
||||
ADD COLUMN visible_region_id BIGINT NOT NULL DEFAULT 0 COMMENT '房间可见区域 ID' AFTER anchor_id`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
hasIndex, err := r.indexExists(ctx, table, "idx_lucky_draw_region_created")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasIndex {
|
||||
if _, err := r.db.ExecContext(ctx, `CREATE INDEX idx_lucky_draw_region_created ON lucky_draw_records (app_code, visible_region_id, created_at_ms, draw_id)`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureLuckyGiftOutboxColumns(ctx context.Context) error {
|
||||
func (r *Repository) ensureActivityOutboxDeliveryColumns(ctx context.Context) error {
|
||||
additions := []struct {
|
||||
name string
|
||||
sql string
|
||||
@ -363,14 +232,14 @@ func (r *Repository) ensureLuckyGiftOutboxColumns(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureLuckyGiftOutboxIndexes(ctx context.Context) error {
|
||||
// LuckyGiftDrawn worker 的 pending 和 lock-expired 两条路径分开查,索引列顺序要和各自 WHERE 匹配。
|
||||
func (r *Repository) ensureActivityOutboxDeliveryIndexes(ctx context.Context) error {
|
||||
// activity_outbox 仍服务 activity-service 自己的异步事件;索引按 event_type 收敛,避免不同活动补偿 worker 互相扫全表。
|
||||
additions := []struct {
|
||||
name string
|
||||
sql string
|
||||
}{
|
||||
{"idx_activity_outbox_lucky_retry", `CREATE INDEX idx_activity_outbox_lucky_retry ON activity_outbox (app_code, event_type, status, next_retry_at_ms, created_at_ms, outbox_id)`},
|
||||
{"idx_activity_outbox_lucky_lock", `CREATE INDEX idx_activity_outbox_lucky_lock ON activity_outbox (app_code, event_type, status, lock_until_ms, created_at_ms, outbox_id)`},
|
||||
{"idx_activity_outbox_event_retry", `CREATE INDEX idx_activity_outbox_event_retry ON activity_outbox (app_code, event_type, status, next_retry_at_ms, created_at_ms, outbox_id)`},
|
||||
{"idx_activity_outbox_event_lock", `CREATE INDEX idx_activity_outbox_event_lock ON activity_outbox (app_code, event_type, status, lock_until_ms, created_at_ms, outbox_id)`},
|
||||
{"idx_activity_outbox_retention", `CREATE INDEX idx_activity_outbox_retention ON activity_outbox (app_code, status, updated_at_ms, outbox_id)`},
|
||||
}
|
||||
for _, addition := range additions {
|
||||
@ -408,184 +277,6 @@ func (r *Repository) ensureBroadcastOutboxIndexes(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureLuckyDrawRewardSettlementColumns(ctx context.Context) error {
|
||||
additions := []struct {
|
||||
name string
|
||||
sql string
|
||||
}{
|
||||
{"reward_transaction_id", `ALTER TABLE lucky_draw_records ADD COLUMN reward_transaction_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '返奖钱包交易 ID' AFTER reward_status`},
|
||||
{"reward_failure_reason", `ALTER TABLE lucky_draw_records ADD COLUMN reward_failure_reason VARCHAR(255) NOT NULL DEFAULT '' COMMENT '返奖失败原因' AFTER reward_transaction_id`},
|
||||
}
|
||||
for _, addition := range additions {
|
||||
exists, err := r.columnExists(ctx, "lucky_draw_records", addition.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureLuckyGiftRuleVersionTables(ctx context.Context) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE IF NOT EXISTS lucky_gift_rule_versions (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
rule_version BIGINT NOT NULL COMMENT '不可变规则版本',
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否启用',
|
||||
target_rtp_ppm BIGINT NOT NULL COMMENT '基础返奖目标,ppm',
|
||||
pool_rate_ppm BIGINT NOT NULL COMMENT '每笔扣费进入基础奖池比例,ppm',
|
||||
settlement_window_wager BIGINT NOT NULL COMMENT 'RTP 结算窗口流水,单位金币',
|
||||
control_band_ppm BIGINT NOT NULL COMMENT '正常波动带,ppm',
|
||||
gift_price_reference BIGINT NOT NULL COMMENT '配置参考礼物价格',
|
||||
novice_max_equivalent_draws BIGINT NOT NULL DEFAULT 2000 COMMENT '新手阶段截止等价抽数,按累计金币/参考价格折算',
|
||||
normal_max_equivalent_draws BIGINT NOT NULL DEFAULT 20000 COMMENT '正常阶段截止等价抽数,超过后进入高阶阶段',
|
||||
max_single_payout BIGINT NOT NULL DEFAULT 50000 COMMENT '单次基础返奖上限,按参考价格配置',
|
||||
user_hourly_payout_cap BIGINT NOT NULL DEFAULT 34200 COMMENT '单用户每小时基础返奖上限,按参考价格配置',
|
||||
user_daily_payout_cap BIGINT NOT NULL DEFAULT 615600 COMMENT '单用户每日基础返奖上限,按参考价格配置',
|
||||
device_daily_payout_cap BIGINT NOT NULL DEFAULT 1026000 COMMENT '单设备每日基础返奖上限,按参考价格配置',
|
||||
room_hourly_payout_cap BIGINT NOT NULL DEFAULT 684000 COMMENT '单房间每小时基础返奖上限,按参考价格配置',
|
||||
anchor_daily_payout_cap BIGINT NOT NULL DEFAULT 12312000 COMMENT '单主播每日基础返奖上限,按参考价格配置',
|
||||
effective_from_ms BIGINT NOT NULL COMMENT '生效时间,UTC epoch ms',
|
||||
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '发布人',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, pool_id, rule_version),
|
||||
KEY idx_lucky_gift_rule_versions_latest (app_code, pool_id, rule_version),
|
||||
KEY idx_lucky_gift_rule_versions_enabled (app_code, enabled, created_at_ms)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物 v2 规则版本表'`,
|
||||
`CREATE TABLE IF NOT EXISTS lucky_gift_stage_tiers (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
rule_version BIGINT NOT NULL COMMENT '规则版本',
|
||||
stage VARCHAR(32) NOT NULL COMMENT 'novice/normal/advanced',
|
||||
tier_id VARCHAR(96) NOT NULL COMMENT '奖档 ID',
|
||||
multiplier_ppm BIGINT NOT NULL COMMENT '倍率,1x = 1000000',
|
||||
base_weight_ppm BIGINT NOT NULL COMMENT '正常模式基础概率,ppm',
|
||||
reward_source VARCHAR(32) NOT NULL COMMENT 'base_rtp/activity_subsidy/presentation_only',
|
||||
high_water_only TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否高水位才开放',
|
||||
broadcast_level VARCHAR(32) NOT NULL DEFAULT 'none' COMMENT 'none/room/region/global',
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
PRIMARY KEY (app_code, pool_id, rule_version, stage, tier_id),
|
||||
KEY idx_lucky_gift_stage_tiers_stage (app_code, pool_id, rule_version, stage)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物 v2 阶段奖档表'`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := r.db.ExecContext(ctx, statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := r.ensureLuckyGiftRuleVersionStageThresholdColumns(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureLuckyGiftRuleVersionRiskCapColumns(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureLuckyGiftRuleVersionStageThresholdColumns(ctx context.Context) error {
|
||||
// 阶段阈值从硬编码抽数升级为“累计金币流水折算等价抽数”;旧开发库重启后必须自动补列,避免线上逻辑读到零阈值。
|
||||
additions := []struct {
|
||||
name string
|
||||
sql string
|
||||
}{
|
||||
{"novice_max_equivalent_draws", `ALTER TABLE lucky_gift_rule_versions ADD COLUMN novice_max_equivalent_draws BIGINT NOT NULL DEFAULT 2000 COMMENT '新手阶段截止等价抽数,按累计金币/参考价格折算' AFTER gift_price_reference`},
|
||||
{"normal_max_equivalent_draws", `ALTER TABLE lucky_gift_rule_versions ADD COLUMN normal_max_equivalent_draws BIGINT NOT NULL DEFAULT 20000 COMMENT '正常阶段截止等价抽数,超过后进入高阶阶段' AFTER novice_max_equivalent_draws`},
|
||||
}
|
||||
for _, addition := range additions {
|
||||
exists, err := r.columnExists(ctx, "lucky_gift_rule_versions", addition.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureLuckyGiftRuleVersionRiskCapColumns(ctx context.Context) error {
|
||||
// 开发库可能已经存在旧 v2 表;这里做幂等补列,保证服务重启后后台立即可以读写风控上限。
|
||||
additions := []struct {
|
||||
name string
|
||||
sql string
|
||||
}{
|
||||
{"max_single_payout", `ALTER TABLE lucky_gift_rule_versions ADD COLUMN max_single_payout BIGINT NOT NULL DEFAULT 50000 COMMENT '单次基础返奖上限,按参考价格配置' AFTER gift_price_reference`},
|
||||
{"user_hourly_payout_cap", `ALTER TABLE lucky_gift_rule_versions ADD COLUMN user_hourly_payout_cap BIGINT NOT NULL DEFAULT 34200 COMMENT '单用户每小时基础返奖上限,按参考价格配置' AFTER max_single_payout`},
|
||||
{"user_daily_payout_cap", `ALTER TABLE lucky_gift_rule_versions ADD COLUMN user_daily_payout_cap BIGINT NOT NULL DEFAULT 615600 COMMENT '单用户每日基础返奖上限,按参考价格配置' AFTER user_hourly_payout_cap`},
|
||||
{"device_daily_payout_cap", `ALTER TABLE lucky_gift_rule_versions ADD COLUMN device_daily_payout_cap BIGINT NOT NULL DEFAULT 1026000 COMMENT '单设备每日基础返奖上限,按参考价格配置' AFTER user_daily_payout_cap`},
|
||||
{"room_hourly_payout_cap", `ALTER TABLE lucky_gift_rule_versions ADD COLUMN room_hourly_payout_cap BIGINT NOT NULL DEFAULT 684000 COMMENT '单房间每小时基础返奖上限,按参考价格配置' AFTER device_daily_payout_cap`},
|
||||
{"anchor_daily_payout_cap", `ALTER TABLE lucky_gift_rule_versions ADD COLUMN anchor_daily_payout_cap BIGINT NOT NULL DEFAULT 12312000 COMMENT '单主播每日基础返奖上限,按参考价格配置' AFTER room_hourly_payout_cap`},
|
||||
}
|
||||
for _, addition := range additions {
|
||||
exists, err := r.columnExists(ctx, "lucky_gift_rule_versions", addition.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureDefaultLuckyGiftRules(ctx context.Context) error {
|
||||
// 本地和 Docker 开发库通常保留 MySQL volume;这里补齐 v2 lucky/super_lucky 契约,但不覆盖后台已发布版本。
|
||||
const seedNowMS int64 = 1779259000000
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
INSERT IGNORE INTO lucky_gift_rule_versions (
|
||||
app_code, pool_id, rule_version, enabled, target_rtp_ppm, pool_rate_ppm,
|
||||
settlement_window_wager, control_band_ppm, gift_price_reference,
|
||||
novice_max_equivalent_draws, normal_max_equivalent_draws,
|
||||
max_single_payout, user_hourly_payout_cap, user_daily_payout_cap, device_daily_payout_cap,
|
||||
room_hourly_payout_cap, anchor_daily_payout_cap, effective_from_ms, created_by_admin_id, created_at_ms
|
||||
) VALUES
|
||||
('lalu', 'lucky', 1, true, 950000, 960000, 1000000, 10000, 100, 2000, 20000, 50000, 34200, 615600, 1026000, 684000, 12312000, ?, 0, ?),
|
||||
('lalu', 'super_lucky', 1, true, 950000, 960000, 1000000, 10000, 100, 2000, 20000, 50000, 34200, 615600, 1026000, 684000, 12312000, ?, 0, ?)`,
|
||||
seedNowMS, seedNowMS, seedNowMS, seedNowMS,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
INSERT IGNORE INTO lucky_gift_stage_tiers (
|
||||
app_code, pool_id, rule_version, stage, tier_id, multiplier_ppm, base_weight_ppm,
|
||||
reward_source, high_water_only, broadcast_level, enabled
|
||||
) VALUES
|
||||
('lalu', 'lucky', 1, 'novice', 'novice_none', 0, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'novice', 'novice_0_5x', 500000, 200000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'novice', 'novice_1x', 1000000, 550000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'novice', 'novice_2x', 2000000, 150000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'normal', 'normal_none', 0, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'normal', 'normal_0_5x', 500000, 200000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'normal', 'normal_1x', 1000000, 550000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'normal', 'normal_2x', 2000000, 150000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'advanced', 'advanced_none', 0, 270000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'advanced', 'advanced_1x', 1000000, 600000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'advanced', 'advanced_2x', 2000000, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'lucky', 1, 'advanced', 'advanced_5x', 5000000, 30000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'novice', 'novice_none', 0, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'novice', 'novice_0_5x', 500000, 200000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'novice', 'novice_1x', 1000000, 550000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'novice', 'novice_2x', 2000000, 150000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'normal', 'normal_none', 0, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'normal', 'normal_0_5x', 500000, 200000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'normal', 'normal_1x', 1000000, 550000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'normal', 'normal_2x', 2000000, 150000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'advanced', 'advanced_none', 0, 270000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'advanced', 'advanced_1x', 1000000, 600000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'advanced', 'advanced_2x', 2000000, 100000, 'base_rtp', false, 'none', true),
|
||||
('lalu', 'super_lucky', 1, 'advanced', 'advanced_5x', 5000000, 30000, 'base_rtp', false, 'none', true)`)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) columnExists(ctx context.Context, table string, column string) (bool, error) {
|
||||
database, err := r.currentDatabase(ctx)
|
||||
if err != nil {
|
||||
|
||||
@ -7,26 +7,26 @@ import (
|
||||
"hyapp/internal/testutil/mysqlschema"
|
||||
)
|
||||
|
||||
func TestMigrateAddsLuckyDrawPoolIDToLegacyTable(t *testing.T) {
|
||||
func TestMigrateAddsActivityOutboxDeliveryColumnsToLegacyTable(t *testing.T) {
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hyapp_activity_migrate_test",
|
||||
})
|
||||
if _, err := schema.DB.ExecContext(context.Background(), `ALTER TABLE lucky_draw_records DROP INDEX idx_lucky_draw_pool`); err != nil {
|
||||
t.Fatalf("drop legacy pool index: %v", err)
|
||||
}
|
||||
if _, err := schema.DB.ExecContext(context.Background(), `ALTER TABLE lucky_draw_records DROP COLUMN pool_id`); err != nil {
|
||||
t.Fatalf("drop legacy pool column: %v", err)
|
||||
}
|
||||
if _, err := schema.DB.ExecContext(context.Background(), `ALTER TABLE lucky_draw_records DROP INDEX idx_lucky_draw_region_created`); err != nil {
|
||||
t.Fatalf("drop legacy region index: %v", err)
|
||||
}
|
||||
if _, err := schema.DB.ExecContext(context.Background(), `ALTER TABLE lucky_draw_records DROP COLUMN visible_region_id`); err != nil {
|
||||
t.Fatalf("drop legacy region column: %v", err)
|
||||
}
|
||||
if _, err := schema.DB.ExecContext(context.Background(), `DROP TABLE lucky_draw_pool_day_stats`); err != nil {
|
||||
t.Fatalf("drop legacy databi day stats table: %v", err)
|
||||
|
||||
// 旧库可能只保留最早的 activity_outbox 形态;迁移必须补齐锁字段和按 event_type 收敛的索引,
|
||||
// 否则不同 activity worker 会在同一张 outbox 上互相扫全表或重复抢任务。
|
||||
for _, statement := range []string{
|
||||
`ALTER TABLE activity_outbox DROP INDEX idx_activity_outbox_event_retry`,
|
||||
`ALTER TABLE activity_outbox DROP INDEX idx_activity_outbox_event_lock`,
|
||||
`ALTER TABLE activity_outbox DROP INDEX idx_activity_outbox_retention`,
|
||||
`ALTER TABLE activity_outbox DROP COLUMN last_error`,
|
||||
`ALTER TABLE activity_outbox DROP COLUMN lock_until_ms`,
|
||||
`ALTER TABLE activity_outbox DROP COLUMN locked_by`,
|
||||
} {
|
||||
if _, err := schema.DB.ExecContext(context.Background(), statement); err != nil {
|
||||
t.Fatalf("prepare legacy activity_outbox: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
@ -37,29 +37,14 @@ func TestMigrateAddsLuckyDrawPoolIDToLegacyTable(t *testing.T) {
|
||||
if err := repository.Migrate(context.Background()); err != nil {
|
||||
t.Fatalf("Migrate failed: %v", err)
|
||||
}
|
||||
if hasColumn, err := repository.columnExists(context.Background(), "lucky_draw_records", "pool_id"); err != nil || !hasColumn {
|
||||
t.Fatalf("pool_id column missing after migrate: has=%v err=%v", hasColumn, err)
|
||||
for _, column := range []string{"locked_by", "lock_until_ms", "last_error"} {
|
||||
if hasColumn, err := repository.columnExists(context.Background(), "activity_outbox", column); err != nil || !hasColumn {
|
||||
t.Fatalf("%s column missing after migrate: has=%v err=%v", column, hasColumn, err)
|
||||
}
|
||||
}
|
||||
if hasIndex, err := repository.indexExists(context.Background(), "lucky_draw_records", "idx_lucky_draw_pool"); err != nil || !hasIndex {
|
||||
t.Fatalf("idx_lucky_draw_pool missing after migrate: has=%v err=%v", hasIndex, err)
|
||||
}
|
||||
if hasColumn, err := repository.columnExists(context.Background(), "lucky_draw_records", "visible_region_id"); err != nil || !hasColumn {
|
||||
t.Fatalf("visible_region_id column missing after migrate: has=%v err=%v", hasColumn, err)
|
||||
}
|
||||
if hasIndex, err := repository.indexExists(context.Background(), "lucky_draw_records", "idx_lucky_draw_region_created"); err != nil || !hasIndex {
|
||||
t.Fatalf("idx_lucky_draw_region_created missing after migrate: has=%v err=%v", hasIndex, err)
|
||||
}
|
||||
if hasTable, err := activityTableExists(context.Background(), repository, "lucky_draw_pool_day_stats"); err != nil || !hasTable {
|
||||
t.Fatalf("lucky_draw_pool_day_stats table missing after migrate: has=%v err=%v", hasTable, err)
|
||||
for _, index := range []string{"idx_activity_outbox_event_retry", "idx_activity_outbox_event_lock", "idx_activity_outbox_retention"} {
|
||||
if hasIndex, err := repository.indexExists(context.Background(), "activity_outbox", index); err != nil || !hasIndex {
|
||||
t.Fatalf("%s missing after migrate: has=%v err=%v", index, hasIndex, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func activityTableExists(ctx context.Context, repository *Repository, tableName string) (bool, error) {
|
||||
var count int
|
||||
err := repository.db.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = ?`, tableName,
|
||||
).Scan(&count)
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ func (r *Repository) ListVisibleDefinitions(ctx context.Context, nowMS int64) ([
|
||||
audience_type, icon_key, icon_url, action_type, action_param,
|
||||
COALESCE(CAST(action_payload_json AS CHAR), '{}'),
|
||||
COALESCE(CAST(dimension_filter_json AS CHAR), '{}'),
|
||||
target_value, target_unit, reward_coin_amount, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, 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
|
||||
@ -59,7 +59,7 @@ func (r *Repository) ListUserProgress(ctx context.Context, userID int64, cycleKe
|
||||
}
|
||||
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
|
||||
target_value, reward_coin_amount, reward_asset_type, 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 {
|
||||
@ -171,7 +171,7 @@ func (r *Repository) ListTaskDefinitions(ctx context.Context, query taskdomain.D
|
||||
audience_type, icon_key, icon_url, action_type, action_param,
|
||||
COALESCE(CAST(action_payload_json AS CHAR), '{}'),
|
||||
COALESCE(CAST(dimension_filter_json AS CHAR), '{}'),
|
||||
target_value, target_unit, reward_coin_amount, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, 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+`
|
||||
@ -230,6 +230,7 @@ func (r *Repository) UpsertTaskDefinition(ctx context.Context, command taskdomai
|
||||
next.TargetValue = command.TargetValue
|
||||
next.TargetUnit = command.TargetUnit
|
||||
next.RewardCoinAmount = command.RewardCoinAmount
|
||||
next.RewardAssetType = command.RewardAssetType
|
||||
next.Status = command.Status
|
||||
next.SortOrder = command.SortOrder
|
||||
next.EffectiveFromMS = command.EffectiveFromMS
|
||||
@ -249,14 +250,14 @@ func (r *Repository) UpsertTaskDefinition(ctx context.Context, command taskdomai
|
||||
SET task_type = ?, category = ?, metric_type = ?, title = ?, description = ?,
|
||||
audience_type = ?, icon_key = ?, icon_url = ?, action_type = ?, action_param = ?,
|
||||
action_payload_json = CAST(? AS JSON), dimension_filter_json = CAST(? AS JSON),
|
||||
target_value = ?, target_unit = ?, reward_coin_amount = ?, status = ?, sort_order = ?,
|
||||
target_value = ?, target_unit = ?, reward_coin_amount = ?, reward_asset_type = ?, 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.AudienceType, next.IconKey, next.IconURL, next.ActionType, next.ActionParam,
|
||||
next.ActionPayloadJSON, next.DimensionFilterJSON,
|
||||
next.TargetValue, next.TargetUnit, next.RewardCoinAmount, next.Status, next.SortOrder,
|
||||
next.TargetValue, next.TargetUnit, next.RewardCoinAmount, next.RewardAssetType, next.Status, next.SortOrder,
|
||||
next.Version, next.CurrentVersionID, next.EffectiveFromMS, next.EffectiveToMS,
|
||||
next.UpdatedByAdminID, next.UpdatedAtMS, appcode.FromContext(ctx), next.TaskID,
|
||||
); err != nil {
|
||||
@ -358,6 +359,7 @@ func (r *Repository) PrepareTaskClaim(ctx context.Context, command taskdomain.Cl
|
||||
TaskType: command.TaskType,
|
||||
CycleKey: command.CycleKey,
|
||||
RewardCoinAmount: progress.RewardCoinAmount,
|
||||
RewardAssetType: progress.RewardAssetType,
|
||||
Status: taskdomain.ClaimStatusPending,
|
||||
CreatedAtMS: nowMS,
|
||||
UpdatedAtMS: nowMS,
|
||||
@ -366,10 +368,10 @@ func (r *Repository) PrepareTaskClaim(ctx context.Context, command taskdomain.Cl
|
||||
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, ?)`,
|
||||
reward_coin_amount, reward_asset_type, 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,
|
||||
claim.CycleKey, claim.RewardCoinAmount, claim.RewardAssetType, claim.WalletCommandID, claim.Status, claim.CreatedAtMS, claim.UpdatedAtMS,
|
||||
); err != nil {
|
||||
return taskdomain.Claim{}, err
|
||||
}
|
||||
@ -472,7 +474,7 @@ func (r *Repository) listDefinitionsForEvent(ctx context.Context, tx *sql.Tx, me
|
||||
audience_type, icon_key, icon_url, action_type, action_param,
|
||||
COALESCE(CAST(action_payload_json AS CHAR), '{}'),
|
||||
COALESCE(CAST(dimension_filter_json AS CHAR), '{}'),
|
||||
target_value, target_unit, reward_coin_amount, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, 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
|
||||
@ -506,10 +508,10 @@ func (r *Repository) applyTaskProgressDelta(ctx context.Context, tx *sql.Tx, use
|
||||
_, 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, ?)`,
|
||||
target_value, reward_coin_amount, reward_asset_type, 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,
|
||||
definition.TargetValue, definition.RewardCoinAmount, definition.RewardAssetType, status, completedAtMS, nowMS,
|
||||
)
|
||||
return err == nil, err
|
||||
}
|
||||
@ -553,6 +555,7 @@ func (r *Repository) createTaskDefinition(ctx context.Context, tx *sql.Tx, comma
|
||||
TargetValue: command.TargetValue,
|
||||
TargetUnit: command.TargetUnit,
|
||||
RewardCoinAmount: command.RewardCoinAmount,
|
||||
RewardAssetType: command.RewardAssetType,
|
||||
Status: command.Status,
|
||||
SortOrder: command.SortOrder,
|
||||
Version: 1,
|
||||
@ -572,15 +575,15 @@ func (r *Repository) createTaskDefinition(ctx context.Context, tx *sql.Tx, comma
|
||||
INSERT INTO task_definitions (
|
||||
app_code, task_id, task_type, category, metric_type, title, description,
|
||||
audience_type, icon_key, icon_url, action_type, action_param, action_payload_json, dimension_filter_json,
|
||||
target_value, target_unit, reward_coin_amount, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
definition.AppCode, definition.TaskID, definition.TaskType, definition.Category, definition.MetricType,
|
||||
definition.Title, definition.Description, definition.AudienceType, definition.IconKey, definition.IconURL,
|
||||
definition.ActionType, definition.ActionParam, definition.ActionPayloadJSON, definition.DimensionFilterJSON,
|
||||
definition.TargetValue, definition.TargetUnit,
|
||||
definition.RewardCoinAmount, definition.Status, definition.SortOrder, definition.Version,
|
||||
definition.RewardCoinAmount, definition.RewardAssetType, definition.Status, definition.SortOrder, definition.Version,
|
||||
definition.CurrentVersionID, definition.EffectiveFromMS, definition.EffectiveToMS,
|
||||
definition.CreatedByAdminID, definition.UpdatedByAdminID, definition.CreatedAtMS, definition.UpdatedAtMS,
|
||||
)
|
||||
@ -614,7 +617,7 @@ func (r *Repository) getTaskDefinition(ctx context.Context, taskID string) (task
|
||||
audience_type, icon_key, icon_url, action_type, action_param,
|
||||
COALESCE(CAST(action_payload_json AS CHAR), '{}'),
|
||||
COALESCE(CAST(dimension_filter_json AS CHAR), '{}'),
|
||||
target_value, target_unit, reward_coin_amount, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, 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
|
||||
@ -630,7 +633,7 @@ func (r *Repository) getTaskDefinitionForUpdate(ctx context.Context, tx *sql.Tx,
|
||||
audience_type, icon_key, icon_url, action_type, action_param,
|
||||
COALESCE(CAST(action_payload_json AS CHAR), '{}'),
|
||||
COALESCE(CAST(dimension_filter_json AS CHAR), '{}'),
|
||||
target_value, target_unit, reward_coin_amount, status, sort_order, version,
|
||||
target_value, target_unit, reward_coin_amount, reward_asset_type, 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
|
||||
@ -648,7 +651,7 @@ func (r *Repository) getTaskDefinitionForUpdate(ctx context.Context, tx *sql.Tx,
|
||||
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
|
||||
target_value, reward_coin_amount, reward_asset_type, 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`,
|
||||
@ -729,6 +732,7 @@ func scanDefinition(row rowScanner) (taskdomain.Definition, error) {
|
||||
&definition.TargetValue,
|
||||
&definition.TargetUnit,
|
||||
&definition.RewardCoinAmount,
|
||||
&definition.RewardAssetType,
|
||||
&definition.Status,
|
||||
&definition.SortOrder,
|
||||
&definition.Version,
|
||||
@ -754,6 +758,7 @@ func scanProgress(row rowScanner) (taskdomain.Progress, error) {
|
||||
&progress.ProgressValue,
|
||||
&progress.TargetValue,
|
||||
&progress.RewardCoinAmount,
|
||||
&progress.RewardAssetType,
|
||||
&progress.Status,
|
||||
&progress.CompletedAtMS,
|
||||
&progress.ClaimedAtMS,
|
||||
@ -772,6 +777,7 @@ func scanClaim(row rowScanner) (taskdomain.Claim, error) {
|
||||
&claim.TaskType,
|
||||
&claim.CycleKey,
|
||||
&claim.RewardCoinAmount,
|
||||
&claim.RewardAssetType,
|
||||
&claim.WalletCommandID,
|
||||
&claim.WalletTransactionID,
|
||||
&claim.Status,
|
||||
@ -783,7 +789,7 @@ func scanClaim(row rowScanner) (taskdomain.Claim, error) {
|
||||
}
|
||||
|
||||
func taskClaimSelectSQL() string {
|
||||
return `SELECT claim_id, command_id, user_id, task_id, task_type, cycle_key, reward_coin_amount,
|
||||
return `SELECT claim_id, command_id, user_id, task_id, task_type, cycle_key, reward_coin_amount, reward_asset_type,
|
||||
wallet_command_id, wallet_transaction_id, status, failure_reason, created_at_ms, updated_at_ms
|
||||
FROM task_reward_claims `
|
||||
}
|
||||
@ -820,7 +826,8 @@ func taskDefinitionNeedsVersion(current taskdomain.Definition, next taskdomain.D
|
||||
current.DimensionFilterJSON != next.DimensionFilterJSON ||
|
||||
current.TargetValue != next.TargetValue ||
|
||||
current.TargetUnit != next.TargetUnit ||
|
||||
current.RewardCoinAmount != next.RewardCoinAmount
|
||||
current.RewardCoinAmount != next.RewardCoinAmount ||
|
||||
current.RewardAssetType != next.RewardAssetType
|
||||
}
|
||||
|
||||
func dimensionFilterMatches(filterJSON string, dimensionsJSON string) (bool, error) {
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
taskdomain "hyapp/services/activity-service/internal/domain/task"
|
||||
)
|
||||
|
||||
func (r *Repository) GetActiveTaskRewardAssetType(ctx context.Context, nowMS int64) (string, bool, error) {
|
||||
row := r.db.QueryRowContext(ctx, `
|
||||
SELECT reward_asset_type
|
||||
FROM task_reward_policy_instances
|
||||
WHERE app_code = ? AND status = 'active'
|
||||
ORDER BY published_at_ms DESC, instance_code DESC
|
||||
LIMIT 1`,
|
||||
appcode.FromContext(ctx),
|
||||
)
|
||||
var assetType string
|
||||
if err := row.Scan(&assetType); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", false, nil
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
return assetType, true, nil
|
||||
}
|
||||
|
||||
func (r *Repository) PublishTaskRewardPolicy(ctx context.Context, command taskdomain.RewardPolicyCommand, nowMS int64) error {
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO task_reward_policy_instances (
|
||||
app_code, instance_code, template_code, template_version, status,
|
||||
reward_asset_type, rule_json, published_by_admin_id, published_at_ms,
|
||||
created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
template_code = VALUES(template_code),
|
||||
template_version = VALUES(template_version),
|
||||
status = VALUES(status),
|
||||
reward_asset_type = VALUES(reward_asset_type),
|
||||
rule_json = VALUES(rule_json),
|
||||
published_by_admin_id = VALUES(published_by_admin_id),
|
||||
published_at_ms = VALUES(published_at_ms),
|
||||
updated_at_ms = VALUES(updated_at_ms)`,
|
||||
appcode.FromContext(ctx),
|
||||
command.InstanceCode,
|
||||
command.TemplateCode,
|
||||
command.TemplateVersion,
|
||||
command.Status,
|
||||
command.RewardAssetType,
|
||||
command.RuleJSON,
|
||||
command.OperatorAdminID,
|
||||
command.PublishedAtMS,
|
||||
nowMS,
|
||||
nowMS,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@ -167,7 +167,7 @@ func (r *Repository) ExecuteWheelDraw(ctx context.Context, cmd domain.DrawComman
|
||||
results := make([]domain.DrawResult, 0, cmd.DrawCount)
|
||||
var totalPoolIn, totalPoolOut int64
|
||||
for i := int32(1); i <= cmd.DrawCount; i++ {
|
||||
unitSpent := luckyDrawUnitSpend(cmd.CoinSpent, cmd.DrawCount, i)
|
||||
unitSpent := wheelDrawUnitSpend(cmd.CoinSpent, cmd.DrawCount, i)
|
||||
totalPoolIn += unitSpent * config.PoolRatePPM / wheelPPMScale
|
||||
pool.Balance += unitSpent * config.PoolRatePPM / wheelPPMScale
|
||||
subCommand := wheelSubCommandID(cmd.CommandID, i, cmd.DrawCount)
|
||||
@ -679,7 +679,33 @@ func (r *Repository) insertWheelRewardOutbox(ctx context.Context, tx *sql.Tx, ap
|
||||
"visible_region_id": cmd.VisibleRegionID,
|
||||
"created_at_ms": nowMS,
|
||||
})
|
||||
return r.insertLuckyDrawOutbox(ctx, tx, appCode, "wheel_reward_"+result.DrawID, domain.EventTypeWheelRewardSettlement, payload, nowMS)
|
||||
return r.insertActivityOutbox(ctx, tx, appCode, "wheel_reward_"+result.DrawID, domain.EventTypeWheelRewardSettlement, payload, nowMS)
|
||||
}
|
||||
|
||||
func wheelDrawUnitSpend(totalSpent int64, drawCount int32, drawIndex int32) int64 {
|
||||
if drawCount <= 1 {
|
||||
return totalSpent
|
||||
}
|
||||
// 多连抽在同一扣费 receipt 下推进多次 RTP 观察;余数按前序 draw 分摊,保证每个子 draw 的金额之和严格等于钱包扣费事实。
|
||||
base := totalSpent / int64(drawCount)
|
||||
remainder := totalSpent % int64(drawCount)
|
||||
if int64(drawIndex) <= remainder {
|
||||
return base + 1
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func (r *Repository) insertActivityOutbox(ctx context.Context, tx *sql.Tx, appCode string, outboxID string, eventType string, payload []byte, nowMS int64) error {
|
||||
// activity_outbox 现在只承载 activity-service 自己的异步副作用;wheel 使用自己的 event_type,
|
||||
// 不能继续调用旧 lucky gift helper,否则会把已迁出的 lucky owner 边界重新引入 activity-service。
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO activity_outbox (
|
||||
app_code, outbox_id, event_type, payload, status, retry_count, next_retry_at_ms,
|
||||
locked_by, lock_until_ms, last_error, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, 'pending', 0, ?, '', 0, '', ?, ?)`,
|
||||
appCode, outboxID, eventType, payload, nowMS, nowMS, nowMS,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repository) collectWheelDrawsByCommand(ctx context.Context, tx *sql.Tx, appCode, commandID string, drawCount int32) ([]domain.DrawResult, bool, error) {
|
||||
|
||||
@ -1,314 +0,0 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
domain "hyapp/services/activity-service/internal/domain/luckygift"
|
||||
service "hyapp/services/activity-service/internal/service/luckygift"
|
||||
)
|
||||
|
||||
// LuckyGiftServer 暴露房间送礼主链路调用的幸运礼物查询和抽奖入口。
|
||||
type LuckyGiftServer struct {
|
||||
activityv1.UnimplementedLuckyGiftServiceServer
|
||||
|
||||
svc *service.Service
|
||||
}
|
||||
|
||||
func NewLuckyGiftServer(svc *service.Service) *LuckyGiftServer {
|
||||
return &LuckyGiftServer{svc: svc}
|
||||
}
|
||||
|
||||
func (s *LuckyGiftServer) CheckLuckyGift(ctx context.Context, req *activityv1.CheckLuckyGiftRequest) (*activityv1.CheckLuckyGiftResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
result, err := s.svc.Check(ctx, domain.CheckCommand{
|
||||
PoolID: req.GetPoolId(),
|
||||
GiftID: req.GetGiftId(),
|
||||
UserID: req.GetUserId(),
|
||||
RoomID: req.GetRoomId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.CheckLuckyGiftResponse{
|
||||
Enabled: result.Enabled,
|
||||
Reason: result.Reason,
|
||||
PoolId: result.PoolID,
|
||||
GiftId: result.GiftID,
|
||||
GiftPrice: result.GiftPrice,
|
||||
RuleVersion: result.RuleVersion,
|
||||
TargetRtpPpm: result.TargetRTPPPM,
|
||||
ExperiencePool: result.ExperiencePool,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *LuckyGiftServer) ExecuteLuckyGiftDraw(ctx context.Context, req *activityv1.ExecuteLuckyGiftDrawRequest) (*activityv1.ExecuteLuckyGiftDrawResponse, error) {
|
||||
meta := req.GetLuckyGift()
|
||||
ctx = appcode.WithContext(ctx, meta.GetMeta().GetAppCode())
|
||||
result, err := s.svc.Draw(ctx, luckyDrawCommandFromProto(meta))
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.ExecuteLuckyGiftDrawResponse{Result: luckyDrawResultToProto(result)}, nil
|
||||
}
|
||||
|
||||
func (s *LuckyGiftServer) BatchExecuteLuckyGiftDraw(ctx context.Context, req *activityv1.BatchExecuteLuckyGiftDrawRequest) (*activityv1.BatchExecuteLuckyGiftDrawResponse, error) {
|
||||
if len(req.GetLuckyGifts()) == 0 {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift batch draw commands are required"))
|
||||
}
|
||||
ctx = appcode.WithContext(ctx, req.GetLuckyGifts()[0].GetMeta().GetAppCode())
|
||||
cmds := make([]domain.DrawCommand, 0, len(req.GetLuckyGifts()))
|
||||
for _, meta := range req.GetLuckyGifts() {
|
||||
cmds = append(cmds, luckyDrawCommandFromProto(meta))
|
||||
}
|
||||
results, err := s.svc.DrawBatch(ctx, cmds)
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &activityv1.BatchExecuteLuckyGiftDrawResponse{Results: make([]*activityv1.LuckyGiftDrawResult, 0, len(results))}
|
||||
for _, result := range results {
|
||||
resp.Results = append(resp.Results, luckyDrawResultToProto(result))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func luckyDrawCommandFromProto(meta *activityv1.LuckyGiftMeta) domain.DrawCommand {
|
||||
return domain.DrawCommand{
|
||||
CommandID: meta.GetCommandId(),
|
||||
PoolID: meta.GetPoolId(),
|
||||
UserID: meta.GetUserId(),
|
||||
TargetUserID: meta.GetTargetUserId(),
|
||||
DeviceID: meta.GetDeviceId(),
|
||||
RoomID: meta.GetRoomId(),
|
||||
AnchorID: meta.GetAnchorId(),
|
||||
GiftID: meta.GetGiftId(),
|
||||
GiftCount: meta.GetGiftCount(),
|
||||
CoinSpent: meta.GetCoinSpent(),
|
||||
PaidAtMS: meta.GetPaidAtMs(),
|
||||
VisibleRegionID: meta.GetVisibleRegionId(),
|
||||
CountryID: meta.GetCountryId(),
|
||||
}
|
||||
}
|
||||
|
||||
// AdminLuckyGiftServer 暴露后台幸运礼物规则配置和抽奖审计入口。
|
||||
type AdminLuckyGiftServer struct {
|
||||
activityv1.UnimplementedAdminLuckyGiftServiceServer
|
||||
|
||||
svc *service.Service
|
||||
}
|
||||
|
||||
func NewAdminLuckyGiftServer(svc *service.Service) *AdminLuckyGiftServer {
|
||||
return &AdminLuckyGiftServer{svc: svc}
|
||||
}
|
||||
|
||||
func (s *AdminLuckyGiftServer) GetLuckyGiftConfig(ctx context.Context, req *activityv1.GetLuckyGiftConfigRequest) (*activityv1.GetLuckyGiftConfigResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
config, err := s.svc.GetConfig(ctx, req.GetPoolId())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.GetLuckyGiftConfigResponse{Config: luckyRuleConfigToProto(config)}, nil
|
||||
}
|
||||
|
||||
func (s *AdminLuckyGiftServer) UpsertLuckyGiftConfig(ctx context.Context, req *activityv1.UpsertLuckyGiftConfigRequest) (*activityv1.UpsertLuckyGiftConfigResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
config := luckyRuleConfigFromProto(req.GetConfig())
|
||||
config.CreatedByAdminID = req.GetOperatorAdminId()
|
||||
updated, err := s.svc.UpsertConfig(ctx, config)
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.UpsertLuckyGiftConfigResponse{Config: luckyRuleConfigToProto(updated)}, nil
|
||||
}
|
||||
|
||||
func (s *AdminLuckyGiftServer) ListLuckyGiftConfigs(ctx context.Context, req *activityv1.ListLuckyGiftConfigsRequest) (*activityv1.ListLuckyGiftConfigsResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
configs, err := s.svc.ListConfigs(ctx)
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &activityv1.ListLuckyGiftConfigsResponse{Configs: make([]*activityv1.LuckyGiftRuleConfig, 0, len(configs))}
|
||||
for _, config := range configs {
|
||||
resp.Configs = append(resp.Configs, luckyRuleConfigToProto(config))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *AdminLuckyGiftServer) ListLuckyGiftDraws(ctx context.Context, req *activityv1.ListLuckyGiftDrawsRequest) (*activityv1.ListLuckyGiftDrawsResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
draws, total, err := s.svc.ListDraws(ctx, domain.DrawQuery{
|
||||
GiftID: req.GetGiftId(),
|
||||
PoolID: req.GetPoolId(),
|
||||
UserID: req.GetUserId(),
|
||||
RoomID: req.GetRoomId(),
|
||||
Status: req.GetStatus(),
|
||||
Page: req.GetPage(),
|
||||
PageSize: req.GetPageSize(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &activityv1.ListLuckyGiftDrawsResponse{Draws: make([]*activityv1.LuckyGiftDrawResult, 0, len(draws)), Total: total}
|
||||
for _, draw := range draws {
|
||||
resp.Draws = append(resp.Draws, luckyDrawResultToProto(draw))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *AdminLuckyGiftServer) GetLuckyGiftDrawSummary(ctx context.Context, req *activityv1.GetLuckyGiftDrawSummaryRequest) (*activityv1.GetLuckyGiftDrawSummaryResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
summary, err := s.svc.GetDrawSummary(ctx, domain.DrawQuery{
|
||||
GiftID: req.GetGiftId(),
|
||||
PoolID: req.GetPoolId(),
|
||||
UserID: req.GetUserId(),
|
||||
RoomID: req.GetRoomId(),
|
||||
Status: req.GetStatus(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.GetLuckyGiftDrawSummaryResponse{Summary: luckyDrawSummaryToProto(summary)}, nil
|
||||
}
|
||||
|
||||
func luckyRuleConfigFromProto(config *activityv1.LuckyGiftRuleConfig) domain.RuleConfig {
|
||||
if config == nil {
|
||||
return domain.RuleConfig{}
|
||||
}
|
||||
stages := make([]domain.RuleStage, 0, len(config.GetStages()))
|
||||
for _, stage := range config.GetStages() {
|
||||
tiers := make([]domain.RuleTier, 0, len(stage.GetTiers()))
|
||||
for _, tier := range stage.GetTiers() {
|
||||
tiers = append(tiers, domain.RuleTier{
|
||||
Stage: tier.GetStage(),
|
||||
TierID: tier.GetTierId(),
|
||||
MultiplierPPM: tier.GetMultiplierPpm(),
|
||||
BaseWeightPPM: tier.GetBaseWeightPpm(),
|
||||
RewardSource: tier.GetRewardSource(),
|
||||
HighWaterOnly: tier.GetHighWaterOnly(),
|
||||
BroadcastLevel: tier.GetBroadcastLevel(),
|
||||
Enabled: tier.GetEnabled(),
|
||||
})
|
||||
}
|
||||
stages = append(stages, domain.RuleStage{
|
||||
Stage: stage.GetStage(),
|
||||
Tiers: tiers,
|
||||
})
|
||||
}
|
||||
return domain.RuleConfig{
|
||||
AppCode: config.GetAppCode(),
|
||||
PoolID: config.GetPoolId(),
|
||||
RuleVersion: config.GetRuleVersion(),
|
||||
Enabled: config.GetEnabled(),
|
||||
TargetRTPPPM: config.GetTargetRtpPpm(),
|
||||
PoolRatePPM: config.GetPoolRatePpm(),
|
||||
SettlementWindowWager: config.GetSettlementWindowWager(),
|
||||
ControlBandPPM: config.GetControlBandPpm(),
|
||||
GiftPriceReference: config.GetGiftPriceReference(),
|
||||
NoviceMaxEquivalentDraws: config.GetNoviceMaxEquivalentDraws(),
|
||||
NormalMaxEquivalentDraws: config.GetNormalMaxEquivalentDraws(),
|
||||
EffectiveFromMS: config.GetEffectiveFromMs(),
|
||||
CreatedByAdminID: config.GetCreatedByAdminId(),
|
||||
CreatedAtMS: config.GetCreatedAtMs(),
|
||||
MaxSinglePayout: config.GetMaxSinglePayout(),
|
||||
UserHourlyPayoutCap: config.GetUserHourlyPayoutCap(),
|
||||
UserDailyPayoutCap: config.GetUserDailyPayoutCap(),
|
||||
DeviceDailyPayoutCap: config.GetDeviceDailyPayoutCap(),
|
||||
RoomHourlyPayoutCap: config.GetRoomHourlyPayoutCap(),
|
||||
AnchorDailyPayoutCap: config.GetAnchorDailyPayoutCap(),
|
||||
Stages: stages,
|
||||
}
|
||||
}
|
||||
|
||||
func luckyRuleConfigToProto(config domain.RuleConfig) *activityv1.LuckyGiftRuleConfig {
|
||||
stages := make([]*activityv1.LuckyGiftRuleStage, 0, len(config.Stages))
|
||||
for _, stage := range config.Stages {
|
||||
tiers := make([]*activityv1.LuckyGiftRuleTier, 0, len(stage.Tiers))
|
||||
for _, tier := range stage.Tiers {
|
||||
tiers = append(tiers, &activityv1.LuckyGiftRuleTier{
|
||||
Stage: tier.Stage,
|
||||
TierId: tier.TierID,
|
||||
MultiplierPpm: tier.MultiplierPPM,
|
||||
BaseWeightPpm: tier.BaseWeightPPM,
|
||||
RewardSource: tier.RewardSource,
|
||||
HighWaterOnly: tier.HighWaterOnly,
|
||||
BroadcastLevel: tier.BroadcastLevel,
|
||||
Enabled: tier.Enabled,
|
||||
})
|
||||
}
|
||||
stages = append(stages, &activityv1.LuckyGiftRuleStage{
|
||||
Stage: stage.Stage,
|
||||
Tiers: tiers,
|
||||
})
|
||||
}
|
||||
return &activityv1.LuckyGiftRuleConfig{
|
||||
AppCode: config.AppCode,
|
||||
PoolId: config.PoolID,
|
||||
RuleVersion: config.RuleVersion,
|
||||
Enabled: config.Enabled,
|
||||
TargetRtpPpm: config.TargetRTPPPM,
|
||||
PoolRatePpm: config.PoolRatePPM,
|
||||
SettlementWindowWager: config.SettlementWindowWager,
|
||||
ControlBandPpm: config.ControlBandPPM,
|
||||
GiftPriceReference: config.GiftPriceReference,
|
||||
NoviceMaxEquivalentDraws: config.NoviceMaxEquivalentDraws,
|
||||
NormalMaxEquivalentDraws: config.NormalMaxEquivalentDraws,
|
||||
EffectiveFromMs: config.EffectiveFromMS,
|
||||
CreatedByAdminId: config.CreatedByAdminID,
|
||||
CreatedAtMs: config.CreatedAtMS,
|
||||
MaxSinglePayout: config.MaxSinglePayout,
|
||||
UserHourlyPayoutCap: config.UserHourlyPayoutCap,
|
||||
UserDailyPayoutCap: config.UserDailyPayoutCap,
|
||||
DeviceDailyPayoutCap: config.DeviceDailyPayoutCap,
|
||||
RoomHourlyPayoutCap: config.RoomHourlyPayoutCap,
|
||||
AnchorDailyPayoutCap: config.AnchorDailyPayoutCap,
|
||||
Stages: stages,
|
||||
}
|
||||
}
|
||||
|
||||
func luckyDrawResultToProto(result domain.DrawResult) *activityv1.LuckyGiftDrawResult {
|
||||
return &activityv1.LuckyGiftDrawResult{
|
||||
DrawId: result.DrawID,
|
||||
CommandId: result.CommandID,
|
||||
PoolId: result.PoolID,
|
||||
GiftId: result.GiftID,
|
||||
RuleVersion: result.RuleVersion,
|
||||
ExperiencePool: result.ExperiencePool,
|
||||
SelectedTierId: result.SelectedTierID,
|
||||
MultiplierPpm: result.MultiplierPPM,
|
||||
BaseRewardCoins: result.BaseRewardCoins,
|
||||
RoomAtmosphereRewardCoins: result.RoomAtmosphereRewardCoins,
|
||||
ActivitySubsidyCoins: result.ActivitySubsidyCoins,
|
||||
EffectiveRewardCoins: result.EffectiveRewardCoins,
|
||||
BudgetSourcesJson: result.BudgetSourcesJSON,
|
||||
RewardStatus: result.RewardStatus,
|
||||
RtpWindowIndex: result.RTPWindowIndex,
|
||||
GiftRtpWindowIndex: result.GiftRTPWindowIndex,
|
||||
GlobalBaseRtpPpm: result.GlobalBaseRTPPPM,
|
||||
GiftBaseRtpPpm: result.GiftBaseRTPPPM,
|
||||
StageFeedback: result.StageFeedback,
|
||||
HighMultiplier: result.HighMultiplier,
|
||||
CreatedAtMs: result.CreatedAtMS,
|
||||
WalletTransactionId: result.WalletTransactionID,
|
||||
CoinBalanceAfter: result.CoinBalanceAfter,
|
||||
}
|
||||
}
|
||||
|
||||
func luckyDrawSummaryToProto(summary domain.DrawSummary) *activityv1.LuckyGiftDrawSummary {
|
||||
return &activityv1.LuckyGiftDrawSummary{
|
||||
PoolId: summary.PoolID,
|
||||
TotalDraws: summary.TotalDraws,
|
||||
UniqueUsers: summary.UniqueUsers,
|
||||
UniqueRooms: summary.UniqueRooms,
|
||||
TotalSpentCoins: summary.TotalSpentCoins,
|
||||
TotalRewardCoins: summary.TotalRewardCoins,
|
||||
BaseRewardCoins: summary.BaseRewardCoins,
|
||||
RoomAtmosphereRewardCoins: summary.RoomAtmosphereRewardCoins,
|
||||
ActivitySubsidyCoins: summary.ActivitySubsidyCoins,
|
||||
ActualRtpPpm: summary.ActualRTPPPM,
|
||||
PendingDraws: summary.PendingDraws,
|
||||
GrantedDraws: summary.GrantedDraws,
|
||||
FailedDraws: summary.FailedDraws,
|
||||
}
|
||||
}
|
||||
@ -124,6 +124,7 @@ func (s *AdminTaskServer) UpsertTaskDefinition(ctx context.Context, req *activit
|
||||
TargetValue: req.GetTargetValue(),
|
||||
TargetUnit: req.GetTargetUnit(),
|
||||
RewardCoinAmount: req.GetRewardCoinAmount(),
|
||||
RewardAssetType: req.GetRewardAssetType(),
|
||||
Status: req.GetStatus(),
|
||||
SortOrder: req.GetSortOrder(),
|
||||
EffectiveFromMS: req.GetEffectiveFromMs(),
|
||||
@ -146,6 +147,31 @@ func (s *AdminTaskServer) SetTaskDefinitionStatus(ctx context.Context, req *acti
|
||||
return &activityv1.SetTaskDefinitionStatusResponse{Task: definitionToProto(item)}, nil
|
||||
}
|
||||
|
||||
// PublishTaskRewardPolicy 写入 policy instance 编译后的 activity 运行侧任务奖励默认资产。
|
||||
func (s *AdminTaskServer) PublishTaskRewardPolicy(ctx context.Context, req *activityv1.PublishTaskRewardPolicyRequest) (*activityv1.PublishTaskRewardPolicyResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
command := taskdomain.RewardPolicyCommand{
|
||||
InstanceCode: req.GetInstanceCode(),
|
||||
TemplateCode: req.GetTemplateCode(),
|
||||
TemplateVersion: req.GetTemplateVersion(),
|
||||
Status: req.GetStatus(),
|
||||
RewardAssetType: req.GetRewardAssetType(),
|
||||
RuleJSON: req.GetRuleJson(),
|
||||
OperatorAdminID: int64(req.GetOperatorAdminId()),
|
||||
PublishedAtMS: req.GetPublishedAtMs(),
|
||||
}
|
||||
if err := s.svc.PublishTaskRewardPolicy(ctx, command); err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.PublishTaskRewardPolicyResponse{
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
InstanceCode: command.InstanceCode,
|
||||
Status: command.Status,
|
||||
RewardAssetType: command.RewardAssetType,
|
||||
PublishedAtMs: command.PublishedAtMS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func taskListToProto(result taskdomain.ListResult) *activityv1.ListUserTasksResponse {
|
||||
resp := &activityv1.ListUserTasksResponse{
|
||||
Sections: make([]*activityv1.TaskSection, 0, len(result.Sections)),
|
||||
@ -186,6 +212,7 @@ func itemToProto(item taskdomain.Item) *activityv1.TaskItem {
|
||||
TargetUnit: item.TargetUnit,
|
||||
ProgressValue: item.ProgressValue,
|
||||
RewardCoinAmount: item.RewardCoinAmount,
|
||||
RewardAssetType: item.RewardAssetType,
|
||||
Status: item.UserStatus,
|
||||
Claimable: item.Claimable,
|
||||
TaskDay: item.TaskDay,
|
||||
@ -203,6 +230,7 @@ func claimToProto(claim taskdomain.Claim) *activityv1.ClaimTaskRewardResponse {
|
||||
TaskType: claim.TaskType,
|
||||
TaskDay: claim.CycleKey,
|
||||
RewardCoinAmount: claim.RewardCoinAmount,
|
||||
RewardAssetType: claim.RewardAssetType,
|
||||
Status: claim.Status,
|
||||
WalletTransactionId: claim.WalletTransactionID,
|
||||
GrantedAtMs: claim.UpdatedAtMS,
|
||||
@ -228,6 +256,7 @@ func definitionToProto(item taskdomain.Definition) *activityv1.TaskDefinition {
|
||||
TargetValue: item.TargetValue,
|
||||
TargetUnit: item.TargetUnit,
|
||||
RewardCoinAmount: item.RewardCoinAmount,
|
||||
RewardAssetType: item.RewardAssetType,
|
||||
Status: item.Status,
|
||||
SortOrder: item.SortOrder,
|
||||
Version: item.Version,
|
||||
|
||||
@ -33,6 +33,7 @@ room_service_addr: "room-service:13001"
|
||||
user_service_addr: "user-service:13005"
|
||||
wallet_service_addr: "wallet-service:13004"
|
||||
activity_service_addr: "activity-service:13006"
|
||||
lucky_gift_service_addr: "lucky-gift-service:13013"
|
||||
game_service_addr: "game-service:13008"
|
||||
statistics:
|
||||
base_url: "http://statistics-service:13010"
|
||||
|
||||
@ -37,6 +37,7 @@ room_service_addr: "room-service.internal:13001"
|
||||
user_service_addr: "user-service.internal:13005"
|
||||
wallet_service_addr: "wallet-service.internal:13004"
|
||||
activity_service_addr: "activity-service.internal:13006"
|
||||
lucky_gift_service_addr: "lucky-gift-service.internal:13013"
|
||||
game_service_addr: "game-service.internal:13008"
|
||||
statistics:
|
||||
base_url: "http://statistics-service.internal:13010"
|
||||
|
||||
@ -33,6 +33,7 @@ room_service_addr: "127.0.0.1:13001"
|
||||
user_service_addr: "127.0.0.1:13005"
|
||||
wallet_service_addr: "127.0.0.1:13004"
|
||||
activity_service_addr: "127.0.0.1:13006"
|
||||
lucky_gift_service_addr: "127.0.0.1:13013"
|
||||
game_service_addr: "127.0.0.1:13008"
|
||||
statistics:
|
||||
base_url: "http://127.0.0.1:13010"
|
||||
|
||||
@ -29,18 +29,19 @@ import (
|
||||
|
||||
// App 装配 gateway 的 HTTP 入口。
|
||||
type App struct {
|
||||
server *http.Server
|
||||
listener net.Listener
|
||||
roomConn *grpc.ClientConn
|
||||
userConn *grpc.ClientConn
|
||||
walletConn *grpc.ClientConn
|
||||
activityConn *grpc.ClientConn
|
||||
gameConn *grpc.ClientConn
|
||||
appConfig *appconfig.MySQLReader
|
||||
withdrawal *financewithdrawal.MySQLWriter
|
||||
redisClose func() error
|
||||
health *healthcheck.State
|
||||
closeOnce sync.Once
|
||||
server *http.Server
|
||||
listener net.Listener
|
||||
roomConn *grpc.ClientConn
|
||||
userConn *grpc.ClientConn
|
||||
walletConn *grpc.ClientConn
|
||||
activityConn *grpc.ClientConn
|
||||
luckyGiftConn *grpc.ClientConn
|
||||
gameConn *grpc.ClientConn
|
||||
appConfig *appconfig.MySQLReader
|
||||
withdrawal *financewithdrawal.MySQLWriter
|
||||
redisClose func() error
|
||||
health *healthcheck.State
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
// New 初始化 gateway 应用。
|
||||
@ -74,12 +75,21 @@ func New(cfg config.Config) (*App, error) {
|
||||
_ = walletConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
luckyGiftConn, err := grpcclient.Dial(cfg.LuckyGiftServiceAddr, grpcConfig)
|
||||
if err != nil {
|
||||
_ = roomConn.Close()
|
||||
_ = userConn.Close()
|
||||
_ = walletConn.Close()
|
||||
_ = activityConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
gameConn, err := grpcclient.Dial(cfg.GameServiceAddr, grpcConfig)
|
||||
if err != nil {
|
||||
_ = roomConn.Close()
|
||||
_ = userConn.Close()
|
||||
_ = walletConn.Close()
|
||||
_ = activityConn.Close()
|
||||
_ = luckyGiftConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -109,7 +119,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
var cumulativeRechargeRewardClient client.CumulativeRechargeRewardClient = client.NewGRPCCumulativeRechargeRewardClient(activityConn)
|
||||
var inviteActivityRewardClient client.InviteActivityRewardClient = client.NewGRPCInviteActivityRewardClient(activityConn)
|
||||
var sevenDayCheckInClient client.SevenDayCheckInClient = client.NewGRPCSevenDayCheckInClient(activityConn)
|
||||
var luckyGiftClient client.LuckyGiftClient = client.NewGRPCLuckyGiftClient(activityConn)
|
||||
var luckyGiftClient client.LuckyGiftClient = client.NewGRPCLuckyGiftClient(luckyGiftConn)
|
||||
var roomTurnoverRewardClient client.RoomTurnoverRewardClient = client.NewGRPCRoomTurnoverRewardClient(activityConn)
|
||||
var weeklyStarClient client.WeeklyStarClient = client.NewGRPCWeeklyStarClient(activityConn)
|
||||
var cpWeeklyRankClient client.CPWeeklyRankClient = client.NewGRPCCPWeeklyRankClient(activityConn)
|
||||
@ -124,6 +134,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
_ = userConn.Close()
|
||||
_ = walletConn.Close()
|
||||
_ = activityConn.Close()
|
||||
_ = luckyGiftConn.Close()
|
||||
_ = gameConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
@ -136,6 +147,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
_ = userConn.Close()
|
||||
_ = walletConn.Close()
|
||||
_ = activityConn.Close()
|
||||
_ = luckyGiftConn.Close()
|
||||
_ = gameConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
@ -151,6 +163,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
_ = userConn.Close()
|
||||
_ = walletConn.Close()
|
||||
_ = activityConn.Close()
|
||||
_ = luckyGiftConn.Close()
|
||||
_ = gameConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
@ -168,6 +181,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
_ = userConn.Close()
|
||||
_ = walletConn.Close()
|
||||
_ = activityConn.Close()
|
||||
_ = luckyGiftConn.Close()
|
||||
_ = gameConn.Close()
|
||||
}
|
||||
handler := httptransport.NewHandlerWithConfig(roomClient, roomGuardClient, userClient, userIdentityClient, httptransport.TencentIMConfig{
|
||||
@ -290,6 +304,7 @@ func New(cfg config.Config) (*App, error) {
|
||||
healthcheck.GRPCDependency{Name: "user_grpc", Conn: userConn},
|
||||
healthcheck.GRPCDependency{Name: "wallet_grpc", Conn: walletConn},
|
||||
healthcheck.GRPCDependency{Name: "activity_grpc", Conn: activityConn},
|
||||
healthcheck.GRPCDependency{Name: "lucky_gift_grpc", Conn: luckyGiftConn},
|
||||
healthcheck.GRPCDependency{Name: "game_grpc", Conn: gameConn},
|
||||
)
|
||||
|
||||
@ -318,17 +333,18 @@ func New(cfg config.Config) (*App, error) {
|
||||
}
|
||||
|
||||
return &App{
|
||||
server: server,
|
||||
listener: listener,
|
||||
roomConn: roomConn,
|
||||
userConn: userConn,
|
||||
walletConn: walletConn,
|
||||
activityConn: activityConn,
|
||||
gameConn: gameConn,
|
||||
appConfig: appConfigReader,
|
||||
withdrawal: withdrawalWriter,
|
||||
redisClose: redisClose,
|
||||
health: healthState,
|
||||
server: server,
|
||||
listener: listener,
|
||||
roomConn: roomConn,
|
||||
userConn: userConn,
|
||||
walletConn: walletConn,
|
||||
activityConn: activityConn,
|
||||
luckyGiftConn: luckyGiftConn,
|
||||
gameConn: gameConn,
|
||||
appConfig: appConfigReader,
|
||||
withdrawal: withdrawalWriter,
|
||||
redisClose: redisClose,
|
||||
health: healthState,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -490,6 +506,9 @@ func (a *App) Close() error {
|
||||
if a.activityConn != nil {
|
||||
err = errors.Join(err, a.activityConn.Close())
|
||||
}
|
||||
if a.luckyGiftConn != nil {
|
||||
err = errors.Join(err, a.luckyGiftConn.Close())
|
||||
}
|
||||
if a.gameConn != nil {
|
||||
err = errors.Join(err, a.gameConn.Close())
|
||||
}
|
||||
|
||||
@ -4,22 +4,23 @@ import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
|
||||
)
|
||||
|
||||
// LuckyGiftClient abstracts gateway access to activity-service lucky gift eligibility checks.
|
||||
// LuckyGiftClient abstracts gateway access to lucky-gift-service eligibility checks.
|
||||
// gateway-service 只做 App HTTP 协议转换;幸运礼物规则、RTP 和奖池仍由 lucky-gift-service 独占。
|
||||
type LuckyGiftClient interface {
|
||||
CheckLuckyGift(ctx context.Context, req *activityv1.CheckLuckyGiftRequest) (*activityv1.CheckLuckyGiftResponse, error)
|
||||
CheckLuckyGift(ctx context.Context, req *luckygiftv1.CheckLuckyGiftRequest) (*luckygiftv1.CheckLuckyGiftResponse, error)
|
||||
}
|
||||
|
||||
type grpcLuckyGiftClient struct {
|
||||
client activityv1.LuckyGiftServiceClient
|
||||
client luckygiftv1.LuckyGiftServiceClient
|
||||
}
|
||||
|
||||
func NewGRPCLuckyGiftClient(conn *grpc.ClientConn) LuckyGiftClient {
|
||||
return &grpcLuckyGiftClient{client: activityv1.NewLuckyGiftServiceClient(conn)}
|
||||
return &grpcLuckyGiftClient{client: luckygiftv1.NewLuckyGiftServiceClient(conn)}
|
||||
}
|
||||
|
||||
func (c *grpcLuckyGiftClient) CheckLuckyGift(ctx context.Context, req *activityv1.CheckLuckyGiftRequest) (*activityv1.CheckLuckyGiftResponse, error) {
|
||||
func (c *grpcLuckyGiftClient) CheckLuckyGift(ctx context.Context, req *luckygiftv1.CheckLuckyGiftRequest) (*luckygiftv1.CheckLuckyGiftResponse, error) {
|
||||
return c.client.CheckLuckyGift(ctx, req)
|
||||
}
|
||||
|
||||
@ -204,6 +204,14 @@ func (c *grpcWalletClient) ReleaseSalaryWithdrawal(ctx context.Context, req *wal
|
||||
return c.client.ReleaseSalaryWithdrawal(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcWalletClient) FreezePointWithdrawal(ctx context.Context, req *walletv1.FreezePointWithdrawalRequest) (*walletv1.FreezePointWithdrawalResponse, error) {
|
||||
return c.client.FreezePointWithdrawal(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcWalletClient) ReleasePointWithdrawal(ctx context.Context, req *walletv1.ReleasePointWithdrawalRequest) (*walletv1.ReleasePointWithdrawalResponse, error) {
|
||||
return c.client.ReleasePointWithdrawal(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcWalletClient) ListResources(ctx context.Context, req *walletv1.ListResourcesRequest) (*walletv1.ListResourcesResponse, error) {
|
||||
return c.client.ListResources(ctx, req)
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ type Config struct {
|
||||
UserServiceAddr string `yaml:"user_service_addr"`
|
||||
WalletServiceAddr string `yaml:"wallet_service_addr"`
|
||||
ActivityServiceAddr string `yaml:"activity_service_addr"`
|
||||
LuckyGiftServiceAddr string `yaml:"lucky_gift_service_addr"`
|
||||
GameServiceAddr string `yaml:"game_service_addr"`
|
||||
Statistics StatisticsConfig `yaml:"statistics"`
|
||||
GRPCClient GRPCClientConfig `yaml:"grpc_client"`
|
||||
@ -307,12 +308,13 @@ func Default() Config {
|
||||
AllowCredentials: true,
|
||||
MaxAgeSec: 600,
|
||||
},
|
||||
JWTSecret: "gateway-dev-secret",
|
||||
RoomServiceAddr: "127.0.0.1:13001",
|
||||
UserServiceAddr: "127.0.0.1:13005",
|
||||
WalletServiceAddr: "127.0.0.1:13004",
|
||||
ActivityServiceAddr: "127.0.0.1:13006",
|
||||
GameServiceAddr: "127.0.0.1:13008",
|
||||
JWTSecret: "gateway-dev-secret",
|
||||
RoomServiceAddr: "127.0.0.1:13001",
|
||||
UserServiceAddr: "127.0.0.1:13005",
|
||||
WalletServiceAddr: "127.0.0.1:13004",
|
||||
ActivityServiceAddr: "127.0.0.1:13006",
|
||||
LuckyGiftServiceAddr: "127.0.0.1:13013",
|
||||
GameServiceAddr: "127.0.0.1:13008",
|
||||
Statistics: StatisticsConfig{
|
||||
BaseURL: "http://127.0.0.1:13010",
|
||||
Timeout: 2 * time.Second,
|
||||
@ -437,6 +439,7 @@ func (cfg *Config) Normalize() error {
|
||||
cfg.UserServiceAddr = strings.TrimSpace(cfg.UserServiceAddr)
|
||||
cfg.WalletServiceAddr = strings.TrimSpace(cfg.WalletServiceAddr)
|
||||
cfg.ActivityServiceAddr = strings.TrimSpace(cfg.ActivityServiceAddr)
|
||||
cfg.LuckyGiftServiceAddr = strings.TrimSpace(cfg.LuckyGiftServiceAddr)
|
||||
cfg.GameServiceAddr = strings.TrimSpace(cfg.GameServiceAddr)
|
||||
cfg.Statistics.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.Statistics.BaseURL), "/")
|
||||
cfg.applyFinanceNotificationEnvOverrides()
|
||||
@ -446,6 +449,9 @@ func (cfg *Config) Normalize() error {
|
||||
if cfg.ActivityServiceAddr == "" {
|
||||
cfg.ActivityServiceAddr = "127.0.0.1:13006"
|
||||
}
|
||||
if cfg.LuckyGiftServiceAddr == "" {
|
||||
cfg.LuckyGiftServiceAddr = "127.0.0.1:13013"
|
||||
}
|
||||
if cfg.GameServiceAddr == "" {
|
||||
cfg.GameServiceAddr = "127.0.0.1:13008"
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
mysqlDriver "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -97,6 +97,13 @@ func (w *MySQLWriter) CreateApplication(ctx context.Context, command CreateAppli
|
||||
return Application{}, err
|
||||
}
|
||||
|
||||
if existing, found, err := w.getApplicationByFreezeCommand(ctx, command.AppCode, command.FreezeCommandID); err != nil || found {
|
||||
if err != nil {
|
||||
return Application{}, err
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
result, err := w.db.ExecContext(ctx, `
|
||||
INSERT INTO admin_user_withdrawal_applications
|
||||
(app_code, user_id, salary_asset_type, withdraw_amount, withdraw_amount_minor, withdraw_method, withdraw_address, freeze_command_id, freeze_transaction_id, status, created_at_ms, updated_at_ms)
|
||||
@ -116,6 +123,14 @@ VALUES
|
||||
command.CreatedAtMS,
|
||||
)
|
||||
if err != nil {
|
||||
if isDuplicateWithdrawalFreezeCommand(err) {
|
||||
if existing, found, lookupErr := w.getApplicationByFreezeCommand(ctx, command.AppCode, command.FreezeCommandID); lookupErr != nil || found {
|
||||
if lookupErr != nil {
|
||||
return Application{}, lookupErr
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
}
|
||||
return Application{}, err
|
||||
}
|
||||
id, err := result.LastInsertId()
|
||||
@ -139,6 +154,49 @@ VALUES
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *MySQLWriter) getApplicationByFreezeCommand(ctx context.Context, appCode string, freezeCommandID string) (Application, bool, error) {
|
||||
row := w.db.QueryRowContext(ctx, `
|
||||
SELECT id, app_code, user_id, salary_asset_type, withdraw_amount, withdraw_amount_minor,
|
||||
withdraw_method, withdraw_address, freeze_command_id, freeze_transaction_id,
|
||||
status, created_at_ms, updated_at_ms
|
||||
FROM admin_user_withdrawal_applications
|
||||
WHERE app_code = ? AND freeze_command_id = ?
|
||||
LIMIT 1`, appCode, freezeCommandID)
|
||||
var item Application
|
||||
var userID string
|
||||
if err := row.Scan(
|
||||
&item.ID,
|
||||
&item.AppCode,
|
||||
&userID,
|
||||
&item.SalaryAssetType,
|
||||
&item.WithdrawAmount,
|
||||
&item.WithdrawAmountMinor,
|
||||
&item.WithdrawMethod,
|
||||
&item.WithdrawAddress,
|
||||
&item.FreezeCommandID,
|
||||
&item.FreezeTransactionID,
|
||||
&item.Status,
|
||||
&item.CreatedAtMS,
|
||||
&item.UpdatedAtMS,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return Application{}, false, nil
|
||||
}
|
||||
return Application{}, false, err
|
||||
}
|
||||
parsedUserID, err := parseUserID(userID)
|
||||
if err != nil {
|
||||
return Application{}, false, err
|
||||
}
|
||||
item.UserID = parsedUserID
|
||||
return item, true, nil
|
||||
}
|
||||
|
||||
func isDuplicateWithdrawalFreezeCommand(err error) bool {
|
||||
var mysqlErr *mysqlDriver.MySQLError
|
||||
return errors.As(err, &mysqlErr) && mysqlErr.Number == 1062
|
||||
}
|
||||
|
||||
func normalizeCreateApplicationCommand(command CreateApplicationCommand) CreateApplicationCommand {
|
||||
command.AppCode = strings.TrimSpace(command.AppCode)
|
||||
command.SalaryAssetType = strings.ToUpper(strings.TrimSpace(command.SalaryAssetType))
|
||||
@ -183,3 +241,12 @@ func validateCreateApplicationCommand(command CreateApplicationCommand) error {
|
||||
func formatUserID(userID int64) string {
|
||||
return fmt.Sprintf("%d", userID)
|
||||
}
|
||||
|
||||
func parseUserID(value string) (int64, error) {
|
||||
var userID int64
|
||||
_, err := fmt.Sscanf(strings.TrimSpace(value), "%d", &userID)
|
||||
if err != nil || userID <= 0 {
|
||||
return 0, errors.New("user_id is invalid")
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
@ -0,0 +1,146 @@
|
||||
package financewithdrawal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
)
|
||||
|
||||
func TestCreateApplicationRequiresFreezeCommandID(t *testing.T) {
|
||||
command := normalizeCreateApplicationCommand(CreateApplicationCommand{
|
||||
AppCode: "huwaa",
|
||||
UserID: 42001,
|
||||
SalaryAssetType: "point",
|
||||
WithdrawAmount: "9.50",
|
||||
WithdrawAmountMinor: 1_000_000,
|
||||
WithdrawAddress: "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
||||
FreezeTransactionID: "tx-freeze",
|
||||
CreatedAtMS: 1700000000000,
|
||||
})
|
||||
if err := validateCreateApplicationCommand(command); err == nil || !strings.Contains(err.Error(), "freeze_command_id") {
|
||||
t.Fatalf("missing freeze command id should be rejected, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateApplicationReturnsExistingByAppAndFreezeCommand(t *testing.T) {
|
||||
db, mock, closeDB := newWriterSQLMock(t)
|
||||
defer closeDB()
|
||||
writer := &MySQLWriter{db: db}
|
||||
|
||||
mock.ExpectQuery(`(?s)FROM admin_user_withdrawal_applications\s+WHERE app_code = \? AND freeze_command_id = \?`).
|
||||
WithArgs("huwaa", "cmd-point-freeze").
|
||||
WillReturnRows(withdrawalApplicationRows().AddRow(
|
||||
int64(77),
|
||||
"huwaa",
|
||||
"42001",
|
||||
"POINT",
|
||||
"9.50",
|
||||
int64(1000000),
|
||||
MethodUSDTTRC20,
|
||||
"TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
||||
"cmd-point-freeze",
|
||||
"tx-freeze",
|
||||
StatusPending,
|
||||
int64(1700000000000),
|
||||
int64(1700000000000),
|
||||
))
|
||||
|
||||
got, err := writer.CreateApplication(context.Background(), CreateApplicationCommand{
|
||||
AppCode: "huwaa",
|
||||
UserID: 42001,
|
||||
SalaryAssetType: "POINT",
|
||||
WithdrawAmount: "9.50",
|
||||
WithdrawAmountMinor: 1_000_000,
|
||||
WithdrawMethod: MethodUSDTTRC20,
|
||||
WithdrawAddress: "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
||||
FreezeCommandID: "cmd-point-freeze",
|
||||
FreezeTransactionID: "tx-new-should-not-insert",
|
||||
CreatedAtMS: 1700000009999,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateApplication existing lookup failed: %v", err)
|
||||
}
|
||||
if got.ID != 77 || got.AppCode != "huwaa" || got.UserID != 42001 || got.FreezeTransactionID != "tx-freeze" {
|
||||
t.Fatalf("existing withdrawal application mismatch: %+v", got)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateApplicationInsertsAfterAppScopedMiss(t *testing.T) {
|
||||
db, mock, closeDB := newWriterSQLMock(t)
|
||||
defer closeDB()
|
||||
writer := &MySQLWriter{db: db}
|
||||
|
||||
mock.ExpectQuery(`(?s)FROM admin_user_withdrawal_applications\s+WHERE app_code = \? AND freeze_command_id = \?`).
|
||||
WithArgs("huwaa", "cmd-point-freeze").
|
||||
WillReturnRows(withdrawalApplicationRows())
|
||||
mock.ExpectExec(`(?s)INSERT INTO admin_user_withdrawal_applications`).
|
||||
WithArgs(
|
||||
"huwaa",
|
||||
"42001",
|
||||
"POINT",
|
||||
"9.50",
|
||||
int64(1000000),
|
||||
MethodUSDTTRC20,
|
||||
"TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
||||
"cmd-point-freeze",
|
||||
"tx-freeze",
|
||||
StatusPending,
|
||||
int64(1700000000000),
|
||||
int64(1700000000000),
|
||||
).
|
||||
WillReturnResult(sqlmock.NewResult(88, 1))
|
||||
|
||||
got, err := writer.CreateApplication(context.Background(), CreateApplicationCommand{
|
||||
AppCode: "huwaa",
|
||||
UserID: 42001,
|
||||
SalaryAssetType: "point",
|
||||
WithdrawAmount: "9.50",
|
||||
WithdrawAmountMinor: 1_000_000,
|
||||
WithdrawAddress: "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
||||
FreezeCommandID: "cmd-point-freeze",
|
||||
FreezeTransactionID: "tx-freeze",
|
||||
CreatedAtMS: 1700000000000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateApplication insert failed: %v", err)
|
||||
}
|
||||
if got.ID != 88 || got.AppCode != "huwaa" || got.SalaryAssetType != "POINT" || got.WithdrawMethod != MethodUSDTTRC20 {
|
||||
t.Fatalf("inserted withdrawal application mismatch: %+v", got)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func withdrawalApplicationRows() *sqlmock.Rows {
|
||||
return sqlmock.NewRows([]string{
|
||||
"id",
|
||||
"app_code",
|
||||
"user_id",
|
||||
"salary_asset_type",
|
||||
"withdraw_amount",
|
||||
"withdraw_amount_minor",
|
||||
"withdraw_method",
|
||||
"withdraw_address",
|
||||
"freeze_command_id",
|
||||
"freeze_transaction_id",
|
||||
"status",
|
||||
"created_at_ms",
|
||||
"updated_at_ms",
|
||||
})
|
||||
}
|
||||
|
||||
func newWriterSQLMock(t *testing.T) (*sql.DB, sqlmock.Sqlmock, func()) {
|
||||
t.Helper()
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("create sql mock failed: %v", err)
|
||||
}
|
||||
return db, mock, func() { _ = db.Close() }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user