Split lucky gift service and ops center

This commit is contained in:
zhx 2026-07-08 22:30:46 +08:00
parent 779d864e80
commit 8affe63f6a
174 changed files with 28613 additions and 21842 deletions

View File

@ -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

View File

@ -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);

View File

@ -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"

View File

@ -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 (

View File

@ -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

File diff suppressed because it is too large Load Diff

View 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);
}

View 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

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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,

View File

@ -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",

View File

@ -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

View File

@ -4,4 +4,5 @@ use (
.
./api
./server/admin
./server/luck-gateway
)

View File

@ -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

View File

@ -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}"

View File

@ -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

View File

@ -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"

View File

@ -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),

View File

@ -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"

View File

@ -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"

View File

@ -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")
}

View File

@ -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)
}

View 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)
}

View File

@ -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)
}

View File

@ -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"`

View File

@ -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(),

View File

@ -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 {

View File

@ -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

View 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
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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)
}

View 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
}

View 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)
}
}

View 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)
}

View 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"`
}

View 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
}

View 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)
}

View 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(&regionID); 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
}

View 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() }
}

View File

@ -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{},

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)

View 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';

View File

@ -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;

View 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"]

View 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)
}
}

View 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

View 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=

View 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
}

View File

@ -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(),
}
}

View 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_mspool 由 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))
}

View 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
}

View 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))
}

View 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[:])
}

View 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)
}

View 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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 '是否启用',

View File

@ -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;

View File

@ -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='任务奖励运行侧策略实例表';

View File

@ -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)

View File

@ -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))

View File

@ -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,

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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")
}
}

View File

@ -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), &regionPayload); 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)
}
}

View File

@ -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{

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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) {

View File

@ -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,
}
}

View File

@ -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,

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -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