七日签到,幸运礼物

This commit is contained in:
zhx 2026-05-20 10:22:35 +08:00
parent b8ead156e6
commit 7499582c45
120 changed files with 24123 additions and 13686 deletions

File diff suppressed because it is too large Load Diff

View File

@ -531,6 +531,125 @@ message ListRegistrationRewardClaimsResponse {
int64 total = 2;
}
// SevenDayCheckInReward
message SevenDayCheckInReward {
int32 day_index = 1;
int64 resource_group_id = 2;
}
// SevenDayCheckInConfig App wallet-service
message SevenDayCheckInConfig {
string app_code = 1;
bool enabled = 2;
int64 config_version = 3;
repeated SevenDayCheckInReward rewards = 4;
int64 updated_by_admin_id = 5;
int64 created_at_ms = 6;
int64 updated_at_ms = 7;
}
// SevenDayCheckInRewardStatus App
message SevenDayCheckInRewardStatus {
int32 day_index = 1;
int64 resource_group_id = 2;
string status = 3;
string claim_id = 4;
string wallet_grant_id = 5;
int64 granted_at_ms = 6;
}
message GetSevenDayCheckInStatusRequest {
RequestMeta meta = 1;
int64 user_id = 2;
}
message GetSevenDayCheckInStatusResponse {
bool enabled = 1;
bool can_sign = 2;
string reason = 3;
int64 cycle_no = 4;
int32 current_day_index = 5;
int32 next_day_index = 6;
string checkin_day = 7;
string last_checkin_day = 8;
int64 config_version = 9;
repeated SevenDayCheckInRewardStatus rewards = 10;
int64 server_time_ms = 11;
int64 next_refresh_at_ms = 12;
}
message SignSevenDayCheckInRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string command_id = 3;
}
message SignSevenDayCheckInResponse {
string claim_id = 1;
int64 cycle_no = 2;
string checkin_day = 3;
int32 day_index = 4;
int64 resource_group_id = 5;
string status = 6;
string wallet_grant_id = 7;
int64 granted_at_ms = 8;
int64 server_time_ms = 9;
}
message GetSevenDayCheckInConfigRequest {
RequestMeta meta = 1;
}
message GetSevenDayCheckInConfigResponse {
SevenDayCheckInConfig config = 1;
}
message UpdateSevenDayCheckInConfigRequest {
RequestMeta meta = 1;
bool enabled = 2;
repeated SevenDayCheckInReward rewards = 3;
int64 operator_admin_id = 4;
}
message UpdateSevenDayCheckInConfigResponse {
SevenDayCheckInConfig config = 1;
}
// SevenDayCheckInClaim 使
message SevenDayCheckInClaim {
string claim_id = 1;
string command_id = 2;
int64 user_id = 3;
int64 cycle_no = 4;
string checkin_day = 5;
int32 day_index = 6;
int64 config_version = 7;
int64 resource_group_id = 8;
string wallet_command_id = 9;
string wallet_grant_id = 10;
string status = 11;
string failure_reason = 12;
int64 signed_at_ms = 13;
int64 granted_at_ms = 14;
int64 created_at_ms = 15;
int64 updated_at_ms = 16;
}
message ListSevenDayCheckInClaimsRequest {
RequestMeta meta = 1;
string status = 2;
int64 user_id = 3;
string checkin_day = 4;
int32 day_index = 5;
int32 page = 6;
int32 page_size = 7;
}
message ListSevenDayCheckInClaimsResponse {
repeated SevenDayCheckInClaim claims = 1;
int64 total = 2;
}
// LevelTrack wealth/game/charm
message LevelTrack {
string track = 1;
@ -877,6 +996,149 @@ message UpsertAchievementDefinitionResponse {
bool created = 2;
}
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;
}
message LuckyGiftTier {
string pool = 1;
string tier_id = 2;
int64 reward_coins = 3;
int64 weight = 4;
bool high_water_only = 5;
bool enabled = 6;
}
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;
}
message CheckLuckyGiftRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string room_id = 3;
string gift_id = 4;
}
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;
}
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;
}
message ExecuteLuckyGiftDrawRequest {
LuckyGiftMeta lucky_gift = 1;
}
message ExecuteLuckyGiftDrawResponse {
LuckyGiftDrawResult result = 1;
}
message GetLuckyGiftConfigRequest {
RequestMeta meta = 1;
string gift_id = 2;
}
message GetLuckyGiftConfigResponse {
LuckyGiftConfig config = 1;
}
message UpsertLuckyGiftConfigRequest {
RequestMeta meta = 1;
LuckyGiftConfig config = 2;
int64 operator_admin_id = 3;
}
message UpsertLuckyGiftConfigResponse {
LuckyGiftConfig config = 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;
}
message ListLuckyGiftDrawsResponse {
repeated LuckyGiftDrawResult draws = 1;
int64 total = 2;
}
// ActivityService RPC
service ActivityService {
rpc PingActivity(PingActivityRequest) returns (PingActivityResponse);
@ -924,6 +1186,12 @@ 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);
}
// BroadcastService owns server-side Tencent IM global/region broadcast delivery.
service BroadcastService {
rpc EnsureBroadcastGroups(EnsureBroadcastGroupsRequest) returns (EnsureBroadcastGroupsResponse);
@ -958,6 +1226,19 @@ service AdminRegistrationRewardService {
rpc ListRegistrationRewardClaims(ListRegistrationRewardClaimsRequest) returns (ListRegistrationRewardClaimsResponse);
}
// SevenDayCheckInService App
service SevenDayCheckInService {
rpc GetSevenDayCheckInStatus(GetSevenDayCheckInStatusRequest) returns (GetSevenDayCheckInStatusResponse);
rpc SignSevenDayCheckIn(SignSevenDayCheckInRequest) returns (SignSevenDayCheckInResponse);
}
// AdminSevenDayCheckInService 访
service AdminSevenDayCheckInService {
rpc GetSevenDayCheckInConfig(GetSevenDayCheckInConfigRequest) returns (GetSevenDayCheckInConfigResponse);
rpc UpdateSevenDayCheckInConfig(UpdateSevenDayCheckInConfigRequest) returns (UpdateSevenDayCheckInConfigResponse);
rpc ListSevenDayCheckInClaims(ListSevenDayCheckInClaimsRequest) returns (ListSevenDayCheckInClaimsResponse);
}
// AdminGrowthLevelService 访 activity-service
service AdminGrowthLevelService {
rpc ListLevelConfig(ListLevelConfigRequest) returns (ListLevelConfigResponse);
@ -971,3 +1252,10 @@ service AdminAchievementService {
rpc ListAchievementDefinitions(ListAchievementsRequest) returns (ListAchievementsResponse);
rpc UpsertAchievementDefinition(UpsertAchievementDefinitionRequest) returns (UpsertAchievementDefinitionResponse);
}
// 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 ListLuckyGiftDraws(ListLuckyGiftDrawsRequest) returns (ListLuckyGiftDrawsResponse);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.1
// protoc v4.25.3
// protoc-gen-go v1.36.11
// protoc v5.28.3
// source: proto/events/room/v1/events.proto
package roomeventsv1
@ -11,6 +11,7 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@ -23,17 +24,16 @@ const (
// EventEnvelope 是 room-service outbox 的统一事件外壳。
// body 保存具体事件消息序列化后的二进制event_type 负责告诉消费者如何解码。
type EventEnvelope struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
EventId string `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"`
RoomId string `protobuf:"bytes,2,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"`
EventType string `protobuf:"bytes,3,opt,name=event_type,json=eventType,proto3" json:"event_type,omitempty"`
RoomVersion int64 `protobuf:"varint,4,opt,name=room_version,json=roomVersion,proto3" json:"room_version,omitempty"`
OccurredAtMs int64 `protobuf:"varint,5,opt,name=occurred_at_ms,json=occurredAtMs,proto3" json:"occurred_at_ms,omitempty"`
Body []byte `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
AppCode string `protobuf:"bytes,7,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
unknownFields protoimpl.UnknownFields
EventId string `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"`
RoomId string `protobuf:"bytes,2,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"`
EventType string `protobuf:"bytes,3,opt,name=event_type,json=eventType,proto3" json:"event_type,omitempty"`
RoomVersion int64 `protobuf:"varint,4,opt,name=room_version,json=roomVersion,proto3" json:"room_version,omitempty"`
OccurredAtMs int64 `protobuf:"varint,5,opt,name=occurred_at_ms,json=occurredAtMs,proto3" json:"occurred_at_ms,omitempty"`
Body []byte `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
AppCode string `protobuf:"bytes,7,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *EventEnvelope) Reset() {
@ -117,17 +117,16 @@ func (x *EventEnvelope) GetAppCode() string {
// RoomCreated 表达房间第一次建立成功。
type RoomCreated struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
OwnerUserId int64 `protobuf:"varint,1,opt,name=owner_user_id,json=ownerUserId,proto3" json:"owner_user_id,omitempty"`
SeatCount int32 `protobuf:"varint,3,opt,name=seat_count,json=seatCount,proto3" json:"seat_count,omitempty"`
Mode string `protobuf:"bytes,4,opt,name=mode,proto3" json:"mode,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
OwnerUserId int64 `protobuf:"varint,1,opt,name=owner_user_id,json=ownerUserId,proto3" json:"owner_user_id,omitempty"`
SeatCount int32 `protobuf:"varint,3,opt,name=seat_count,json=seatCount,proto3" json:"seat_count,omitempty"`
Mode string `protobuf:"bytes,4,opt,name=mode,proto3" json:"mode,omitempty"`
// room_name/room_avatar/room_description 是创建时确定的展示资料快照,消费者不需要反查 room-service。
RoomName string `protobuf:"bytes,5,opt,name=room_name,json=roomName,proto3" json:"room_name,omitempty"`
RoomAvatar string `protobuf:"bytes,6,opt,name=room_avatar,json=roomAvatar,proto3" json:"room_avatar,omitempty"`
RoomDescription string `protobuf:"bytes,7,opt,name=room_description,json=roomDescription,proto3" json:"room_description,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomCreated) Reset() {
@ -204,15 +203,14 @@ func (x *RoomCreated) GetRoomDescription() string {
// RoomProfileUpdated 表达房间展示资料或麦位数量变更。
type RoomProfileUpdated struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
RoomName string `protobuf:"bytes,2,opt,name=room_name,json=roomName,proto3" json:"room_name,omitempty"`
RoomAvatar string `protobuf:"bytes,3,opt,name=room_avatar,json=roomAvatar,proto3" json:"room_avatar,omitempty"`
RoomDescription string `protobuf:"bytes,4,opt,name=room_description,json=roomDescription,proto3" json:"room_description,omitempty"`
SeatCount int32 `protobuf:"varint,5,opt,name=seat_count,json=seatCount,proto3" json:"seat_count,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
RoomName string `protobuf:"bytes,2,opt,name=room_name,json=roomName,proto3" json:"room_name,omitempty"`
RoomAvatar string `protobuf:"bytes,3,opt,name=room_avatar,json=roomAvatar,proto3" json:"room_avatar,omitempty"`
RoomDescription string `protobuf:"bytes,4,opt,name=room_description,json=roomDescription,proto3" json:"room_description,omitempty"`
SeatCount int32 `protobuf:"varint,5,opt,name=seat_count,json=seatCount,proto3" json:"seat_count,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomProfileUpdated) Reset() {
@ -282,12 +280,11 @@ func (x *RoomProfileUpdated) GetSeatCount() int32 {
// RoomUserJoined 表达用户业务态进房成功。
type RoomUserJoined struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"`
unknownFields protoimpl.UnknownFields
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomUserJoined) Reset() {
@ -336,11 +333,10 @@ func (x *RoomUserJoined) GetRole() string {
// RoomUserLeft 表达用户离房成功。
type RoomUserLeft struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
unknownFields protoimpl.UnknownFields
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomUserLeft) Reset() {
@ -382,12 +378,11 @@ func (x *RoomUserLeft) GetUserId() int64 {
// RoomClosed 表达房间生命周期关闭完成。
type RoomClosed struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"`
unknownFields protoimpl.UnknownFields
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomClosed) Reset() {
@ -436,15 +431,12 @@ func (x *RoomClosed) GetReason() string {
// RoomMicChanged 表达任意上下麦和换麦位动作。
type RoomMicChanged struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
FromSeat int32 `protobuf:"varint,3,opt,name=from_seat,json=fromSeat,proto3" json:"from_seat,omitempty"`
ToSeat int32 `protobuf:"varint,4,opt,name=to_seat,json=toSeat,proto3" json:"to_seat,omitempty"`
Action string `protobuf:"bytes,5,opt,name=action,proto3" json:"action,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
FromSeat int32 `protobuf:"varint,3,opt,name=from_seat,json=fromSeat,proto3" json:"from_seat,omitempty"`
ToSeat int32 `protobuf:"varint,4,opt,name=to_seat,json=toSeat,proto3" json:"to_seat,omitempty"`
Action string `protobuf:"bytes,5,opt,name=action,proto3" json:"action,omitempty"`
// mic_session_id 用于把业务麦位事件和 RTC 发流会话绑定,客户端和 webhook 都必须透传。
MicSessionId string `protobuf:"bytes,6,opt,name=mic_session_id,json=micSessionId,proto3" json:"mic_session_id,omitempty"`
// publish_state 透出 pending_publish/publishing/idle避免把“占麦”和“已发流”混成一个状态。
@ -454,6 +446,9 @@ type RoomMicChanged struct {
PublishDeadlineMs int64 `protobuf:"varint,9,opt,name=publish_deadline_ms,json=publishDeadlineMs,proto3" json:"publish_deadline_ms,omitempty"`
PublishEventTimeMs int64 `protobuf:"varint,10,opt,name=publish_event_time_ms,json=publishEventTimeMs,proto3" json:"publish_event_time_ms,omitempty"`
MicMuted bool `protobuf:"varint,11,opt,name=mic_muted,json=micMuted,proto3" json:"mic_muted,omitempty"`
SeatStatus string `protobuf:"bytes,12,opt,name=seat_status,json=seatStatus,proto3" json:"seat_status,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomMicChanged) Reset() {
@ -563,15 +558,21 @@ func (x *RoomMicChanged) GetMicMuted() bool {
return false
}
func (x *RoomMicChanged) GetSeatStatus() string {
if x != nil {
return x.SeatStatus
}
return ""
}
// RoomMicSeatLocked 表达单个麦位锁定状态变更。
type RoomMicSeatLocked struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
SeatNo int32 `protobuf:"varint,2,opt,name=seat_no,json=seatNo,proto3" json:"seat_no,omitempty"`
Locked bool `protobuf:"varint,3,opt,name=locked,proto3" json:"locked,omitempty"`
unknownFields protoimpl.UnknownFields
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
SeatNo int32 `protobuf:"varint,2,opt,name=seat_no,json=seatNo,proto3" json:"seat_no,omitempty"`
Locked bool `protobuf:"varint,3,opt,name=locked,proto3" json:"locked,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomMicSeatLocked) Reset() {
@ -627,12 +628,11 @@ func (x *RoomMicSeatLocked) GetLocked() bool {
// RoomChatEnabledChanged 表达公屏开关状态变更。
type RoomChatEnabledChanged struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"`
unknownFields protoimpl.UnknownFields
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
Enabled bool `protobuf:"varint,2,opt,name=enabled,proto3" json:"enabled,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomChatEnabledChanged) Reset() {
@ -681,12 +681,11 @@ func (x *RoomChatEnabledChanged) GetEnabled() bool {
// RoomPasswordChanged 表达房间入房密码状态变更,不携带明文或哈希。
type RoomPasswordChanged struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
Locked bool `protobuf:"varint,2,opt,name=locked,proto3" json:"locked,omitempty"`
unknownFields protoimpl.UnknownFields
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
Locked bool `protobuf:"varint,2,opt,name=locked,proto3" json:"locked,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomPasswordChanged) Reset() {
@ -735,13 +734,12 @@ func (x *RoomPasswordChanged) GetLocked() bool {
// RoomAdminChanged 表达房间管理员集合变更。
type RoomAdminChanged struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"`
unknownFields protoimpl.UnknownFields
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomAdminChanged) Reset() {
@ -797,13 +795,12 @@ func (x *RoomAdminChanged) GetEnabled() bool {
// RoomUserMuted 表达禁言状态变更。
type RoomUserMuted struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
Muted bool `protobuf:"varint,3,opt,name=muted,proto3" json:"muted,omitempty"`
unknownFields protoimpl.UnknownFields
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
Muted bool `protobuf:"varint,3,opt,name=muted,proto3" json:"muted,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomUserMuted) Reset() {
@ -859,12 +856,11 @@ func (x *RoomUserMuted) GetMuted() bool {
// RoomUserKicked 表达踢人动作。
type RoomUserKicked struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
unknownFields protoimpl.UnknownFields
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomUserKicked) Reset() {
@ -913,12 +909,11 @@ func (x *RoomUserKicked) GetTargetUserId() int64 {
// RoomUserUnbanned 表达解除房间 ban。
type RoomUserUnbanned struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
unknownFields protoimpl.UnknownFields
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomUserUnbanned) Reset() {
@ -967,18 +962,17 @@ func (x *RoomUserUnbanned) GetTargetUserId() int64 {
// RoomGiftSent 表达送礼成功且已落房间状态。
type RoomGiftSent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
SenderUserId int64 `protobuf:"varint,1,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
GiftId string `protobuf:"bytes,3,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
GiftCount int32 `protobuf:"varint,4,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
GiftValue int64 `protobuf:"varint,5,opt,name=gift_value,json=giftValue,proto3" json:"gift_value,omitempty"`
BillingReceiptId string `protobuf:"bytes,6,opt,name=billing_receipt_id,json=billingReceiptId,proto3" json:"billing_receipt_id,omitempty"`
VisibleRegionId int64 `protobuf:"varint,7,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
CommandId string `protobuf:"bytes,8,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
SenderUserId int64 `protobuf:"varint,1,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
GiftId string `protobuf:"bytes,3,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
GiftCount int32 `protobuf:"varint,4,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
GiftValue int64 `protobuf:"varint,5,opt,name=gift_value,json=giftValue,proto3" json:"gift_value,omitempty"`
BillingReceiptId string `protobuf:"bytes,6,opt,name=billing_receipt_id,json=billingReceiptId,proto3" json:"billing_receipt_id,omitempty"`
VisibleRegionId int64 `protobuf:"varint,7,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
CommandId string `protobuf:"bytes,8,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomGiftSent) Reset() {
@ -1069,12 +1063,11 @@ func (x *RoomGiftSent) GetCommandId() string {
// RoomHeatChanged 表达热度变化结果。
type RoomHeatChanged struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Delta int64 `protobuf:"varint,1,opt,name=delta,proto3" json:"delta,omitempty"`
CurrentHeat int64 `protobuf:"varint,2,opt,name=current_heat,json=currentHeat,proto3" json:"current_heat,omitempty"`
unknownFields protoimpl.UnknownFields
Delta int64 `protobuf:"varint,1,opt,name=delta,proto3" json:"delta,omitempty"`
CurrentHeat int64 `protobuf:"varint,2,opt,name=current_heat,json=currentHeat,proto3" json:"current_heat,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomHeatChanged) Reset() {
@ -1123,13 +1116,12 @@ func (x *RoomHeatChanged) GetCurrentHeat() int64 {
// RoomRankChanged 表达本地礼物榜变化结果。
type RoomRankChanged struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
Score int64 `protobuf:"varint,2,opt,name=score,proto3" json:"score,omitempty"`
GiftValue int64 `protobuf:"varint,3,opt,name=gift_value,json=giftValue,proto3" json:"gift_value,omitempty"`
unknownFields protoimpl.UnknownFields
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
Score int64 `protobuf:"varint,2,opt,name=score,proto3" json:"score,omitempty"`
GiftValue int64 `protobuf:"varint,3,opt,name=gift_value,json=giftValue,proto3" json:"gift_value,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RoomRankChanged) Reset() {
@ -1185,173 +1177,112 @@ func (x *RoomRankChanged) GetGiftValue() int64 {
var File_proto_events_room_v1_events_proto protoreflect.FileDescriptor
var file_proto_events_room_v1_events_proto_rawDesc = []byte{
0x0a, 0x21, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x72,
0x6f, 0x6f, 0x6d, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x12, 0x14, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x65, 0x76, 0x65, 0x6e, 0x74,
0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x6d, 0x2e, 0x76, 0x31, 0x22, 0xda, 0x01, 0x0a, 0x0d, 0x45, 0x76,
0x65, 0x6e, 0x74, 0x45, 0x6e, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65,
0x76, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65,
0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x69,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6f, 0x6d, 0x49, 0x64, 0x12,
0x1d, 0x0a, 0x0a, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x21,
0x0a, 0x0c, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x72, 0x6f, 0x6f, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x63, 0x63, 0x75, 0x72, 0x72, 0x65, 0x64, 0x5f, 0x61, 0x74,
0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6f, 0x63, 0x63, 0x75, 0x72,
0x72, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18,
0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x61,
0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61,
0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x22, 0xcd, 0x01, 0x0a, 0x0b, 0x52, 0x6f, 0x6f, 0x6d, 0x43,
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6f,
0x77, 0x6e, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65,
0x61, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09,
0x73, 0x65, 0x61, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64,
0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a,
0x09, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x72, 0x6f, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x6f,
0x6f, 0x6d, 0x5f, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0a, 0x72, 0x6f, 0x6f, 0x6d, 0x41, 0x76, 0x61, 0x74, 0x61, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x72,
0x6f, 0x6f, 0x6d, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18,
0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x72, 0x6f, 0x6f, 0x6d, 0x44, 0x65, 0x73, 0x63, 0x72,
0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xc0, 0x01, 0x0a, 0x12, 0x52, 0x6f, 0x6f, 0x6d, 0x50,
0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x12, 0x22, 0x0a,
0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f,
0x0a, 0x0b, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x6f, 0x6f, 0x6d, 0x41, 0x76, 0x61, 0x74, 0x61, 0x72, 0x12,
0x29, 0x0a, 0x10, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x72, 0x6f, 0x6f, 0x6d, 0x44,
0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65,
0x61, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09,
0x73, 0x65, 0x61, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x3d, 0x0a, 0x0e, 0x52, 0x6f, 0x6f,
0x6d, 0x55, 0x73, 0x65, 0x72, 0x4a, 0x6f, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75,
0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73,
0x65, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x27, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d,
0x55, 0x73, 0x65, 0x72, 0x4c, 0x65, 0x66, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49,
0x64, 0x22, 0x48, 0x0a, 0x0a, 0x52, 0x6f, 0x6f, 0x6d, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x12,
0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65,
0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x8b, 0x03, 0x0a, 0x0e,
0x52, 0x6f, 0x6f, 0x6d, 0x4d, 0x69, 0x63, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x22,
0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72,
0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67,
0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x6f, 0x6d,
0x5f, 0x73, 0x65, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x72, 0x6f,
0x6d, 0x53, 0x65, 0x61, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x6f, 0x5f, 0x73, 0x65, 0x61, 0x74,
0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x74, 0x6f, 0x53, 0x65, 0x61, 0x74, 0x12, 0x16,
0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x69, 0x63, 0x5f, 0x73, 0x65,
0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
0x6d, 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x07, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74,
0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28,
0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x70, 0x75, 0x62,
0x6c, 0x69, 0x73, 0x68, 0x5f, 0x64, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x6d, 0x73,
0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x44,
0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x4d, 0x73, 0x12, 0x31, 0x0a, 0x15, 0x70, 0x75, 0x62,
0x6c, 0x69, 0x73, 0x68, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f,
0x6d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73,
0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x12, 0x1b, 0x0a, 0x09,
0x6d, 0x69, 0x63, 0x5f, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52,
0x08, 0x6d, 0x69, 0x63, 0x4d, 0x75, 0x74, 0x65, 0x64, 0x22, 0x68, 0x0a, 0x11, 0x52, 0x6f, 0x6f,
0x6d, 0x4d, 0x69, 0x63, 0x53, 0x65, 0x61, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x22,
0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72,
0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x65, 0x61, 0x74, 0x5f, 0x6e, 0x6f, 0x18, 0x02, 0x20,
0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x65, 0x61, 0x74, 0x4e, 0x6f, 0x12, 0x16, 0x0a, 0x06, 0x6c,
0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, 0x6f, 0x63,
0x6b, 0x65, 0x64, 0x22, 0x56, 0x0a, 0x16, 0x52, 0x6f, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x74, 0x45,
0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x22, 0x0a,
0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01,
0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x51, 0x0a, 0x13, 0x52,
0x6f, 0x6f, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67,
0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72,
0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64,
0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x22, 0x76,
0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67,
0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72,
0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74,
0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c,
0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07,
0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65,
0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x6f, 0x0a, 0x0d, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73,
0x65, 0x72, 0x4d, 0x75, 0x74, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72,
0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74,
0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
0x52, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x22, 0x5a, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d, 0x55,
0x73, 0x65, 0x72, 0x4b, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74,
0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a,
0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65,
0x72, 0x49, 0x64, 0x22, 0x5c, 0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x55,
0x6e, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72,
0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74,
0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x22, 0xaa, 0x02, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65,
0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64,
0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17,
0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66,
0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74,
0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67,
0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28,
0x09, 0x52, 0x10, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70,
0x74, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72,
0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f,
0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12,
0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20,
0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 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, 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,
}
const file_proto_events_room_v1_events_proto_rawDesc = "" +
"\n" +
"!proto/events/room/v1/events.proto\x12\x14hyapp.events.room.v1\"\xda\x01\n" +
"\rEventEnvelope\x12\x19\n" +
"\bevent_id\x18\x01 \x01(\tR\aeventId\x12\x17\n" +
"\aroom_id\x18\x02 \x01(\tR\x06roomId\x12\x1d\n" +
"\n" +
"event_type\x18\x03 \x01(\tR\teventType\x12!\n" +
"\froom_version\x18\x04 \x01(\x03R\vroomVersion\x12$\n" +
"\x0eoccurred_at_ms\x18\x05 \x01(\x03R\foccurredAtMs\x12\x12\n" +
"\x04body\x18\x06 \x01(\fR\x04body\x12\x19\n" +
"\bapp_code\x18\a \x01(\tR\aappCode\"\xcd\x01\n" +
"\vRoomCreated\x12\"\n" +
"\rowner_user_id\x18\x01 \x01(\x03R\vownerUserId\x12\x1d\n" +
"\n" +
"seat_count\x18\x03 \x01(\x05R\tseatCount\x12\x12\n" +
"\x04mode\x18\x04 \x01(\tR\x04mode\x12\x1b\n" +
"\troom_name\x18\x05 \x01(\tR\broomName\x12\x1f\n" +
"\vroom_avatar\x18\x06 \x01(\tR\n" +
"roomAvatar\x12)\n" +
"\x10room_description\x18\a \x01(\tR\x0froomDescription\"\xc0\x01\n" +
"\x12RoomProfileUpdated\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12\x1b\n" +
"\troom_name\x18\x02 \x01(\tR\broomName\x12\x1f\n" +
"\vroom_avatar\x18\x03 \x01(\tR\n" +
"roomAvatar\x12)\n" +
"\x10room_description\x18\x04 \x01(\tR\x0froomDescription\x12\x1d\n" +
"\n" +
"seat_count\x18\x05 \x01(\x05R\tseatCount\"=\n" +
"\x0eRoomUserJoined\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x12\n" +
"\x04role\x18\x02 \x01(\tR\x04role\"'\n" +
"\fRoomUserLeft\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x03R\x06userId\"H\n" +
"\n" +
"RoomClosed\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12\x16\n" +
"\x06reason\x18\x02 \x01(\tR\x06reason\"\xac\x03\n" +
"\x0eRoomMicChanged\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12$\n" +
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x1b\n" +
"\tfrom_seat\x18\x03 \x01(\x05R\bfromSeat\x12\x17\n" +
"\ato_seat\x18\x04 \x01(\x05R\x06toSeat\x12\x16\n" +
"\x06action\x18\x05 \x01(\tR\x06action\x12$\n" +
"\x0emic_session_id\x18\x06 \x01(\tR\fmicSessionId\x12#\n" +
"\rpublish_state\x18\a \x01(\tR\fpublishState\x12\x16\n" +
"\x06reason\x18\b \x01(\tR\x06reason\x12.\n" +
"\x13publish_deadline_ms\x18\t \x01(\x03R\x11publishDeadlineMs\x121\n" +
"\x15publish_event_time_ms\x18\n" +
" \x01(\x03R\x12publishEventTimeMs\x12\x1b\n" +
"\tmic_muted\x18\v \x01(\bR\bmicMuted\x12\x1f\n" +
"\vseat_status\x18\f \x01(\tR\n" +
"seatStatus\"h\n" +
"\x11RoomMicSeatLocked\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12\x17\n" +
"\aseat_no\x18\x02 \x01(\x05R\x06seatNo\x12\x16\n" +
"\x06locked\x18\x03 \x01(\bR\x06locked\"V\n" +
"\x16RoomChatEnabledChanged\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12\x18\n" +
"\aenabled\x18\x02 \x01(\bR\aenabled\"Q\n" +
"\x13RoomPasswordChanged\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12\x16\n" +
"\x06locked\x18\x02 \x01(\bR\x06locked\"v\n" +
"\x10RoomAdminChanged\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12$\n" +
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x18\n" +
"\aenabled\x18\x03 \x01(\bR\aenabled\"o\n" +
"\rRoomUserMuted\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12$\n" +
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x14\n" +
"\x05muted\x18\x03 \x01(\bR\x05muted\"Z\n" +
"\x0eRoomUserKicked\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12$\n" +
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\\\n" +
"\x10RoomUserUnbanned\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12$\n" +
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\xaa\x02\n" +
"\fRoomGiftSent\x12$\n" +
"\x0esender_user_id\x18\x01 \x01(\x03R\fsenderUserId\x12$\n" +
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x17\n" +
"\agift_id\x18\x03 \x01(\tR\x06giftId\x12\x1d\n" +
"\n" +
"gift_count\x18\x04 \x01(\x05R\tgiftCount\x12\x1d\n" +
"\n" +
"gift_value\x18\x05 \x01(\x03R\tgiftValue\x12,\n" +
"\x12billing_receipt_id\x18\x06 \x01(\tR\x10billingReceiptId\x12*\n" +
"\x11visible_region_id\x18\a \x01(\x03R\x0fvisibleRegionId\x12\x1d\n" +
"\n" +
"command_id\x18\b \x01(\tR\tcommandId\"J\n" +
"\x0fRoomHeatChanged\x12\x14\n" +
"\x05delta\x18\x01 \x01(\x03R\x05delta\x12!\n" +
"\fcurrent_heat\x18\x02 \x01(\x03R\vcurrentHeat\"_\n" +
"\x0fRoomRankChanged\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x14\n" +
"\x05score\x18\x02 \x01(\x03R\x05score\x12\x1d\n" +
"\n" +
"gift_value\x18\x03 \x01(\x03R\tgiftValueB3Z1hyapp.local/api/proto/events/room/v1;roomeventsv1b\x06proto3"
var (
file_proto_events_room_v1_events_proto_rawDescOnce sync.Once
file_proto_events_room_v1_events_proto_rawDescData = file_proto_events_room_v1_events_proto_rawDesc
file_proto_events_room_v1_events_proto_rawDescData []byte
)
func file_proto_events_room_v1_events_proto_rawDescGZIP() []byte {
file_proto_events_room_v1_events_proto_rawDescOnce.Do(func() {
file_proto_events_room_v1_events_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_events_room_v1_events_proto_rawDescData)
file_proto_events_room_v1_events_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_events_room_v1_events_proto_rawDesc), len(file_proto_events_room_v1_events_proto_rawDesc)))
})
return file_proto_events_room_v1_events_proto_rawDescData
}
@ -1393,7 +1324,7 @@ func file_proto_events_room_v1_events_proto_init() {
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_events_room_v1_events_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_events_room_v1_events_proto_rawDesc), len(file_proto_events_room_v1_events_proto_rawDesc)),
NumEnums: 0,
NumMessages: 17,
NumExtensions: 0,
@ -1404,7 +1335,6 @@ func file_proto_events_room_v1_events_proto_init() {
MessageInfos: file_proto_events_room_v1_events_proto_msgTypes,
}.Build()
File_proto_events_room_v1_events_proto = out.File
file_proto_events_room_v1_events_proto_rawDesc = nil
file_proto_events_room_v1_events_proto_goTypes = nil
file_proto_events_room_v1_events_proto_depIdxs = nil
}

View File

@ -69,6 +69,7 @@ message RoomMicChanged {
int64 publish_deadline_ms = 9;
int64 publish_event_time_ms = 10;
bool mic_muted = 11;
string seat_status = 12;
}
// RoomMicSeatLocked

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v4.25.3
// - protoc-gen-go-grpc v1.6.2
// - protoc v5.28.3
// source: proto/game/v1/game.proto
package gamev1
@ -76,10 +76,10 @@ type GameAppServiceServer interface {
type UnimplementedGameAppServiceServer struct{}
func (UnimplementedGameAppServiceServer) ListGames(context.Context, *ListGamesRequest) (*ListGamesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListGames not implemented")
return nil, status.Error(codes.Unimplemented, "method ListGames not implemented")
}
func (UnimplementedGameAppServiceServer) LaunchGame(context.Context, *LaunchGameRequest) (*LaunchGameResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method LaunchGame not implemented")
return nil, status.Error(codes.Unimplemented, "method LaunchGame not implemented")
}
func (UnimplementedGameAppServiceServer) mustEmbedUnimplementedGameAppServiceServer() {}
func (UnimplementedGameAppServiceServer) testEmbeddedByValue() {}
@ -92,7 +92,7 @@ type UnsafeGameAppServiceServer interface {
}
func RegisterGameAppServiceServer(s grpc.ServiceRegistrar, srv GameAppServiceServer) {
// If the following call pancis, it indicates UnimplementedGameAppServiceServer was
// If the following call panics, it indicates UnimplementedGameAppServiceServer 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.
@ -203,7 +203,7 @@ type GameCallbackServiceServer interface {
type UnimplementedGameCallbackServiceServer struct{}
func (UnimplementedGameCallbackServiceServer) HandleCallback(context.Context, *CallbackRequest) (*CallbackResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method HandleCallback not implemented")
return nil, status.Error(codes.Unimplemented, "method HandleCallback not implemented")
}
func (UnimplementedGameCallbackServiceServer) mustEmbedUnimplementedGameCallbackServiceServer() {}
func (UnimplementedGameCallbackServiceServer) testEmbeddedByValue() {}
@ -216,7 +216,7 @@ type UnsafeGameCallbackServiceServer interface {
}
func RegisterGameCallbackServiceServer(s grpc.ServiceRegistrar, srv GameCallbackServiceServer) {
// If the following call pancis, it indicates UnimplementedGameCallbackServiceServer was
// If the following call panics, it indicates UnimplementedGameCallbackServiceServer 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.
@ -309,7 +309,7 @@ type GameCronServiceServer interface {
type UnimplementedGameCronServiceServer struct{}
func (UnimplementedGameCronServiceServer) ProcessLevelEventOutboxBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ProcessLevelEventOutboxBatch not implemented")
return nil, status.Error(codes.Unimplemented, "method ProcessLevelEventOutboxBatch not implemented")
}
func (UnimplementedGameCronServiceServer) mustEmbedUnimplementedGameCronServiceServer() {}
func (UnimplementedGameCronServiceServer) testEmbeddedByValue() {}
@ -322,7 +322,7 @@ type UnsafeGameCronServiceServer interface {
}
func RegisterGameCronServiceServer(s grpc.ServiceRegistrar, srv GameCronServiceServer) {
// If the following call pancis, it indicates UnimplementedGameCronServiceServer was
// If the following call panics, it indicates UnimplementedGameCronServiceServer 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.
@ -463,19 +463,19 @@ type GameAdminServiceServer interface {
type UnimplementedGameAdminServiceServer struct{}
func (UnimplementedGameAdminServiceServer) ListPlatforms(context.Context, *ListPlatformsRequest) (*ListPlatformsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListPlatforms not implemented")
return nil, status.Error(codes.Unimplemented, "method ListPlatforms not implemented")
}
func (UnimplementedGameAdminServiceServer) UpsertPlatform(context.Context, *UpsertPlatformRequest) (*PlatformResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpsertPlatform not implemented")
return nil, status.Error(codes.Unimplemented, "method UpsertPlatform not implemented")
}
func (UnimplementedGameAdminServiceServer) ListCatalog(context.Context, *ListCatalogRequest) (*ListCatalogResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListCatalog not implemented")
return nil, status.Error(codes.Unimplemented, "method ListCatalog not implemented")
}
func (UnimplementedGameAdminServiceServer) UpsertCatalog(context.Context, *UpsertCatalogRequest) (*CatalogResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpsertCatalog not implemented")
return nil, status.Error(codes.Unimplemented, "method UpsertCatalog not implemented")
}
func (UnimplementedGameAdminServiceServer) SetGameStatus(context.Context, *SetGameStatusRequest) (*CatalogResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetGameStatus not implemented")
return nil, status.Error(codes.Unimplemented, "method SetGameStatus not implemented")
}
func (UnimplementedGameAdminServiceServer) mustEmbedUnimplementedGameAdminServiceServer() {}
func (UnimplementedGameAdminServiceServer) testEmbeddedByValue() {}
@ -488,7 +488,7 @@ type UnsafeGameAdminServiceServer interface {
}
func RegisterGameAdminServiceServer(s grpc.ServiceRegistrar, srv GameAdminServiceServer) {
// If the following call pancis, it indicates UnimplementedGameAdminServiceServer was
// If the following call panics, it indicates UnimplementedGameAdminServiceServer 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.

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,8 @@ message SeatState {
int64 last_publish_event_time_ms = 8;
// mic_muted UI
bool mic_muted = 9;
// seat_status empty/locked/occupied/publishing/muted
string seat_status = 10;
}
// RankItem

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v4.25.3
// - protoc-gen-go-grpc v1.6.2
// - protoc v5.28.3
// source: proto/room/v1/room.proto
package roomv1
@ -353,73 +353,73 @@ type RoomCommandServiceServer interface {
type UnimplementedRoomCommandServiceServer struct{}
func (UnimplementedRoomCommandServiceServer) CreateRoom(context.Context, *CreateRoomRequest) (*CreateRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateRoom not implemented")
return nil, status.Error(codes.Unimplemented, "method CreateRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateRoomProfile not implemented")
return nil, status.Error(codes.Unimplemented, "method UpdateRoomProfile not implemented")
}
func (UnimplementedRoomCommandServiceServer) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method JoinRoom not implemented")
return nil, status.Error(codes.Unimplemented, "method JoinRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) RoomHeartbeat(context.Context, *RoomHeartbeatRequest) (*RoomHeartbeatResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RoomHeartbeat not implemented")
return nil, status.Error(codes.Unimplemented, "method RoomHeartbeat not implemented")
}
func (UnimplementedRoomCommandServiceServer) LeaveRoom(context.Context, *LeaveRoomRequest) (*LeaveRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method LeaveRoom not implemented")
return nil, status.Error(codes.Unimplemented, "method LeaveRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) CloseRoom(context.Context, *CloseRoomRequest) (*CloseRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CloseRoom not implemented")
return nil, status.Error(codes.Unimplemented, "method CloseRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method MicUp not implemented")
return nil, status.Error(codes.Unimplemented, "method MicUp not implemented")
}
func (UnimplementedRoomCommandServiceServer) MicDown(context.Context, *MicDownRequest) (*MicDownResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method MicDown not implemented")
return nil, status.Error(codes.Unimplemented, "method MicDown not implemented")
}
func (UnimplementedRoomCommandServiceServer) ChangeMicSeat(context.Context, *ChangeMicSeatRequest) (*ChangeMicSeatResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ChangeMicSeat not implemented")
return nil, status.Error(codes.Unimplemented, "method ChangeMicSeat not implemented")
}
func (UnimplementedRoomCommandServiceServer) ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ConfirmMicPublishing not implemented")
return nil, status.Error(codes.Unimplemented, "method ConfirmMicPublishing not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetMicMute not implemented")
return nil, status.Error(codes.Unimplemented, "method SetMicMute not implemented")
}
func (UnimplementedRoomCommandServiceServer) ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ApplyRTCEvent not implemented")
return nil, status.Error(codes.Unimplemented, "method ApplyRTCEvent not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetMicSeatLock not implemented")
return nil, status.Error(codes.Unimplemented, "method SetMicSeatLock not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetChatEnabled(context.Context, *SetChatEnabledRequest) (*SetChatEnabledResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetChatEnabled not implemented")
return nil, status.Error(codes.Unimplemented, "method SetChatEnabled not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetRoomPassword(context.Context, *SetRoomPasswordRequest) (*SetRoomPasswordResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetRoomPassword not implemented")
return nil, status.Error(codes.Unimplemented, "method SetRoomPassword not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetRoomAdmin(context.Context, *SetRoomAdminRequest) (*SetRoomAdminResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetRoomAdmin not implemented")
return nil, status.Error(codes.Unimplemented, "method SetRoomAdmin not implemented")
}
func (UnimplementedRoomCommandServiceServer) MuteUser(context.Context, *MuteUserRequest) (*MuteUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method MuteUser not implemented")
return nil, status.Error(codes.Unimplemented, "method MuteUser not implemented")
}
func (UnimplementedRoomCommandServiceServer) KickUser(context.Context, *KickUserRequest) (*KickUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method KickUser not implemented")
return nil, status.Error(codes.Unimplemented, "method KickUser not implemented")
}
func (UnimplementedRoomCommandServiceServer) UnbanUser(context.Context, *UnbanUserRequest) (*UnbanUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UnbanUser not implemented")
return nil, status.Error(codes.Unimplemented, "method UnbanUser not implemented")
}
func (UnimplementedRoomCommandServiceServer) SystemEvictUser(context.Context, *SystemEvictUserRequest) (*SystemEvictUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SystemEvictUser not implemented")
return nil, status.Error(codes.Unimplemented, "method SystemEvictUser not implemented")
}
func (UnimplementedRoomCommandServiceServer) SendGift(context.Context, *SendGiftRequest) (*SendGiftResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SendGift not implemented")
return nil, status.Error(codes.Unimplemented, "method SendGift not implemented")
}
func (UnimplementedRoomCommandServiceServer) FollowRoom(context.Context, *FollowRoomRequest) (*FollowRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method FollowRoom not implemented")
return nil, status.Error(codes.Unimplemented, "method FollowRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) UnfollowRoom(context.Context, *UnfollowRoomRequest) (*UnfollowRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UnfollowRoom not implemented")
return nil, status.Error(codes.Unimplemented, "method UnfollowRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) mustEmbedUnimplementedRoomCommandServiceServer() {}
func (UnimplementedRoomCommandServiceServer) testEmbeddedByValue() {}
@ -432,7 +432,7 @@ type UnsafeRoomCommandServiceServer interface {
}
func RegisterRoomCommandServiceServer(s grpc.ServiceRegistrar, srv RoomCommandServiceServer) {
// If the following call pancis, it indicates UnimplementedRoomCommandServiceServer was
// If the following call panics, it indicates UnimplementedRoomCommandServiceServer 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.
@ -1022,10 +1022,10 @@ type RoomGuardServiceServer interface {
type UnimplementedRoomGuardServiceServer struct{}
func (UnimplementedRoomGuardServiceServer) CheckSpeakPermission(context.Context, *CheckSpeakPermissionRequest) (*CheckSpeakPermissionResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckSpeakPermission not implemented")
return nil, status.Error(codes.Unimplemented, "method CheckSpeakPermission not implemented")
}
func (UnimplementedRoomGuardServiceServer) VerifyRoomPresence(context.Context, *VerifyRoomPresenceRequest) (*VerifyRoomPresenceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method VerifyRoomPresence not implemented")
return nil, status.Error(codes.Unimplemented, "method VerifyRoomPresence not implemented")
}
func (UnimplementedRoomGuardServiceServer) mustEmbedUnimplementedRoomGuardServiceServer() {}
func (UnimplementedRoomGuardServiceServer) testEmbeddedByValue() {}
@ -1038,7 +1038,7 @@ type UnsafeRoomGuardServiceServer interface {
}
func RegisterRoomGuardServiceServer(s grpc.ServiceRegistrar, srv RoomGuardServiceServer) {
// If the following call pancis, it indicates UnimplementedRoomGuardServiceServer was
// If the following call panics, it indicates UnimplementedRoomGuardServiceServer 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.
@ -1218,22 +1218,22 @@ type RoomQueryServiceServer interface {
type UnimplementedRoomQueryServiceServer struct{}
func (UnimplementedRoomQueryServiceServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRooms not implemented")
return nil, status.Error(codes.Unimplemented, "method ListRooms not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRoomFeeds not implemented")
return nil, status.Error(codes.Unimplemented, "method ListRoomFeeds not implemented")
}
func (UnimplementedRoomQueryServiceServer) GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMyRoom not implemented")
return nil, status.Error(codes.Unimplemented, "method GetMyRoom not implemented")
}
func (UnimplementedRoomQueryServiceServer) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetCurrentRoom not implemented")
return nil, status.Error(codes.Unimplemented, "method GetCurrentRoom not implemented")
}
func (UnimplementedRoomQueryServiceServer) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetRoomSnapshot not implemented")
return nil, status.Error(codes.Unimplemented, "method GetRoomSnapshot not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRoomOnlineUsers not implemented")
return nil, status.Error(codes.Unimplemented, "method ListRoomOnlineUsers not implemented")
}
func (UnimplementedRoomQueryServiceServer) mustEmbedUnimplementedRoomQueryServiceServer() {}
func (UnimplementedRoomQueryServiceServer) testEmbeddedByValue() {}
@ -1246,7 +1246,7 @@ type UnsafeRoomQueryServiceServer interface {
}
func RegisterRoomQueryServiceServer(s grpc.ServiceRegistrar, srv RoomQueryServiceServer) {
// If the following call pancis, it indicates UnimplementedRoomQueryServiceServer was
// If the following call panics, it indicates UnimplementedRoomQueryServiceServer 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.

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.1
// protoc v4.25.3
// protoc-gen-go v1.36.11
// protoc v5.28.3
// source: proto/user/v1/auth.proto
package userv1
@ -11,6 +11,7 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
@ -22,13 +23,12 @@ const (
// LoginPasswordRequest 使用当前 display_user_id 和已设置密码登录。
type LoginPasswordRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
DisplayUserId string `protobuf:"bytes,2,opt,name=display_user_id,json=displayUserId,proto3" json:"display_user_id,omitempty"`
Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
DisplayUserId string `protobuf:"bytes,2,opt,name=display_user_id,json=displayUserId,proto3" json:"display_user_id,omitempty"`
Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LoginPasswordRequest) Reset() {
@ -84,30 +84,29 @@ func (x *LoginPasswordRequest) GetPassword() string {
// LoginThirdPartyRequest 使用三方凭证登录或注册。
type LoginThirdPartyRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"`
Credential string `protobuf:"bytes,3,opt,name=credential,proto3" json:"credential,omitempty"`
Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"`
Gender string `protobuf:"bytes,5,opt,name=gender,proto3" json:"gender,omitempty"`
Country string `protobuf:"bytes,6,opt,name=country,proto3" json:"country,omitempty"`
InviteCode string `protobuf:"bytes,7,opt,name=invite_code,json=inviteCode,proto3" json:"invite_code,omitempty"`
DeviceId string `protobuf:"bytes,8,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"`
Device string `protobuf:"bytes,9,opt,name=device,proto3" json:"device,omitempty"`
Avatar string `protobuf:"bytes,10,opt,name=avatar,proto3" json:"avatar,omitempty"`
Birth string `protobuf:"bytes,11,opt,name=birth,proto3" json:"birth,omitempty"`
AppVersion string `protobuf:"bytes,12,opt,name=app_version,json=appVersion,proto3" json:"app_version,omitempty"`
Source string `protobuf:"bytes,13,opt,name=source,proto3" json:"source,omitempty"`
Platform string `protobuf:"bytes,14,opt,name=platform,proto3" json:"platform,omitempty"`
Language string `protobuf:"bytes,15,opt,name=language,proto3" json:"language,omitempty"`
OsVersion string `protobuf:"bytes,16,opt,name=os_version,json=osVersion,proto3" json:"os_version,omitempty"`
BuildNumber string `protobuf:"bytes,17,opt,name=build_number,json=buildNumber,proto3" json:"build_number,omitempty"`
Timezone string `protobuf:"bytes,18,opt,name=timezone,proto3" json:"timezone,omitempty"`
InstallChannel string `protobuf:"bytes,19,opt,name=install_channel,json=installChannel,proto3" json:"install_channel,omitempty"`
Campaign string `protobuf:"bytes,20,opt,name=campaign,proto3" json:"campaign,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"`
Credential string `protobuf:"bytes,3,opt,name=credential,proto3" json:"credential,omitempty"`
Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"`
Gender string `protobuf:"bytes,5,opt,name=gender,proto3" json:"gender,omitempty"`
Country string `protobuf:"bytes,6,opt,name=country,proto3" json:"country,omitempty"`
InviteCode string `protobuf:"bytes,7,opt,name=invite_code,json=inviteCode,proto3" json:"invite_code,omitempty"`
DeviceId string `protobuf:"bytes,8,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"`
Device string `protobuf:"bytes,9,opt,name=device,proto3" json:"device,omitempty"`
Avatar string `protobuf:"bytes,10,opt,name=avatar,proto3" json:"avatar,omitempty"`
Birth string `protobuf:"bytes,11,opt,name=birth,proto3" json:"birth,omitempty"`
AppVersion string `protobuf:"bytes,12,opt,name=app_version,json=appVersion,proto3" json:"app_version,omitempty"`
Source string `protobuf:"bytes,13,opt,name=source,proto3" json:"source,omitempty"`
Platform string `protobuf:"bytes,14,opt,name=platform,proto3" json:"platform,omitempty"`
Language string `protobuf:"bytes,15,opt,name=language,proto3" json:"language,omitempty"`
OsVersion string `protobuf:"bytes,16,opt,name=os_version,json=osVersion,proto3" json:"os_version,omitempty"`
BuildNumber string `protobuf:"bytes,17,opt,name=build_number,json=buildNumber,proto3" json:"build_number,omitempty"`
Timezone string `protobuf:"bytes,18,opt,name=timezone,proto3" json:"timezone,omitempty"`
InstallChannel string `protobuf:"bytes,19,opt,name=install_channel,json=installChannel,proto3" json:"install_channel,omitempty"`
Campaign string `protobuf:"bytes,20,opt,name=campaign,proto3" json:"campaign,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LoginThirdPartyRequest) Reset() {
@ -282,14 +281,13 @@ func (x *LoginThirdPartyRequest) GetCampaign() string {
// AuthResponse 返回标准认证令牌。
type AuthResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Token *AuthToken `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
IsNewUser bool `protobuf:"varint,2,opt,name=is_new_user,json=isNewUser,proto3" json:"is_new_user,omitempty"`
ProfileCompleted bool `protobuf:"varint,3,opt,name=profile_completed,json=profileCompleted,proto3" json:"profile_completed,omitempty"`
OnboardingStatus string `protobuf:"bytes,4,opt,name=onboarding_status,json=onboardingStatus,proto3" json:"onboarding_status,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
Token *AuthToken `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
IsNewUser bool `protobuf:"varint,2,opt,name=is_new_user,json=isNewUser,proto3" json:"is_new_user,omitempty"`
ProfileCompleted bool `protobuf:"varint,3,opt,name=profile_completed,json=profileCompleted,proto3" json:"profile_completed,omitempty"`
OnboardingStatus string `protobuf:"bytes,4,opt,name=onboarding_status,json=onboardingStatus,proto3" json:"onboarding_status,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AuthResponse) Reset() {
@ -352,13 +350,12 @@ func (x *AuthResponse) GetOnboardingStatus() string {
// SetPasswordRequest 为已经三方登录的用户首次设置密码。
type SetPasswordRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *SetPasswordRequest) Reset() {
@ -414,11 +411,10 @@ func (x *SetPasswordRequest) GetPassword() string {
// SetPasswordResponse 返回密码是否已经成功设置。
type SetPasswordResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
PasswordSet bool `protobuf:"varint,1,opt,name=password_set,json=passwordSet,proto3" json:"password_set,omitempty"`
unknownFields protoimpl.UnknownFields
PasswordSet bool `protobuf:"varint,1,opt,name=password_set,json=passwordSet,proto3" json:"password_set,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *SetPasswordResponse) Reset() {
@ -460,12 +456,11 @@ func (x *SetPasswordResponse) GetPasswordSet() bool {
// RefreshTokenRequest 使用 refresh token 换取新 token。
type RefreshTokenRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
RefreshToken string `protobuf:"bytes,2,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
RefreshToken string `protobuf:"bytes,2,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RefreshTokenRequest) Reset() {
@ -514,11 +509,10 @@ func (x *RefreshTokenRequest) GetRefreshToken() string {
// RefreshTokenResponse 返回轮换后的令牌。
type RefreshTokenResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Token *AuthToken `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
unknownFields protoimpl.UnknownFields
Token *AuthToken `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RefreshTokenResponse) Reset() {
@ -560,13 +554,12 @@ func (x *RefreshTokenResponse) GetToken() *AuthToken {
// LogoutRequest 失效指定会话或 refresh token。
type LogoutRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
RefreshToken string `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
RefreshToken string `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LogoutRequest) Reset() {
@ -622,11 +615,10 @@ func (x *LogoutRequest) GetRefreshToken() string {
// LogoutResponse 返回会话是否被失效。
type LogoutResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Revoked bool `protobuf:"varint,1,opt,name=revoked,proto3" json:"revoked,omitempty"`
unknownFields protoimpl.UnknownFields
Revoked bool `protobuf:"varint,1,opt,name=revoked,proto3" json:"revoked,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *LogoutResponse) Reset() {
@ -668,16 +660,15 @@ func (x *LogoutResponse) GetRevoked() bool {
// RecordLoginBlockedRequest 记录 gateway 入口快拦拒绝的登录审计。
type RecordLoginBlockedRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
Channel string `protobuf:"bytes,2,opt,name=channel,proto3" json:"channel,omitempty"`
Platform string `protobuf:"bytes,3,opt,name=platform,proto3" json:"platform,omitempty"`
Language string `protobuf:"bytes,4,opt,name=language,proto3" json:"language,omitempty"`
Timezone string `protobuf:"bytes,5,opt,name=timezone,proto3" json:"timezone,omitempty"`
BlockReason string `protobuf:"bytes,6,opt,name=block_reason,json=blockReason,proto3" json:"block_reason,omitempty"`
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
Channel string `protobuf:"bytes,2,opt,name=channel,proto3" json:"channel,omitempty"`
Platform string `protobuf:"bytes,3,opt,name=platform,proto3" json:"platform,omitempty"`
Language string `protobuf:"bytes,4,opt,name=language,proto3" json:"language,omitempty"`
Timezone string `protobuf:"bytes,5,opt,name=timezone,proto3" json:"timezone,omitempty"`
BlockReason string `protobuf:"bytes,6,opt,name=block_reason,json=blockReason,proto3" json:"block_reason,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RecordLoginBlockedRequest) Reset() {
@ -754,11 +745,10 @@ func (x *RecordLoginBlockedRequest) GetBlockReason() string {
// RecordLoginBlockedResponse 返回审计写入是否成功。
type RecordLoginBlockedResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Recorded bool `protobuf:"varint,1,opt,name=recorded,proto3" json:"recorded,omitempty"`
unknownFields protoimpl.UnknownFields
Recorded bool `protobuf:"varint,1,opt,name=recorded,proto3" json:"recorded,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RecordLoginBlockedResponse) Reset() {
@ -800,169 +790,88 @@ func (x *RecordLoginBlockedResponse) GetRecorded() bool {
var File_proto_user_v1_auth_proto protoreflect.FileDescriptor
var file_proto_user_v1_auth_proto_rawDesc = []byte{
0x0a, 0x18, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f,
0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x1a, 0x18, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0x8a, 0x01, 0x0a, 0x14, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04,
0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61,
0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x26, 0x0a, 0x0f,
0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x55, 0x73,
0x65, 0x72, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x22, 0xea, 0x04, 0x0a, 0x16, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50,
0x61, 0x72, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d,
0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x70,
0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70,
0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65,
0x6e, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x72, 0x65,
0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f,
0x75, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x76, 0x69, 0x74, 0x65, 0x5f,
0x63, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x76, 0x69,
0x74, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65,
0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63,
0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x09, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61,
0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x76, 0x61,
0x74, 0x61, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x69, 0x72, 0x74, 0x68, 0x18, 0x0b, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x62, 0x69, 0x72, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x70, 0x70,
0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
0x61, 0x70, 0x70, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x0e,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1a,
0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x73,
0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x6f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x75, 0x69,
0x6c, 0x64, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08,
0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74,
0x61, 0x6c, 0x6c, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x13, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65,
0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x6d, 0x70, 0x61, 0x69, 0x67, 0x6e, 0x18, 0x14, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x6d, 0x70, 0x61, 0x69, 0x67, 0x6e, 0x22, 0xb8, 0x01,
0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e,
0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e,
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75,
0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e,
0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x6e, 0x65, 0x77, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20,
0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x4e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x12, 0x2b,
0x0a, 0x11, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65,
0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x66, 0x69,
0x6c, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x2b, 0x0a, 0x11, 0x6f,
0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x69,
0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x79, 0x0a, 0x12, 0x53, 0x65, 0x74, 0x50,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e,
0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68,
0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17,
0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52,
0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77,
0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77,
0x6f, 0x72, 0x64, 0x22, 0x38, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
0x52, 0x0b, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x53, 0x65, 0x74, 0x22, 0x6a, 0x0a,
0x13, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04,
0x6d, 0x65, 0x74, 0x61, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f,
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x66,
0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x46, 0x0a, 0x14, 0x52, 0x65, 0x66,
0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x2e, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x18, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65,
0x6e, 0x22, 0x83, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76,
0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d,
0x65, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f,
0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65,
0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x2a, 0x0a, 0x0e, 0x4c, 0x6f, 0x67, 0x6f, 0x75,
0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x76,
0x6f, 0x6b, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x76, 0x6f,
0x6b, 0x65, 0x64, 0x22, 0xdc, 0x01, 0x0a, 0x19, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f,
0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x2e, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x1a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74,
0x61, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x70,
0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70,
0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75,
0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75,
0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x18,
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x12,
0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18,
0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x61, 0x73,
0x6f, 0x6e, 0x22, 0x38, 0x0a, 0x1a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69,
0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, 0x64, 0x32, 0x98, 0x04, 0x0a,
0x0b, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x0d,
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x23, 0x2e,
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f,
0x67, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x55, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72,
0x74, 0x79, 0x12, 0x25, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x68, 0x69, 0x72, 0x64, 0x50, 0x61, 0x72,
0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x21, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73,
0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72,
0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70,
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x50, 0x61, 0x73, 0x73,
0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c,
0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x2e, 0x68,
0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x66,
0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x23, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x2e, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12,
0x1c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e,
0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e,
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f,
0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x12,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
0x65, 0x64, 0x12, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c,
0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x68,
0x79, 0x61, 0x70, 0x70, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63,
0x6f, 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x68, 0x79, 0x61, 0x70, 0x70,
0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
const file_proto_user_v1_auth_proto_rawDesc = "" +
"\n" +
"\x18proto/user/v1/auth.proto\x12\rhyapp.user.v1\x1a\x18proto/user/v1/user.proto\"\x8a\x01\n" +
"\x14LoginPasswordRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12&\n" +
"\x0fdisplay_user_id\x18\x02 \x01(\tR\rdisplayUserId\x12\x1a\n" +
"\bpassword\x18\x03 \x01(\tR\bpassword\"\xea\x04\n" +
"\x16LoginThirdPartyRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x1a\n" +
"\bprovider\x18\x02 \x01(\tR\bprovider\x12\x1e\n" +
"\n" +
"credential\x18\x03 \x01(\tR\n" +
"credential\x12\x1a\n" +
"\busername\x18\x04 \x01(\tR\busername\x12\x16\n" +
"\x06gender\x18\x05 \x01(\tR\x06gender\x12\x18\n" +
"\acountry\x18\x06 \x01(\tR\acountry\x12\x1f\n" +
"\vinvite_code\x18\a \x01(\tR\n" +
"inviteCode\x12\x1b\n" +
"\tdevice_id\x18\b \x01(\tR\bdeviceId\x12\x16\n" +
"\x06device\x18\t \x01(\tR\x06device\x12\x16\n" +
"\x06avatar\x18\n" +
" \x01(\tR\x06avatar\x12\x14\n" +
"\x05birth\x18\v \x01(\tR\x05birth\x12\x1f\n" +
"\vapp_version\x18\f \x01(\tR\n" +
"appVersion\x12\x16\n" +
"\x06source\x18\r \x01(\tR\x06source\x12\x1a\n" +
"\bplatform\x18\x0e \x01(\tR\bplatform\x12\x1a\n" +
"\blanguage\x18\x0f \x01(\tR\blanguage\x12\x1d\n" +
"\n" +
"os_version\x18\x10 \x01(\tR\tosVersion\x12!\n" +
"\fbuild_number\x18\x11 \x01(\tR\vbuildNumber\x12\x1a\n" +
"\btimezone\x18\x12 \x01(\tR\btimezone\x12'\n" +
"\x0finstall_channel\x18\x13 \x01(\tR\x0einstallChannel\x12\x1a\n" +
"\bcampaign\x18\x14 \x01(\tR\bcampaign\"\xb8\x01\n" +
"\fAuthResponse\x12.\n" +
"\x05token\x18\x01 \x01(\v2\x18.hyapp.user.v1.AuthTokenR\x05token\x12\x1e\n" +
"\vis_new_user\x18\x02 \x01(\bR\tisNewUser\x12+\n" +
"\x11profile_completed\x18\x03 \x01(\bR\x10profileCompleted\x12+\n" +
"\x11onboarding_status\x18\x04 \x01(\tR\x10onboardingStatus\"y\n" +
"\x12SetPasswordRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x17\n" +
"\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x1a\n" +
"\bpassword\x18\x03 \x01(\tR\bpassword\"8\n" +
"\x13SetPasswordResponse\x12!\n" +
"\fpassword_set\x18\x01 \x01(\bR\vpasswordSet\"j\n" +
"\x13RefreshTokenRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12#\n" +
"\rrefresh_token\x18\x02 \x01(\tR\frefreshToken\"F\n" +
"\x14RefreshTokenResponse\x12.\n" +
"\x05token\x18\x01 \x01(\v2\x18.hyapp.user.v1.AuthTokenR\x05token\"\x83\x01\n" +
"\rLogoutRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x1d\n" +
"\n" +
"session_id\x18\x02 \x01(\tR\tsessionId\x12#\n" +
"\rrefresh_token\x18\x03 \x01(\tR\frefreshToken\"*\n" +
"\x0eLogoutResponse\x12\x18\n" +
"\arevoked\x18\x01 \x01(\bR\arevoked\"\xdc\x01\n" +
"\x19RecordLoginBlockedRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x18\n" +
"\achannel\x18\x02 \x01(\tR\achannel\x12\x1a\n" +
"\bplatform\x18\x03 \x01(\tR\bplatform\x12\x1a\n" +
"\blanguage\x18\x04 \x01(\tR\blanguage\x12\x1a\n" +
"\btimezone\x18\x05 \x01(\tR\btimezone\x12!\n" +
"\fblock_reason\x18\x06 \x01(\tR\vblockReason\"8\n" +
"\x1aRecordLoginBlockedResponse\x12\x1a\n" +
"\brecorded\x18\x01 \x01(\bR\brecorded2\x98\x04\n" +
"\vAuthService\x12Q\n" +
"\rLoginPassword\x12#.hyapp.user.v1.LoginPasswordRequest\x1a\x1b.hyapp.user.v1.AuthResponse\x12U\n" +
"\x0fLoginThirdParty\x12%.hyapp.user.v1.LoginThirdPartyRequest\x1a\x1b.hyapp.user.v1.AuthResponse\x12T\n" +
"\vSetPassword\x12!.hyapp.user.v1.SetPasswordRequest\x1a\".hyapp.user.v1.SetPasswordResponse\x12W\n" +
"\fRefreshToken\x12\".hyapp.user.v1.RefreshTokenRequest\x1a#.hyapp.user.v1.RefreshTokenResponse\x12E\n" +
"\x06Logout\x12\x1c.hyapp.user.v1.LogoutRequest\x1a\x1d.hyapp.user.v1.LogoutResponse\x12i\n" +
"\x12RecordLoginBlocked\x12(.hyapp.user.v1.RecordLoginBlockedRequest\x1a).hyapp.user.v1.RecordLoginBlockedResponseB&Z$hyapp.local/api/proto/user/v1;userv1b\x06proto3"
var (
file_proto_user_v1_auth_proto_rawDescOnce sync.Once
file_proto_user_v1_auth_proto_rawDescData = file_proto_user_v1_auth_proto_rawDesc
file_proto_user_v1_auth_proto_rawDescData []byte
)
func file_proto_user_v1_auth_proto_rawDescGZIP() []byte {
file_proto_user_v1_auth_proto_rawDescOnce.Do(func() {
file_proto_user_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_user_v1_auth_proto_rawDescData)
file_proto_user_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_user_v1_auth_proto_rawDesc), len(file_proto_user_v1_auth_proto_rawDesc)))
})
return file_proto_user_v1_auth_proto_rawDescData
}
@ -1021,7 +930,7 @@ func file_proto_user_v1_auth_proto_init() {
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_user_v1_auth_proto_rawDesc,
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_user_v1_auth_proto_rawDesc), len(file_proto_user_v1_auth_proto_rawDesc)),
NumEnums: 0,
NumMessages: 11,
NumExtensions: 0,
@ -1032,7 +941,6 @@ func file_proto_user_v1_auth_proto_init() {
MessageInfos: file_proto_user_v1_auth_proto_msgTypes,
}.Build()
File_proto_user_v1_auth_proto = out.File
file_proto_user_v1_auth_proto_rawDesc = nil
file_proto_user_v1_auth_proto_goTypes = nil
file_proto_user_v1_auth_proto_depIdxs = nil
}

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v4.25.3
// - protoc-gen-go-grpc v1.6.2
// - protoc v5.28.3
// source: proto/user/v1/auth.proto
package userv1
@ -132,22 +132,22 @@ type AuthServiceServer interface {
type UnimplementedAuthServiceServer struct{}
func (UnimplementedAuthServiceServer) LoginPassword(context.Context, *LoginPasswordRequest) (*AuthResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method LoginPassword not implemented")
return nil, status.Error(codes.Unimplemented, "method LoginPassword not implemented")
}
func (UnimplementedAuthServiceServer) LoginThirdParty(context.Context, *LoginThirdPartyRequest) (*AuthResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method LoginThirdParty not implemented")
return nil, status.Error(codes.Unimplemented, "method LoginThirdParty not implemented")
}
func (UnimplementedAuthServiceServer) SetPassword(context.Context, *SetPasswordRequest) (*SetPasswordResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetPassword not implemented")
return nil, status.Error(codes.Unimplemented, "method SetPassword not implemented")
}
func (UnimplementedAuthServiceServer) RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RefreshToken not implemented")
return nil, status.Error(codes.Unimplemented, "method RefreshToken not implemented")
}
func (UnimplementedAuthServiceServer) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented")
return nil, status.Error(codes.Unimplemented, "method Logout not implemented")
}
func (UnimplementedAuthServiceServer) RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RecordLoginBlocked not implemented")
return nil, status.Error(codes.Unimplemented, "method RecordLoginBlocked not implemented")
}
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
func (UnimplementedAuthServiceServer) testEmbeddedByValue() {}
@ -160,7 +160,7 @@ type UnsafeAuthServiceServer interface {
}
func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) {
// If the following call pancis, it indicates UnimplementedAuthServiceServer was
// If the following call panics, it indicates UnimplementedAuthServiceServer 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.

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v4.25.3
// - protoc-gen-go-grpc v1.6.2
// - protoc v5.28.3
// source: proto/user/v1/host.proto
package userv1
@ -236,46 +236,46 @@ type UserHostServiceServer interface {
type UnimplementedUserHostServiceServer struct{}
func (UnimplementedUserHostServiceServer) SearchAgencies(context.Context, *SearchAgenciesRequest) (*SearchAgenciesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SearchAgencies not implemented")
return nil, status.Error(codes.Unimplemented, "method SearchAgencies not implemented")
}
func (UnimplementedUserHostServiceServer) ApplyToAgency(context.Context, *ApplyToAgencyRequest) (*ApplyToAgencyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ApplyToAgency not implemented")
return nil, status.Error(codes.Unimplemented, "method ApplyToAgency not implemented")
}
func (UnimplementedUserHostServiceServer) ReviewAgencyApplication(context.Context, *ReviewAgencyApplicationRequest) (*ReviewAgencyApplicationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReviewAgencyApplication not implemented")
return nil, status.Error(codes.Unimplemented, "method ReviewAgencyApplication not implemented")
}
func (UnimplementedUserHostServiceServer) KickAgencyHost(context.Context, *KickAgencyHostRequest) (*KickAgencyHostResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method KickAgencyHost not implemented")
return nil, status.Error(codes.Unimplemented, "method KickAgencyHost not implemented")
}
func (UnimplementedUserHostServiceServer) InviteAgency(context.Context, *InviteAgencyRequest) (*InviteAgencyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InviteAgency not implemented")
return nil, status.Error(codes.Unimplemented, "method InviteAgency not implemented")
}
func (UnimplementedUserHostServiceServer) InviteBD(context.Context, *InviteBDRequest) (*InviteBDResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method InviteBD not implemented")
return nil, status.Error(codes.Unimplemented, "method InviteBD not implemented")
}
func (UnimplementedUserHostServiceServer) ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ProcessRoleInvitation not implemented")
return nil, status.Error(codes.Unimplemented, "method ProcessRoleInvitation not implemented")
}
func (UnimplementedUserHostServiceServer) GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetHostProfile not implemented")
return nil, status.Error(codes.Unimplemented, "method GetHostProfile not implemented")
}
func (UnimplementedUserHostServiceServer) GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetBDProfile not implemented")
return nil, status.Error(codes.Unimplemented, "method GetBDProfile not implemented")
}
func (UnimplementedUserHostServiceServer) GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetCoinSellerProfile not implemented")
return nil, status.Error(codes.Unimplemented, "method GetCoinSellerProfile not implemented")
}
func (UnimplementedUserHostServiceServer) GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserRoleSummary not implemented")
return nil, status.Error(codes.Unimplemented, "method GetUserRoleSummary not implemented")
}
func (UnimplementedUserHostServiceServer) CheckBusinessCapability(context.Context, *CheckBusinessCapabilityRequest) (*CheckBusinessCapabilityResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckBusinessCapability not implemented")
return nil, status.Error(codes.Unimplemented, "method CheckBusinessCapability not implemented")
}
func (UnimplementedUserHostServiceServer) GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyMembers not implemented")
return nil, status.Error(codes.Unimplemented, "method GetAgencyMembers not implemented")
}
func (UnimplementedUserHostServiceServer) GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyApplications not implemented")
return nil, status.Error(codes.Unimplemented, "method GetAgencyApplications not implemented")
}
func (UnimplementedUserHostServiceServer) mustEmbedUnimplementedUserHostServiceServer() {}
func (UnimplementedUserHostServiceServer) testEmbeddedByValue() {}
@ -288,7 +288,7 @@ type UnsafeUserHostServiceServer interface {
}
func RegisterUserHostServiceServer(s grpc.ServiceRegistrar, srv UserHostServiceServer) {
// If the following call pancis, it indicates UnimplementedUserHostServiceServer was
// If the following call panics, it indicates UnimplementedUserHostServiceServer 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.
@ -758,28 +758,28 @@ type UserHostAdminServiceServer interface {
type UnimplementedUserHostAdminServiceServer struct{}
func (UnimplementedUserHostAdminServiceServer) CreateBDLeader(context.Context, *CreateBDLeaderRequest) (*CreateBDLeaderResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateBDLeader not implemented")
return nil, status.Error(codes.Unimplemented, "method CreateBDLeader not implemented")
}
func (UnimplementedUserHostAdminServiceServer) CreateBD(context.Context, *CreateBDRequest) (*CreateBDResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateBD not implemented")
return nil, status.Error(codes.Unimplemented, "method CreateBD not implemented")
}
func (UnimplementedUserHostAdminServiceServer) SetBDStatus(context.Context, *SetBDStatusRequest) (*SetBDStatusResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetBDStatus not implemented")
return nil, status.Error(codes.Unimplemented, "method SetBDStatus not implemented")
}
func (UnimplementedUserHostAdminServiceServer) CreateCoinSeller(context.Context, *CreateCoinSellerRequest) (*CreateCoinSellerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateCoinSeller not implemented")
return nil, status.Error(codes.Unimplemented, "method CreateCoinSeller not implemented")
}
func (UnimplementedUserHostAdminServiceServer) SetCoinSellerStatus(context.Context, *SetCoinSellerStatusRequest) (*SetCoinSellerStatusResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetCoinSellerStatus not implemented")
return nil, status.Error(codes.Unimplemented, "method SetCoinSellerStatus not implemented")
}
func (UnimplementedUserHostAdminServiceServer) CreateAgency(context.Context, *CreateAgencyRequest) (*CreateAgencyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateAgency not implemented")
return nil, status.Error(codes.Unimplemented, "method CreateAgency not implemented")
}
func (UnimplementedUserHostAdminServiceServer) CloseAgency(context.Context, *CloseAgencyRequest) (*CloseAgencyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CloseAgency not implemented")
return nil, status.Error(codes.Unimplemented, "method CloseAgency not implemented")
}
func (UnimplementedUserHostAdminServiceServer) SetAgencyJoinEnabled(context.Context, *SetAgencyJoinEnabledRequest) (*SetAgencyJoinEnabledResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetAgencyJoinEnabled not implemented")
return nil, status.Error(codes.Unimplemented, "method SetAgencyJoinEnabled not implemented")
}
func (UnimplementedUserHostAdminServiceServer) mustEmbedUnimplementedUserHostAdminServiceServer() {}
func (UnimplementedUserHostAdminServiceServer) testEmbeddedByValue() {}
@ -792,7 +792,7 @@ type UnsafeUserHostAdminServiceServer interface {
}
func RegisterUserHostAdminServiceServer(s grpc.ServiceRegistrar, srv UserHostAdminServiceServer) {
// If the following call pancis, it indicates UnimplementedUserHostAdminServiceServer was
// If the following call panics, it indicates UnimplementedUserHostAdminServiceServer 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.

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc-gen-go-grpc v1.6.2
// - protoc v5.28.3
// source: proto/user/v1/user.proto
@ -184,34 +184,34 @@ type UserServiceServer interface {
type UnimplementedUserServiceServer struct{}
func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented")
return nil, status.Error(codes.Unimplemented, "method GetUser not implemented")
}
func (UnimplementedUserServiceServer) BusinessUserLookup(context.Context, *BusinessUserLookupRequest) (*BusinessUserLookupResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BusinessUserLookup not implemented")
return nil, status.Error(codes.Unimplemented, "method BusinessUserLookup not implemented")
}
func (UnimplementedUserServiceServer) GetMyProfileStats(context.Context, *GetMyProfileStatsRequest) (*GetMyProfileStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMyProfileStats not implemented")
return nil, status.Error(codes.Unimplemented, "method GetMyProfileStats not implemented")
}
func (UnimplementedUserServiceServer) BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BatchGetUsers not implemented")
return nil, status.Error(codes.Unimplemented, "method BatchGetUsers not implemented")
}
func (UnimplementedUserServiceServer) ListUserIDs(context.Context, *ListUserIDsRequest) (*ListUserIDsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListUserIDs not implemented")
return nil, status.Error(codes.Unimplemented, "method ListUserIDs not implemented")
}
func (UnimplementedUserServiceServer) GetUserMicLifetimeStats(context.Context, *GetUserMicLifetimeStatsRequest) (*GetUserMicLifetimeStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserMicLifetimeStats not implemented")
return nil, status.Error(codes.Unimplemented, "method GetUserMicLifetimeStats not implemented")
}
func (UnimplementedUserServiceServer) UpdateUserProfile(context.Context, *UpdateUserProfileRequest) (*UpdateUserProfileResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateUserProfile not implemented")
return nil, status.Error(codes.Unimplemented, "method UpdateUserProfile not implemented")
}
func (UnimplementedUserServiceServer) ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ChangeUserCountry not implemented")
return nil, status.Error(codes.Unimplemented, "method ChangeUserCountry not implemented")
}
func (UnimplementedUserServiceServer) SetUserStatus(context.Context, *SetUserStatusRequest) (*SetUserStatusResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetUserStatus not implemented")
return nil, status.Error(codes.Unimplemented, "method SetUserStatus not implemented")
}
func (UnimplementedUserServiceServer) CompleteOnboarding(context.Context, *CompleteOnboardingRequest) (*CompleteOnboardingResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CompleteOnboarding not implemented")
return nil, status.Error(codes.Unimplemented, "method CompleteOnboarding not implemented")
}
func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {}
func (UnimplementedUserServiceServer) testEmbeddedByValue() {}
@ -224,7 +224,7 @@ type UnsafeUserServiceServer interface {
}
func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) {
// If the following call pancis, it indicates UnimplementedUserServiceServer was
// If the following call panics, it indicates UnimplementedUserServiceServer 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.
@ -632,34 +632,34 @@ type UserSocialServiceServer interface {
type UnimplementedUserSocialServiceServer struct{}
func (UnimplementedUserSocialServiceServer) RecordProfileVisit(context.Context, *RecordProfileVisitRequest) (*RecordProfileVisitResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RecordProfileVisit not implemented")
return nil, status.Error(codes.Unimplemented, "method RecordProfileVisit not implemented")
}
func (UnimplementedUserSocialServiceServer) ListProfileVisitors(context.Context, *ListProfileVisitorsRequest) (*ListProfileVisitorsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListProfileVisitors not implemented")
return nil, status.Error(codes.Unimplemented, "method ListProfileVisitors not implemented")
}
func (UnimplementedUserSocialServiceServer) FollowUser(context.Context, *FollowUserRequest) (*FollowUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method FollowUser not implemented")
return nil, status.Error(codes.Unimplemented, "method FollowUser not implemented")
}
func (UnimplementedUserSocialServiceServer) UnfollowUser(context.Context, *UnfollowUserRequest) (*UnfollowUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UnfollowUser not implemented")
return nil, status.Error(codes.Unimplemented, "method UnfollowUser not implemented")
}
func (UnimplementedUserSocialServiceServer) ListFollowing(context.Context, *ListFollowingRequest) (*ListFollowingResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListFollowing not implemented")
return nil, status.Error(codes.Unimplemented, "method ListFollowing not implemented")
}
func (UnimplementedUserSocialServiceServer) ApplyFriend(context.Context, *ApplyFriendRequest) (*ApplyFriendResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ApplyFriend not implemented")
return nil, status.Error(codes.Unimplemented, "method ApplyFriend not implemented")
}
func (UnimplementedUserSocialServiceServer) AcceptFriendApplication(context.Context, *AcceptFriendApplicationRequest) (*AcceptFriendApplicationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AcceptFriendApplication not implemented")
return nil, status.Error(codes.Unimplemented, "method AcceptFriendApplication not implemented")
}
func (UnimplementedUserSocialServiceServer) DeleteFriend(context.Context, *DeleteFriendRequest) (*DeleteFriendResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteFriend not implemented")
return nil, status.Error(codes.Unimplemented, "method DeleteFriend not implemented")
}
func (UnimplementedUserSocialServiceServer) ListFriends(context.Context, *ListFriendsRequest) (*ListFriendsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListFriends not implemented")
return nil, status.Error(codes.Unimplemented, "method ListFriends not implemented")
}
func (UnimplementedUserSocialServiceServer) ListFriendApplications(context.Context, *ListFriendApplicationsRequest) (*ListFriendApplicationsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListFriendApplications not implemented")
return nil, status.Error(codes.Unimplemented, "method ListFriendApplications not implemented")
}
func (UnimplementedUserSocialServiceServer) mustEmbedUnimplementedUserSocialServiceServer() {}
func (UnimplementedUserSocialServiceServer) testEmbeddedByValue() {}
@ -672,7 +672,7 @@ type UnsafeUserSocialServiceServer interface {
}
func RegisterUserSocialServiceServer(s grpc.ServiceRegistrar, srv UserSocialServiceServer) {
// If the following call pancis, it indicates UnimplementedUserSocialServiceServer was
// If the following call panics, it indicates UnimplementedUserSocialServiceServer 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.
@ -989,13 +989,13 @@ type UserCronServiceServer interface {
type UnimplementedUserCronServiceServer struct{}
func (UnimplementedUserCronServiceServer) ProcessLoginIPRiskBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ProcessLoginIPRiskBatch not implemented")
return nil, status.Error(codes.Unimplemented, "method ProcessLoginIPRiskBatch not implemented")
}
func (UnimplementedUserCronServiceServer) ProcessRegionRebuildBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ProcessRegionRebuildBatch not implemented")
return nil, status.Error(codes.Unimplemented, "method ProcessRegionRebuildBatch not implemented")
}
func (UnimplementedUserCronServiceServer) CompensateMicOpenSessions(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CompensateMicOpenSessions not implemented")
return nil, status.Error(codes.Unimplemented, "method CompensateMicOpenSessions not implemented")
}
func (UnimplementedUserCronServiceServer) mustEmbedUnimplementedUserCronServiceServer() {}
func (UnimplementedUserCronServiceServer) testEmbeddedByValue() {}
@ -1008,7 +1008,7 @@ type UnsafeUserCronServiceServer interface {
}
func RegisterUserCronServiceServer(s grpc.ServiceRegistrar, srv UserCronServiceServer) {
// If the following call pancis, it indicates UnimplementedUserCronServiceServer was
// If the following call panics, it indicates UnimplementedUserCronServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
@ -1158,10 +1158,10 @@ type UserDeviceServiceServer interface {
type UnimplementedUserDeviceServiceServer struct{}
func (UnimplementedUserDeviceServiceServer) BindPushToken(context.Context, *BindPushTokenRequest) (*BindPushTokenResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BindPushToken not implemented")
return nil, status.Error(codes.Unimplemented, "method BindPushToken not implemented")
}
func (UnimplementedUserDeviceServiceServer) DeletePushToken(context.Context, *DeletePushTokenRequest) (*DeletePushTokenResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeletePushToken not implemented")
return nil, status.Error(codes.Unimplemented, "method DeletePushToken not implemented")
}
func (UnimplementedUserDeviceServiceServer) mustEmbedUnimplementedUserDeviceServiceServer() {}
func (UnimplementedUserDeviceServiceServer) testEmbeddedByValue() {}
@ -1174,7 +1174,7 @@ type UnsafeUserDeviceServiceServer interface {
}
func RegisterUserDeviceServiceServer(s grpc.ServiceRegistrar, srv UserDeviceServiceServer) {
// If the following call pancis, it indicates UnimplementedUserDeviceServiceServer was
// If the following call panics, it indicates UnimplementedUserDeviceServiceServer 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.
@ -1289,7 +1289,7 @@ type AppRegistryServiceServer interface {
type UnimplementedAppRegistryServiceServer struct{}
func (UnimplementedAppRegistryServiceServer) ResolveApp(context.Context, *ResolveAppRequest) (*ResolveAppResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ResolveApp not implemented")
return nil, status.Error(codes.Unimplemented, "method ResolveApp not implemented")
}
func (UnimplementedAppRegistryServiceServer) mustEmbedUnimplementedAppRegistryServiceServer() {}
func (UnimplementedAppRegistryServiceServer) testEmbeddedByValue() {}
@ -1302,7 +1302,7 @@ type UnsafeAppRegistryServiceServer interface {
}
func RegisterAppRegistryServiceServer(s grpc.ServiceRegistrar, srv AppRegistryServiceServer) {
// If the following call pancis, it indicates UnimplementedAppRegistryServiceServer was
// If the following call panics, it indicates UnimplementedAppRegistryServiceServer 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.
@ -1410,10 +1410,10 @@ type CountryAdminServiceServer interface {
type UnimplementedCountryAdminServiceServer struct{}
func (UnimplementedCountryAdminServiceServer) ListCountries(context.Context, *ListCountriesRequest) (*ListCountriesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListCountries not implemented")
return nil, status.Error(codes.Unimplemented, "method ListCountries not implemented")
}
func (UnimplementedCountryAdminServiceServer) UpdateCountry(context.Context, *UpdateCountryRequest) (*CountryResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateCountry not implemented")
return nil, status.Error(codes.Unimplemented, "method UpdateCountry not implemented")
}
func (UnimplementedCountryAdminServiceServer) mustEmbedUnimplementedCountryAdminServiceServer() {}
func (UnimplementedCountryAdminServiceServer) testEmbeddedByValue() {}
@ -1426,7 +1426,7 @@ type UnsafeCountryAdminServiceServer interface {
}
func RegisterCountryAdminServiceServer(s grpc.ServiceRegistrar, srv CountryAdminServiceServer) {
// If the following call pancis, it indicates UnimplementedCountryAdminServiceServer was
// If the following call panics, it indicates UnimplementedCountryAdminServiceServer 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.
@ -1554,10 +1554,10 @@ type CountryQueryServiceServer interface {
type UnimplementedCountryQueryServiceServer struct{}
func (UnimplementedCountryQueryServiceServer) ListRegistrationCountries(context.Context, *ListRegistrationCountriesRequest) (*ListRegistrationCountriesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRegistrationCountries not implemented")
return nil, status.Error(codes.Unimplemented, "method ListRegistrationCountries not implemented")
}
func (UnimplementedCountryQueryServiceServer) ListLoginRiskBlockedCountries(context.Context, *ListLoginRiskBlockedCountriesRequest) (*ListLoginRiskBlockedCountriesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListLoginRiskBlockedCountries not implemented")
return nil, status.Error(codes.Unimplemented, "method ListLoginRiskBlockedCountries not implemented")
}
func (UnimplementedCountryQueryServiceServer) mustEmbedUnimplementedCountryQueryServiceServer() {}
func (UnimplementedCountryQueryServiceServer) testEmbeddedByValue() {}
@ -1570,7 +1570,7 @@ type UnsafeCountryQueryServiceServer interface {
}
func RegisterCountryQueryServiceServer(s grpc.ServiceRegistrar, srv CountryQueryServiceServer) {
// If the following call pancis, it indicates UnimplementedCountryQueryServiceServer was
// If the following call panics, it indicates UnimplementedCountryQueryServiceServer 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.
@ -1726,16 +1726,16 @@ type RegionAdminServiceServer interface {
type UnimplementedRegionAdminServiceServer struct{}
func (UnimplementedRegionAdminServiceServer) ListRegions(context.Context, *ListRegionsRequest) (*ListRegionsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRegions not implemented")
return nil, status.Error(codes.Unimplemented, "method ListRegions not implemented")
}
func (UnimplementedRegionAdminServiceServer) GetRegion(context.Context, *GetRegionRequest) (*RegionResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetRegion not implemented")
return nil, status.Error(codes.Unimplemented, "method GetRegion not implemented")
}
func (UnimplementedRegionAdminServiceServer) UpdateRegion(context.Context, *UpdateRegionRequest) (*RegionResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateRegion not implemented")
return nil, status.Error(codes.Unimplemented, "method UpdateRegion not implemented")
}
func (UnimplementedRegionAdminServiceServer) ReplaceRegionCountries(context.Context, *ReplaceRegionCountriesRequest) (*RegionResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReplaceRegionCountries not implemented")
return nil, status.Error(codes.Unimplemented, "method ReplaceRegionCountries not implemented")
}
func (UnimplementedRegionAdminServiceServer) mustEmbedUnimplementedRegionAdminServiceServer() {}
func (UnimplementedRegionAdminServiceServer) testEmbeddedByValue() {}
@ -1748,7 +1748,7 @@ type UnsafeRegionAdminServiceServer interface {
}
func RegisterRegionAdminServiceServer(s grpc.ServiceRegistrar, srv RegionAdminServiceServer) {
// If the following call pancis, it indicates UnimplementedRegionAdminServiceServer was
// If the following call panics, it indicates UnimplementedRegionAdminServiceServer 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.
@ -1959,19 +1959,19 @@ type UserIdentityServiceServer interface {
type UnimplementedUserIdentityServiceServer struct{}
func (UnimplementedUserIdentityServiceServer) GetUserIdentity(context.Context, *GetUserIdentityRequest) (*GetUserIdentityResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserIdentity not implemented")
return nil, status.Error(codes.Unimplemented, "method GetUserIdentity not implemented")
}
func (UnimplementedUserIdentityServiceServer) ResolveDisplayUserID(context.Context, *ResolveDisplayUserIDRequest) (*ResolveDisplayUserIDResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ResolveDisplayUserID not implemented")
return nil, status.Error(codes.Unimplemented, "method ResolveDisplayUserID not implemented")
}
func (UnimplementedUserIdentityServiceServer) ChangeDisplayUserID(context.Context, *ChangeDisplayUserIDRequest) (*ChangeDisplayUserIDResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ChangeDisplayUserID not implemented")
return nil, status.Error(codes.Unimplemented, "method ChangeDisplayUserID not implemented")
}
func (UnimplementedUserIdentityServiceServer) ApplyPrettyDisplayUserID(context.Context, *ApplyPrettyDisplayUserIDRequest) (*ApplyPrettyDisplayUserIDResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ApplyPrettyDisplayUserID not implemented")
return nil, status.Error(codes.Unimplemented, "method ApplyPrettyDisplayUserID not implemented")
}
func (UnimplementedUserIdentityServiceServer) ExpirePrettyDisplayUserID(context.Context, *ExpirePrettyDisplayUserIDRequest) (*ExpirePrettyDisplayUserIDResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ExpirePrettyDisplayUserID not implemented")
return nil, status.Error(codes.Unimplemented, "method ExpirePrettyDisplayUserID not implemented")
}
func (UnimplementedUserIdentityServiceServer) mustEmbedUnimplementedUserIdentityServiceServer() {}
func (UnimplementedUserIdentityServiceServer) testEmbeddedByValue() {}
@ -1984,7 +1984,7 @@ type UnsafeUserIdentityServiceServer interface {
}
func RegisterUserIdentityServiceServer(s grpc.ServiceRegistrar, srv UserIdentityServiceServer) {
// If the following call pancis, it indicates UnimplementedUserIdentityServiceServer was
// If the following call panics, it indicates UnimplementedUserIdentityServiceServer 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.

File diff suppressed because it is too large Load Diff

View File

@ -205,6 +205,19 @@ message GiftConfig {
repeated string effect_types = 22;
}
message GiftTypeConfig {
string app_code = 1;
string type_code = 2;
string name = 3;
string tab_key = 4;
string status = 5;
int32 sort_order = 6;
int64 created_by_user_id = 7;
int64 updated_by_user_id = 8;
int64 created_at_ms = 9;
int64 updated_at_ms = 10;
}
message UserResourceEntitlement {
string app_code = 1;
string entitlement_id = 2;
@ -421,6 +434,32 @@ message ListGiftConfigsResponse {
int64 total = 2;
}
message ListGiftTypeConfigsRequest {
string request_id = 1;
string app_code = 2;
string status = 3;
bool active_only = 4;
}
message ListGiftTypeConfigsResponse {
repeated GiftTypeConfig gift_types = 1;
}
message UpsertGiftTypeConfigRequest {
string request_id = 1;
string app_code = 2;
string type_code = 3;
string name = 4;
string tab_key = 5;
string status = 6;
int32 sort_order = 7;
int64 operator_user_id = 8;
}
message GiftTypeConfigResponse {
GiftTypeConfig gift_type = 1;
}
message CreateGiftConfigRequest {
string request_id = 1;
string app_code = 2;
@ -959,9 +998,11 @@ service WalletService {
rpc UpdateResourceGroup(UpdateResourceGroupRequest) returns (ResourceGroupResponse);
rpc SetResourceGroupStatus(SetResourceGroupStatusRequest) returns (ResourceGroupResponse);
rpc ListGiftConfigs(ListGiftConfigsRequest) returns (ListGiftConfigsResponse);
rpc ListGiftTypeConfigs(ListGiftTypeConfigsRequest) returns (ListGiftTypeConfigsResponse);
rpc CreateGiftConfig(CreateGiftConfigRequest) returns (GiftConfigResponse);
rpc UpdateGiftConfig(UpdateGiftConfigRequest) returns (GiftConfigResponse);
rpc SetGiftConfigStatus(SetGiftConfigStatusRequest) returns (GiftConfigResponse);
rpc UpsertGiftTypeConfig(UpsertGiftTypeConfigRequest) returns (GiftTypeConfigResponse);
rpc GrantResource(GrantResourceRequest) returns (ResourceGrantResponse);
rpc GrantResourceGroup(GrantResourceGroupRequest) returns (ResourceGrantResponse);
rpc ListUserResources(ListUserResourcesRequest) returns (ListUserResourcesResponse);

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v4.25.3
// - protoc-gen-go-grpc v1.6.2
// - protoc (unknown)
// source: proto/wallet/v1/wallet.proto
package walletv1
@ -35,9 +35,11 @@ const (
WalletService_UpdateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResourceGroup"
WalletService_SetResourceGroupStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceGroupStatus"
WalletService_ListGiftConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftConfigs"
WalletService_ListGiftTypeConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftTypeConfigs"
WalletService_CreateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateGiftConfig"
WalletService_UpdateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateGiftConfig"
WalletService_SetGiftConfigStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetGiftConfigStatus"
WalletService_UpsertGiftTypeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertGiftTypeConfig"
WalletService_GrantResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResource"
WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup"
WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources"
@ -84,9 +86,11 @@ type WalletServiceClient interface {
UpdateResourceGroup(ctx context.Context, in *UpdateResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGroupResponse, error)
SetResourceGroupStatus(ctx context.Context, in *SetResourceGroupStatusRequest, opts ...grpc.CallOption) (*ResourceGroupResponse, error)
ListGiftConfigs(ctx context.Context, in *ListGiftConfigsRequest, opts ...grpc.CallOption) (*ListGiftConfigsResponse, error)
ListGiftTypeConfigs(ctx context.Context, in *ListGiftTypeConfigsRequest, opts ...grpc.CallOption) (*ListGiftTypeConfigsResponse, error)
CreateGiftConfig(ctx context.Context, in *CreateGiftConfigRequest, opts ...grpc.CallOption) (*GiftConfigResponse, error)
UpdateGiftConfig(ctx context.Context, in *UpdateGiftConfigRequest, opts ...grpc.CallOption) (*GiftConfigResponse, error)
SetGiftConfigStatus(ctx context.Context, in *SetGiftConfigStatusRequest, opts ...grpc.CallOption) (*GiftConfigResponse, error)
UpsertGiftTypeConfig(ctx context.Context, in *UpsertGiftTypeConfigRequest, opts ...grpc.CallOption) (*GiftTypeConfigResponse, error)
GrantResource(ctx context.Context, in *GrantResourceRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
GrantResourceGroup(ctx context.Context, in *GrantResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error)
@ -279,6 +283,16 @@ func (c *walletServiceClient) ListGiftConfigs(ctx context.Context, in *ListGiftC
return out, nil
}
func (c *walletServiceClient) ListGiftTypeConfigs(ctx context.Context, in *ListGiftTypeConfigsRequest, opts ...grpc.CallOption) (*ListGiftTypeConfigsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListGiftTypeConfigsResponse)
err := c.cc.Invoke(ctx, WalletService_ListGiftTypeConfigs_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) CreateGiftConfig(ctx context.Context, in *CreateGiftConfigRequest, opts ...grpc.CallOption) (*GiftConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GiftConfigResponse)
@ -309,6 +323,16 @@ func (c *walletServiceClient) SetGiftConfigStatus(ctx context.Context, in *SetGi
return out, nil
}
func (c *walletServiceClient) UpsertGiftTypeConfig(ctx context.Context, in *UpsertGiftTypeConfigRequest, opts ...grpc.CallOption) (*GiftTypeConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GiftTypeConfigResponse)
err := c.cc.Invoke(ctx, WalletService_UpsertGiftTypeConfig_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) GrantResource(ctx context.Context, in *GrantResourceRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResourceGrantResponse)
@ -551,9 +575,11 @@ type WalletServiceServer interface {
UpdateResourceGroup(context.Context, *UpdateResourceGroupRequest) (*ResourceGroupResponse, error)
SetResourceGroupStatus(context.Context, *SetResourceGroupStatusRequest) (*ResourceGroupResponse, error)
ListGiftConfigs(context.Context, *ListGiftConfigsRequest) (*ListGiftConfigsResponse, error)
ListGiftTypeConfigs(context.Context, *ListGiftTypeConfigsRequest) (*ListGiftTypeConfigsResponse, error)
CreateGiftConfig(context.Context, *CreateGiftConfigRequest) (*GiftConfigResponse, error)
UpdateGiftConfig(context.Context, *UpdateGiftConfigRequest) (*GiftConfigResponse, error)
SetGiftConfigStatus(context.Context, *SetGiftConfigStatusRequest) (*GiftConfigResponse, error)
UpsertGiftTypeConfig(context.Context, *UpsertGiftTypeConfigRequest) (*GiftTypeConfigResponse, error)
GrantResource(context.Context, *GrantResourceRequest) (*ResourceGrantResponse, error)
GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error)
ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error)
@ -587,127 +613,133 @@ type WalletServiceServer interface {
type UnimplementedWalletServiceServer struct{}
func (UnimplementedWalletServiceServer) DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DebitGift not implemented")
return nil, status.Error(codes.Unimplemented, "method DebitGift not implemented")
}
func (UnimplementedWalletServiceServer) GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetBalances not implemented")
return nil, status.Error(codes.Unimplemented, "method GetBalances not implemented")
}
func (UnimplementedWalletServiceServer) AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AdminCreditAsset not implemented")
return nil, status.Error(codes.Unimplemented, "method AdminCreditAsset not implemented")
}
func (UnimplementedWalletServiceServer) AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AdminCreditCoinSellerStock not implemented")
return nil, status.Error(codes.Unimplemented, "method AdminCreditCoinSellerStock not implemented")
}
func (UnimplementedWalletServiceServer) TransferCoinFromSeller(context.Context, *TransferCoinFromSellerRequest) (*TransferCoinFromSellerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method TransferCoinFromSeller not implemented")
return nil, status.Error(codes.Unimplemented, "method TransferCoinFromSeller not implemented")
}
func (UnimplementedWalletServiceServer) ListResources(context.Context, *ListResourcesRequest) (*ListResourcesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListResources not implemented")
return nil, status.Error(codes.Unimplemented, "method ListResources not implemented")
}
func (UnimplementedWalletServiceServer) GetResource(context.Context, *GetResourceRequest) (*GetResourceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetResource not implemented")
return nil, status.Error(codes.Unimplemented, "method GetResource not implemented")
}
func (UnimplementedWalletServiceServer) CreateResource(context.Context, *CreateResourceRequest) (*ResourceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateResource not implemented")
return nil, status.Error(codes.Unimplemented, "method CreateResource not implemented")
}
func (UnimplementedWalletServiceServer) UpdateResource(context.Context, *UpdateResourceRequest) (*ResourceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateResource not implemented")
return nil, status.Error(codes.Unimplemented, "method UpdateResource not implemented")
}
func (UnimplementedWalletServiceServer) SetResourceStatus(context.Context, *SetResourceStatusRequest) (*ResourceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetResourceStatus not implemented")
return nil, status.Error(codes.Unimplemented, "method SetResourceStatus not implemented")
}
func (UnimplementedWalletServiceServer) ListResourceGroups(context.Context, *ListResourceGroupsRequest) (*ListResourceGroupsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListResourceGroups not implemented")
return nil, status.Error(codes.Unimplemented, "method ListResourceGroups not implemented")
}
func (UnimplementedWalletServiceServer) GetResourceGroup(context.Context, *GetResourceGroupRequest) (*GetResourceGroupResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetResourceGroup not implemented")
return nil, status.Error(codes.Unimplemented, "method GetResourceGroup not implemented")
}
func (UnimplementedWalletServiceServer) CreateResourceGroup(context.Context, *CreateResourceGroupRequest) (*ResourceGroupResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateResourceGroup not implemented")
return nil, status.Error(codes.Unimplemented, "method CreateResourceGroup not implemented")
}
func (UnimplementedWalletServiceServer) UpdateResourceGroup(context.Context, *UpdateResourceGroupRequest) (*ResourceGroupResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateResourceGroup not implemented")
return nil, status.Error(codes.Unimplemented, "method UpdateResourceGroup not implemented")
}
func (UnimplementedWalletServiceServer) SetResourceGroupStatus(context.Context, *SetResourceGroupStatusRequest) (*ResourceGroupResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetResourceGroupStatus not implemented")
return nil, status.Error(codes.Unimplemented, "method SetResourceGroupStatus not implemented")
}
func (UnimplementedWalletServiceServer) ListGiftConfigs(context.Context, *ListGiftConfigsRequest) (*ListGiftConfigsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListGiftConfigs not implemented")
return nil, status.Error(codes.Unimplemented, "method ListGiftConfigs not implemented")
}
func (UnimplementedWalletServiceServer) ListGiftTypeConfigs(context.Context, *ListGiftTypeConfigsRequest) (*ListGiftTypeConfigsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListGiftTypeConfigs not implemented")
}
func (UnimplementedWalletServiceServer) CreateGiftConfig(context.Context, *CreateGiftConfigRequest) (*GiftConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateGiftConfig not implemented")
return nil, status.Error(codes.Unimplemented, "method CreateGiftConfig not implemented")
}
func (UnimplementedWalletServiceServer) UpdateGiftConfig(context.Context, *UpdateGiftConfigRequest) (*GiftConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateGiftConfig not implemented")
return nil, status.Error(codes.Unimplemented, "method UpdateGiftConfig not implemented")
}
func (UnimplementedWalletServiceServer) SetGiftConfigStatus(context.Context, *SetGiftConfigStatusRequest) (*GiftConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetGiftConfigStatus not implemented")
return nil, status.Error(codes.Unimplemented, "method SetGiftConfigStatus not implemented")
}
func (UnimplementedWalletServiceServer) UpsertGiftTypeConfig(context.Context, *UpsertGiftTypeConfigRequest) (*GiftTypeConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertGiftTypeConfig not implemented")
}
func (UnimplementedWalletServiceServer) GrantResource(context.Context, *GrantResourceRequest) (*ResourceGrantResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GrantResource not implemented")
return nil, status.Error(codes.Unimplemented, "method GrantResource not implemented")
}
func (UnimplementedWalletServiceServer) GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GrantResourceGroup not implemented")
return nil, status.Error(codes.Unimplemented, "method GrantResourceGroup not implemented")
}
func (UnimplementedWalletServiceServer) ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListUserResources not implemented")
return nil, status.Error(codes.Unimplemented, "method ListUserResources not implemented")
}
func (UnimplementedWalletServiceServer) EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method EquipUserResource not implemented")
return nil, status.Error(codes.Unimplemented, "method EquipUserResource not implemented")
}
func (UnimplementedWalletServiceServer) ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListResourceGrants not implemented")
return nil, status.Error(codes.Unimplemented, "method ListResourceGrants not implemented")
}
func (UnimplementedWalletServiceServer) ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRechargeBills not implemented")
return nil, status.Error(codes.Unimplemented, "method ListRechargeBills not implemented")
}
func (UnimplementedWalletServiceServer) GetWalletOverview(context.Context, *GetWalletOverviewRequest) (*GetWalletOverviewResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetWalletOverview not implemented")
return nil, status.Error(codes.Unimplemented, "method GetWalletOverview not implemented")
}
func (UnimplementedWalletServiceServer) GetWalletValueSummary(context.Context, *GetWalletValueSummaryRequest) (*GetWalletValueSummaryResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetWalletValueSummary not implemented")
return nil, status.Error(codes.Unimplemented, "method GetWalletValueSummary not implemented")
}
func (UnimplementedWalletServiceServer) GetUserGiftWall(context.Context, *GetUserGiftWallRequest) (*GetUserGiftWallResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserGiftWall not implemented")
return nil, status.Error(codes.Unimplemented, "method GetUserGiftWall not implemented")
}
func (UnimplementedWalletServiceServer) ListRechargeProducts(context.Context, *ListRechargeProductsRequest) (*ListRechargeProductsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRechargeProducts not implemented")
return nil, status.Error(codes.Unimplemented, "method ListRechargeProducts not implemented")
}
func (UnimplementedWalletServiceServer) ListAdminRechargeProducts(context.Context, *ListAdminRechargeProductsRequest) (*ListAdminRechargeProductsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListAdminRechargeProducts not implemented")
return nil, status.Error(codes.Unimplemented, "method ListAdminRechargeProducts not implemented")
}
func (UnimplementedWalletServiceServer) CreateRechargeProduct(context.Context, *CreateRechargeProductRequest) (*RechargeProductResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateRechargeProduct not implemented")
return nil, status.Error(codes.Unimplemented, "method CreateRechargeProduct not implemented")
}
func (UnimplementedWalletServiceServer) UpdateRechargeProduct(context.Context, *UpdateRechargeProductRequest) (*RechargeProductResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateRechargeProduct not implemented")
return nil, status.Error(codes.Unimplemented, "method UpdateRechargeProduct not implemented")
}
func (UnimplementedWalletServiceServer) DeleteRechargeProduct(context.Context, *DeleteRechargeProductRequest) (*DeleteRechargeProductResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteRechargeProduct not implemented")
return nil, status.Error(codes.Unimplemented, "method DeleteRechargeProduct not implemented")
}
func (UnimplementedWalletServiceServer) GetDiamondExchangeConfig(context.Context, *GetDiamondExchangeConfigRequest) (*GetDiamondExchangeConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDiamondExchangeConfig not implemented")
return nil, status.Error(codes.Unimplemented, "method GetDiamondExchangeConfig not implemented")
}
func (UnimplementedWalletServiceServer) ListWalletTransactions(context.Context, *ListWalletTransactionsRequest) (*ListWalletTransactionsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListWalletTransactions not implemented")
return nil, status.Error(codes.Unimplemented, "method ListWalletTransactions not implemented")
}
func (UnimplementedWalletServiceServer) ApplyWithdrawal(context.Context, *ApplyWithdrawalRequest) (*ApplyWithdrawalResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ApplyWithdrawal not implemented")
return nil, status.Error(codes.Unimplemented, "method ApplyWithdrawal not implemented")
}
func (UnimplementedWalletServiceServer) ListVipPackages(context.Context, *ListVipPackagesRequest) (*ListVipPackagesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListVipPackages not implemented")
return nil, status.Error(codes.Unimplemented, "method ListVipPackages not implemented")
}
func (UnimplementedWalletServiceServer) GetMyVip(context.Context, *GetMyVipRequest) (*GetMyVipResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMyVip not implemented")
return nil, status.Error(codes.Unimplemented, "method GetMyVip not implemented")
}
func (UnimplementedWalletServiceServer) PurchaseVip(context.Context, *PurchaseVipRequest) (*PurchaseVipResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method PurchaseVip not implemented")
return nil, status.Error(codes.Unimplemented, "method PurchaseVip not implemented")
}
func (UnimplementedWalletServiceServer) CreditTaskReward(context.Context, *CreditTaskRewardRequest) (*CreditTaskRewardResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreditTaskReward not implemented")
return nil, status.Error(codes.Unimplemented, "method CreditTaskReward not implemented")
}
func (UnimplementedWalletServiceServer) ApplyGameCoinChange(context.Context, *ApplyGameCoinChangeRequest) (*ApplyGameCoinChangeResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ApplyGameCoinChange not implemented")
return nil, status.Error(codes.Unimplemented, "method ApplyGameCoinChange not implemented")
}
func (UnimplementedWalletServiceServer) mustEmbedUnimplementedWalletServiceServer() {}
func (UnimplementedWalletServiceServer) testEmbeddedByValue() {}
@ -720,7 +752,7 @@ type UnsafeWalletServiceServer interface {
}
func RegisterWalletServiceServer(s grpc.ServiceRegistrar, srv WalletServiceServer) {
// If the following call pancis, it indicates UnimplementedWalletServiceServer was
// If the following call panics, it indicates UnimplementedWalletServiceServer 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.
@ -1018,6 +1050,24 @@ func _WalletService_ListGiftConfigs_Handler(srv interface{}, ctx context.Context
return interceptor(ctx, in, info, handler)
}
func _WalletService_ListGiftTypeConfigs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListGiftTypeConfigsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).ListGiftTypeConfigs(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_ListGiftTypeConfigs_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).ListGiftTypeConfigs(ctx, req.(*ListGiftTypeConfigsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_CreateGiftConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateGiftConfigRequest)
if err := dec(in); err != nil {
@ -1072,6 +1122,24 @@ func _WalletService_SetGiftConfigStatus_Handler(srv interface{}, ctx context.Con
return interceptor(ctx, in, info, handler)
}
func _WalletService_UpsertGiftTypeConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpsertGiftTypeConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).UpsertGiftTypeConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_UpsertGiftTypeConfig_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).UpsertGiftTypeConfig(ctx, req.(*UpsertGiftTypeConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_GrantResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GrantResourceRequest)
if err := dec(in); err != nil {
@ -1539,6 +1607,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListGiftConfigs",
Handler: _WalletService_ListGiftConfigs_Handler,
},
{
MethodName: "ListGiftTypeConfigs",
Handler: _WalletService_ListGiftTypeConfigs_Handler,
},
{
MethodName: "CreateGiftConfig",
Handler: _WalletService_CreateGiftConfig_Handler,
@ -1551,6 +1623,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
MethodName: "SetGiftConfigStatus",
Handler: _WalletService_SetGiftConfigStatus_Handler,
},
{
MethodName: "UpsertGiftTypeConfig",
Handler: _WalletService_UpsertGiftTypeConfig_Handler,
},
{
MethodName: "GrantResource",
Handler: _WalletService_GrantResource_Handler,

View File

@ -1,8 +1,9 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- MySQL's docker entrypoint creates MYSQL_DATABASE before service DDL runs.
-- These ALTER statements keep both fresh and long-lived local volumes on the
-- same utf8mb4 collation so Chinese text and emoji are stored by default.
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
-- 本地 Docker 入口脚本会先创建 MYSQL_DATABASE服务 DDL 随后创建各业务库。
-- 这里统一修复新旧本地卷的默认字符集,保证中文文本、中文 COMMENT 和 emoji 不被旧默认字符集截断。
ALTER DATABASE hyapp_room CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER DATABASE hyapp_user CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER DATABASE hyapp_wallet CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -1,3 +1,5 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
CREATE DATABASE IF NOT EXISTS hyapp_admin DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -1,3 +1,7 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
USE hyapp_wallet;
DROP PROCEDURE IF EXISTS migrate_resource_group_wallet_asset_items;
@ -11,7 +15,7 @@ BEGIN
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resource_group_items' AND COLUMN_NAME = 'item_type'
) THEN
ALTER TABLE resource_group_items
ADD COLUMN item_type VARCHAR(32) NOT NULL DEFAULT 'resource' AFTER group_id;
ADD COLUMN item_type VARCHAR(32) NOT NULL DEFAULT 'resource' COMMENT '明细类型' AFTER group_id;
END IF;
IF NOT EXISTS (
@ -19,7 +23,7 @@ BEGIN
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resource_group_items' AND COLUMN_NAME = 'wallet_asset_type'
) THEN
ALTER TABLE resource_group_items
ADD COLUMN wallet_asset_type VARCHAR(32) NOT NULL DEFAULT '' AFTER resource_id;
ADD COLUMN wallet_asset_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '钱包资产类型' AFTER resource_id;
END IF;
IF NOT EXISTS (
@ -27,7 +31,7 @@ BEGIN
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resource_group_items' AND COLUMN_NAME = 'wallet_asset_amount'
) THEN
ALTER TABLE resource_group_items
ADD COLUMN wallet_asset_amount BIGINT NOT NULL DEFAULT 0 AFTER wallet_asset_type;
ADD COLUMN wallet_asset_amount BIGINT NOT NULL DEFAULT 0 COMMENT '钱包资产数量' AFTER wallet_asset_type;
END IF;
IF EXISTS (

View File

@ -1,6 +1,10 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
USE hyapp_user;
-- Keep the country master data aligned for local databases that already ran the original seed.
-- 对已经执行过旧国家种子的本地库做幂等修正,保证国家主数据与当前产品范围一致。
DELETE FROM region_countries
WHERE app_code = 'lalu'
AND country_code = 'TW';

View File

@ -1,5 +1,7 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
CREATE USER IF NOT EXISTS 'hyapp'@'%' IDENTIFIED BY 'hyapp';
GRANT ALL PRIVILEGES ON hyapp_room.* TO 'hyapp'@'%';

View File

@ -0,0 +1,705 @@
# 七日签到奖励架构
本文定义 App 七日签到奖励的产品规则、服务边界、后台管理、接口契约、数据模型和落地顺序。七日签到属于活动事实,签到状态和发奖状态由 `activity-service` 拥有;奖励内容统一引用 `wallet-service` 资源组,不能把资源明细复制进签到配置。
## 目标
- 后台 `活动管理` 下新增二级菜单 `七日签到`
- 后台可以配置 1 到 7 天的奖励资源组,每天必须选择一个 `resource_group_id`
- 后台可以打开或关闭七日签到;关闭后 App 只读状态,不允许新签到。
- App 提供七日签到面板查询接口和签到接口。
- 每个用户按 UTC 自然日每日最多签到一次。
- 连续签到按 7 天为一轮:第 7 天签到后,下一次签到开启新一轮第 1 天。
- 中断连续签到后下一次签到从第 1 天重新开始。
- 签到奖励发放必须通过 `wallet-service.GrantResourceGroup`,资源组中可以包含金币、钻石和装扮权益。
- 签到事实、奖励发放状态和幂等命令必须可审计、可恢复、可重试。
## 非目标
- 首版不做补签、签到卡、广告补领或客户端本地时区签到。
- 首版不支持多套并行签到活动;每个 `app_code` 同一时间只有一套七日签到配置。
- 首版不做人群定向、地区定向、新老用户差异奖励。
- 首版不让客户端上报连续天数、奖励内容、资源组明细或服务端时间。
- 首版不把七日签到建成每日任务。每日任务依赖事实事件进度,签到是用户主动命令和每日幂等发奖。
- 首版不把签到奖励拆成金币字段;所有奖励都只表达为资源组。
## 服务边界
| 模块 | 责任 | 不负责 |
| --- | --- | --- |
| `gateway-service` | App HTTP 入口、鉴权、业务 envelope、生成并透传 `request_id`、调用 activity gRPC、按需向 wallet 查询资源组展示信息 | 不判断连续天数,不写签到表,不发资源组 |
| `activity-service` | 七日签到配置、配置版本、用户连续签到状态、签到 claim、发奖幂等、失败重试状态 | 不拥有资源组内容,不直接修改钱包余额或用户权益 |
| `wallet-service` | 资源组 catalog、资源组有效性校验、`GrantResourceGroup` 原子展开资源和钱包资产、钱包/资源 outbox | 不判断用户今天能否签到,不保存签到连续天数 |
| `server/admin` | 后台菜单、权限、后台 HTTP API、操作审计、调用 activity 管理 RPC并复用资源组选择能力 | 不直接写 activity/wallet 业务库,不复制 app 服务内部包 |
| Admin 前端 | `活动管理 > 七日签到` 页面、开关、7 行资源组选择、保存发布、领取记录列表 | 不自行拼资源组明细,不绕过后台权限 |
| `notice-service` | 后续如需私有通知,从 activity outbox 消费签到奖励事实 | 不参与签到主链路 |
核心原则:
- `activity-service` 是七日签到状态 owner。
- 资源组内容永远以 `wallet-service` 为准;签到配置只保存 `resource_group_id` 和发放时必要快照。
- 发奖是房间外副作用,必须通过幂等命令调用 wallet-service。
- 所有日期、周期、刷新时间使用 UTC不读取服务器本地时区、用户手机时区或 IP 推断时区。
## 总体架构
```mermaid
flowchart LR
AdminFE["admin 前端\n活动管理 > 七日签到"] --> AdminServer["server/admin\n七日签到后台 API"]
AdminServer --> AdminCheckIn["activity-service\nAdminSevenDayCheckInService"]
AdminServer --> WalletRead["wallet-service\n资源组查询"]
App["App 七日签到页"] --> Gateway["gateway-service\n/api/v1"]
Gateway --> CheckInSvc["activity-service\nSevenDayCheckInService"]
Gateway --> WalletGroup["wallet-service\n资源组展示查询"]
CheckInSvc --> ActivityDB[("hyapp_activity\ncheckin tables")]
CheckInSvc --> WalletGrant["wallet-service\nGrantResourceGroup"]
WalletGrant --> WalletDB[("hyapp_wallet\nresource grants / wallet ledger")]
CheckInSvc --> ActivityOutbox[("activity_outbox\noptional reward event")]
```
## 产品规则
### 签到周期
签到日使用 UTC 日期字符串 `yyyy-mm-dd`
| 场景 | 结果 |
| --- | --- |
| 用户从未签到 | 今天签到为第 1 天 |
| 上次签到日是今天 | 返回已签到,不重复发奖 |
| 上次签到日是昨天,且上次是第 1 到第 6 天 | 今天签到为下一天 |
| 上次签到日是昨天,且上次是第 7 天 | 今天开启新一轮第 1 天 |
| 上次签到日早于昨天 | 连续中断,今天重新从第 1 天开始 |
第 7 天之后不保持在第 7 天;下一次有效签到必须进入新一轮第 1 天,避免一个用户无限领取第 7 天奖励。
### 配置版本
后台每次保存 7 天奖励或开关都生成新的 `config_version`
- App 查询总是展示当前配置版本。
- 用户开始一轮签到时绑定当时的 `config_version`
- 同一轮后续签到继续使用该轮绑定版本,避免运营改配置后用户第 3 天突然看到另一套奖励。
- 第 7 天完成后下一轮读取最新 enabled 配置版本。
- 如果旧版本对应资源组后来被 wallet 下架,发奖时以 wallet-service 校验结果为准,失败 claim 进入可重试/待处理状态。
配置版本不是历史兼容负担,而是发奖审计和用户体验一致性的最小事实。
### 开关
`enabled=false` 时:
- App 查询返回当前配置和 `can_sign=false, reason=disabled`
- 新的签到命令被拒绝。
- 已经创建的 pending/failed claim 仍允许后台或服务端补偿重试,避免开关关闭导致已接受签到无法收尾。
- 后台可以继续编辑 7 天奖励并保存新版本。
### 奖励
- 每天奖励只允许一个资源组 ID。
- 资源组必须由后台从资源组列表选择;不允许手写资源 ID、金币数量、钻石数量或有效期。
- 资源组内容展示由 wallet-service 查询并返回activity-service 只保存 ID 和发放时返回的 `grant_id`
- 发放时调用 `wallet-service.GrantResourceGroup``grant_source=seven_day_checkin``reason=seven_day_checkin`
- wallet-service 在发放事务内重新校验资源组 active、资源 active、钱包资产合法gateway 和 admin 的前端筛选不能替代 owner 校验。
## 后台管理
后台菜单:
```text
活动管理
- 每日任务
- 注册奖励
- 成就配置
- 七日签到
```
建议新增权限:
| Permission | Kind | Usage |
| --- | --- | --- |
| `seven-day-checkin:view` | `menu` | 查看七日签到配置和领取记录 |
| `seven-day-checkin:update` | `button` | 更新开关和 7 天奖励资源组 |
建议菜单:
| 字段 | 值 |
| --- | --- |
| title | `七日签到` |
| code | `seven-day-checkin` |
| path | `/activities/seven-day-checkin` |
| icon | `event_available` |
| permission_code | `seven-day-checkin:view` |
| sort | `72` |
### 页面结构
页面保留两个 Tab
| Tab | 内容 |
| --- | --- |
| `配置` | 顶部开关、7 行奖励配置、当前版本、更新时间、保存按钮 |
| `领取记录` | 用户、签到日、第几天、轮次、配置版本、资源组、状态、wallet grant id、失败原因 |
配置 Tab 字段:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| 开关 | 是 | 保存到 `enabled` |
| 第 1 天资源组 | 是 | `resource_group_id > 0` |
| 第 2 天资源组 | 是 | `resource_group_id > 0` |
| 第 3 天资源组 | 是 | `resource_group_id > 0` |
| 第 4 天资源组 | 是 | `resource_group_id > 0` |
| 第 5 天资源组 | 是 | `resource_group_id > 0` |
| 第 6 天资源组 | 是 | `resource_group_id > 0` |
| 第 7 天资源组 | 是 | `resource_group_id > 0` |
领取记录筛选:
| 字段 | 说明 |
| --- | --- |
| `keyword` | 用户 ID、展示 ID 或昵称,由 `server/admin` 解析成 user_id |
| `status` | `pending` / `granted` / `failed` |
| `checkin_day` | UTC 日期 |
| `day_index` | 1 到 7 |
### 后台 API
`server/admin` 对前端暴露:
```http
GET /api/v1/admin/activity/seven-day-checkin/config
PUT /api/v1/admin/activity/seven-day-checkin/config
GET /api/v1/admin/activity/seven-day-checkin/claims
```
更新配置请求:
```json
{
"enabled": true,
"rewards": [
{"day_index": 1, "resource_group_id": 101},
{"day_index": 2, "resource_group_id": 102},
{"day_index": 3, "resource_group_id": 103},
{"day_index": 4, "resource_group_id": 104},
{"day_index": 5, "resource_group_id": 105},
{"day_index": 6, "resource_group_id": 106},
{"day_index": 7, "resource_group_id": 107}
]
}
```
规则:
- `rewards` 必须刚好包含 1 到 7 天,不能缺失、重复或越界。
- `resource_group_id` 必须大于 0。
- Admin 前端资源组选择器复用现有资源组列表能力,只展示 active 资源组。
- `server/admin` 必须写操作审计,记录 `before_json``after_json`、操作人、request_id。
- `activity-service` 保存配置时只保存资源组 ID资源组是否 active 由 wallet-service 在选择展示和最终发放时确认。
## App 接口
App 只通过 gateway HTTP 访问:
```http
GET /api/v1/activities/seven-day-checkin
POST /api/v1/activities/seven-day-checkin/sign
```
### 查询签到面板
```http
GET /api/v1/activities/seven-day-checkin
Authorization: Bearer <access_token>
```
响应 data
```json
{
"enabled": true,
"can_sign": true,
"reason": "eligible",
"cycle_no": 3,
"current_day_index": 2,
"next_day_index": 3,
"checkin_day": "2026-05-16",
"last_checkin_day": "2026-05-15",
"config_version": 8,
"server_time_ms": 1778889600000,
"next_refresh_at_ms": 1778976000000,
"rewards": [
{
"day_index": 1,
"resource_group_id": 101,
"status": "granted",
"resource_group": {}
},
{
"day_index": 2,
"resource_group_id": 102,
"status": "granted",
"resource_group": {}
},
{
"day_index": 3,
"resource_group_id": 103,
"status": "available",
"resource_group": {}
}
]
}
```
`resource_group` 由 gateway 按 `resource_group_id` 调 wallet-service 查询后嵌入,结构沿用现有资源组 HTTP DTO。activity-service gRPC 只需要返回资源组 ID 和用户签到状态。
`current_day_index` 表示本轮最近已完成的天数,从未签到时返回 `0``next_day_index` 表示今天如果签到会领取第几天奖励。
`reason` 建议枚举:
| reason | 含义 |
| --- | --- |
| `eligible` | 今天可签到 |
| `disabled` | 活动关闭 |
| `not_configured` | 未配置完整 7 天奖励 |
| `already_signed` | 今天已签到 |
| `pending_reward` | 今天签到已接受,奖励仍在处理中 |
| `invalid_config` | 配置不可发放 |
### 执行签到
```http
POST /api/v1/activities/seven-day-checkin/sign
Authorization: Bearer <access_token>
```
请求:
```json
{
"command_id": "cmd_checkin_550e8400"
}
```
规则:
- `command_id` 由客户端按本次点击动作生成,同一次重试必须复用。
- gateway 不补 `command_id`,只裁剪空白并透传当前用户 ID。
- activity-service 决定今天是第几天、是否已签到、应发哪个资源组。
- 同一用户同一 UTC 日期最多一个签到 claim。
- 同一 `command_id` 重试且 payload 一致,返回同一 claim 结果。
响应 data
```json
{
"claim_id": "checkin_claim_123",
"cycle_no": 3,
"checkin_day": "2026-05-16",
"day_index": 3,
"resource_group_id": 103,
"status": "granted",
"wallet_grant_id": "resource_grant_456",
"granted_at_ms": 1778918500000,
"server_time_ms": 1778918500000
}
```
## 内部 gRPC 契约
建议在 `api/proto/activity/v1/activity.proto` 增加:
```proto
message SevenDayCheckInReward {
int32 day_index = 1;
int64 resource_group_id = 2;
}
message SevenDayCheckInConfig {
string app_code = 1;
bool enabled = 2;
int64 config_version = 3;
repeated SevenDayCheckInReward rewards = 4;
int64 updated_by_admin_id = 5;
int64 created_at_ms = 6;
int64 updated_at_ms = 7;
}
message SevenDayCheckInRewardStatus {
int32 day_index = 1;
int64 resource_group_id = 2;
string status = 3;
string claim_id = 4;
string wallet_grant_id = 5;
int64 granted_at_ms = 6;
}
message GetSevenDayCheckInStatusRequest {
RequestMeta meta = 1;
int64 user_id = 2;
}
message GetSevenDayCheckInStatusResponse {
bool enabled = 1;
bool can_sign = 2;
string reason = 3;
int64 cycle_no = 4;
int32 current_day_index = 5;
int32 next_day_index = 6;
string checkin_day = 7;
string last_checkin_day = 8;
int64 config_version = 9;
repeated SevenDayCheckInRewardStatus rewards = 10;
int64 server_time_ms = 11;
int64 next_refresh_at_ms = 12;
}
message SignSevenDayCheckInRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string command_id = 3;
}
message SignSevenDayCheckInResponse {
string claim_id = 1;
int64 cycle_no = 2;
string checkin_day = 3;
int32 day_index = 4;
int64 resource_group_id = 5;
string status = 6;
string wallet_grant_id = 7;
int64 granted_at_ms = 8;
int64 server_time_ms = 9;
}
service SevenDayCheckInService {
rpc GetSevenDayCheckInStatus(GetSevenDayCheckInStatusRequest) returns (GetSevenDayCheckInStatusResponse);
rpc SignSevenDayCheckIn(SignSevenDayCheckInRequest) returns (SignSevenDayCheckInResponse);
}
```
后台管理 RPC
```proto
message GetSevenDayCheckInConfigRequest {
RequestMeta meta = 1;
}
message GetSevenDayCheckInConfigResponse {
SevenDayCheckInConfig config = 1;
}
message UpdateSevenDayCheckInConfigRequest {
RequestMeta meta = 1;
bool enabled = 2;
repeated SevenDayCheckInReward rewards = 3;
int64 operator_admin_id = 4;
}
message UpdateSevenDayCheckInConfigResponse {
SevenDayCheckInConfig config = 1;
}
message SevenDayCheckInClaim {
string claim_id = 1;
string command_id = 2;
int64 user_id = 3;
int64 cycle_no = 4;
string checkin_day = 5;
int32 day_index = 6;
int64 config_version = 7;
int64 resource_group_id = 8;
string wallet_command_id = 9;
string wallet_grant_id = 10;
string status = 11;
string failure_reason = 12;
int64 signed_at_ms = 13;
int64 granted_at_ms = 14;
int64 created_at_ms = 15;
int64 updated_at_ms = 16;
}
message ListSevenDayCheckInClaimsRequest {
RequestMeta meta = 1;
string status = 2;
int64 user_id = 3;
string checkin_day = 4;
int32 day_index = 5;
int32 page = 6;
int32 page_size = 7;
}
message ListSevenDayCheckInClaimsResponse {
repeated SevenDayCheckInClaim claims = 1;
int64 total = 2;
}
service AdminSevenDayCheckInService {
rpc GetSevenDayCheckInConfig(GetSevenDayCheckInConfigRequest) returns (GetSevenDayCheckInConfigResponse);
rpc UpdateSevenDayCheckInConfig(UpdateSevenDayCheckInConfigRequest) returns (UpdateSevenDayCheckInConfigResponse);
rpc ListSevenDayCheckInClaims(ListSevenDayCheckInClaimsRequest) returns (ListSevenDayCheckInClaimsResponse);
}
```
修改 proto 后必须运行:
```bash
make proto
go test ./...
```
## 数据模型
所有表位于 `hyapp_activity`,字段使用 UTC epoch milliseconds 和 UTC 日期字符串。
### `seven_day_checkin_configs`
当前配置入口。
| 字段 | 说明 |
| --- | --- |
| `app_code` | App 租户,主键 |
| `enabled` | 活动开关 |
| `config_version` | 当前配置版本 |
| `updated_by_admin_id` | 最近操作人 |
| `created_at_ms` | 创建时间 |
| `updated_at_ms` | 更新时间 |
### `seven_day_checkin_rewards`
当前版本 7 天奖励读模型。
| 字段 | 说明 |
| --- | --- |
| `app_code` | App 租户 |
| `day_index` | 1 到 7 |
| `config_version` | 当前版本 |
| `resource_group_id` | wallet-service 资源组 ID |
| `created_at_ms` | 创建时间 |
| `updated_at_ms` | 更新时间 |
唯一键:
```text
PRIMARY KEY(app_code, day_index)
```
### `seven_day_checkin_config_versions`
配置版本快照。
| 字段 | 说明 |
| --- | --- |
| `app_code` | App 租户 |
| `config_version` | 版本号 |
| `enabled` | 当时开关 |
| `snapshot_json` | 7 天奖励资源组快照 |
| `created_by_admin_id` | 操作人 |
| `created_at_ms` | 创建时间 |
唯一键:
```text
PRIMARY KEY(app_code, config_version)
```
### `user_seven_day_checkin_accounts`
用户连续签到账户。
| 字段 | 说明 |
| --- | --- |
| `app_code` | App 租户 |
| `user_id` | 用户 ID |
| `cycle_no` | 当前轮次,从 1 开始 |
| `cycle_start_day` | 本轮第 1 天 UTC 日期 |
| `last_checkin_day` | 最近成功签到 UTC 日期 |
| `last_day_index` | 最近成功签到是第几天 |
| `config_version` | 本轮绑定配置版本 |
| `updated_at_ms` | 更新时间 |
唯一键:
```text
PRIMARY KEY(app_code, user_id)
```
### `seven_day_checkin_claims`
每次签到和资源组发放事实。
| 字段 | 说明 |
| --- | --- |
| `claim_id` | 主键 |
| `command_id` | 客户端签到动作幂等键 |
| `app_code` | App 租户 |
| `user_id` | 用户 ID |
| `cycle_no` | 轮次 |
| `checkin_day` | UTC 签到日 |
| `day_index` | 1 到 7 |
| `config_version` | 发奖使用的配置版本 |
| `resource_group_id` | 发奖资源组快照 |
| `wallet_command_id` | 调用 wallet-service 的幂等键 |
| `wallet_grant_id` | wallet-service 返回的资源组 grant ID |
| `status` | `pending` / `granted` / `failed` |
| `failure_reason` | 失败原因 |
| `signed_at_ms` | 签到命令接受时间 |
| `granted_at_ms` | 奖励发放完成时间 |
| `created_at_ms` | 创建时间 |
| `updated_at_ms` | 更新时间 |
唯一键:
```text
PRIMARY KEY(app_code, claim_id)
UNIQUE(app_code, command_id)
UNIQUE(app_code, user_id, checkin_day)
UNIQUE(app_code, user_id, cycle_no, day_index)
```
`UNIQUE(app_code, user_id, checkin_day)` 保证自然日最多一次;`UNIQUE(app_code, user_id, cycle_no, day_index)` 保证同一轮同一天位不会被并发重复发放。
## 签到流程
```mermaid
sequenceDiagram
participant App
participant Gateway
participant Activity as activity-service
participant DB as activity MySQL
participant Wallet as wallet-service
App->>Gateway: POST /activities/seven-day-checkin/sign(command_id)
Gateway->>Activity: SignSevenDayCheckIn(user_id, command_id)
Activity->>DB: 锁 config + user account计算 checkin_day/day_index
Activity->>DB: INSERT claim pending(wallet_command_id)
Activity->>Wallet: GrantResourceGroup(wallet_command_id, resource_group_id)
Wallet-->>Activity: grant_id
Activity->>DB: 锁 claim + account更新 account 和 claim=granted
Activity-->>Gateway: granted
Gateway-->>App: OK
```
关键事务边界:
1. Prepare 事务只创建或复用 `pending` claim不更新连续签到账户。
2. wallet-service 发放资源组使用稳定 `wallet_command_id`,重复调用必须幂等。
3. Finalize 事务在 wallet 成功后更新 `user_seven_day_checkin_accounts` 和 claim 状态。
4. 如果 wallet 成功但 finalize 失败,用户重试同一 `command_id` 时 wallet 返回幂等结果activity 再完成 finalize。
5. 如果 wallet 失败claim 标记 `failed`,账户不前进;同一 `command_id` 重试应重新尝试发放该 claim后台补偿也只能重试同一个 claim。
这样做的原因是签到连续天数必须和奖励成功保持一致。不能出现 activity 已经把用户推进到第 3 天,但第 3 天资源组没有发出的状态。
## 查询流程
```mermaid
sequenceDiagram
participant App
participant Gateway
participant Activity as activity-service
participant Wallet as wallet-service
App->>Gateway: GET /activities/seven-day-checkin
Gateway->>Activity: GetSevenDayCheckInStatus(user_id)
Activity-->>Gateway: config ids + user status
Gateway->>Wallet: GetResourceGroup for visible reward ids
Wallet-->>Gateway: resource group DTOs
Gateway-->>App: envelope(data)
```
查询只读 activity 配置、用户账户和最近 claims不实时扫描钱包流水。资源组展示信息由 gateway 使用 wallet 查询结果补齐。
## 幂等和并发
- `command_id` 是客户端点击动作幂等键。
- `wallet_command_id` 建议格式:`seven_day_checkin:{claim_id}`
- 同一 `command_id` 重试返回同一 claim。
- 同一用户同一 `checkin_day` 并发请求只能创建一个 claim。
- 同一用户同一 `cycle_no + day_index` 只能有一个 granted/pending claim。
- claim payload 冲突时返回 `CONFLICT`,不能静默重写历史 claim。
- 查询接口不产生签到事实。
## 时间规则
- `checkin_day` 按 UTC 日期计算。
- `next_refresh_at_ms` 是下一天 UTC 00:00:00.000 的 epoch milliseconds。
- 所有业务时间字段使用 `*_ms`
- 领取记录筛选的日期字段是 UTC 日期,不接受本地时区字符串。
- MySQL DSN、Docker `TZ` 和数据库默认时区继续保持 UTC。
## 错误和状态
| 状态或错误 | 场景 | 处理 |
| --- | --- | --- |
| `disabled` | 活动关闭 | 查询可见,签到拒绝 |
| `not_configured` | 未配置完整 7 天奖励 | 查询提示不可签,签到拒绝 |
| `already_signed` | 当天已有 granted/pending claim | 返回已有状态 |
| `pending` | claim 已创建,奖励未完成 | 可重试或后台补偿 |
| `failed` | wallet 发放失败或配置不可发放 | 记录原因,账户不前进 |
| `CONFLICT` | 同一 command_id payload 不一致 | 返回冲突 |
| `INVALID_ARGUMENT` | command_id 空、day_index 越界、资源组 ID 无效 | 返回参数错误 |
## Outbox 和通知
签到发奖成功后activity-service 可以写 `activity_outbox`
| Event | Payload |
| --- | --- |
| `SevenDayCheckInRewardGranted` | `claim_id``user_id``cycle_no``checkin_day``day_index``resource_group_id``wallet_grant_id` |
| `SevenDayCheckInRewardFailed` | `claim_id``user_id``checkin_day``failure_reason` |
首版不要求实时私信通知。后续如果产品需要奖励到账通知,由 `notice-service` 消费 activity outbox 并投递,不让 activity-service 直接耦合私有通知实现。
## 实现顺序
1. 文档和契约:确认本架构后修改 `api/proto/activity/v1/activity.proto`,运行 `make proto`
2. activity-service增加 domain/service/repository/gRPC server 和 MySQL initdb 表。
3. wallet 集成:复用 `GrantResourceGroup`,补齐七日签到发放 reason/source 和测试。
4. gateway-service增加签到查询和签到 HTTP接入 activity client并按资源组 ID 补齐展示 DTO。
5. server/admin增加后台模块、路由、权限、菜单 migration、审计和 activity 管理 client。
6. Admin 前端:增加 `/activities/seven-day-checkin` 页面,复用资源组选择器。
7. 补偿:增加 pending/failed claim 重试批处理;由 cron-service 触发 activity-service owner 批处理。
8. 验证:补 UTC 边界、并发幂等、wallet 失败、配置版本切换和后台权限测试。
## 测试要求
至少覆盖:
| 场景 | 验证 |
| --- | --- |
| UTC 当天首次签到 | 创建 claim发资源组账户推进到第 1 天 |
| 同一天重复签到 | 不重复发奖,返回已有 claim |
| 连续 7 天签到 | day_index 从 1 到 7下一天回到第 1 天并 cycle_no + 1 |
| 中断一天后签到 | day_index 重置为 1新 cycle_no |
| UTC 边界 | `23:59:59.999``00:00:00.000` 分属不同 checkin_day |
| 开关关闭 | 查询可见但不能签到 |
| 配置缺失 | 不能打开或不能签到 |
| 修改配置版本 | 已开始轮次继续旧版本,新轮次使用新版本 |
| wallet GrantResourceGroup 失败 | claim failed账户不前进可重试 |
| finalize 失败后重试 | wallet 幂等返回后 activity 完成 finalize |
| admin 权限 | 无 `seven-day-checkin:update` 不能保存配置 |
涉及 proto 和代码实现后运行:
```bash
make proto
make test
go test ./...
```
涉及后台菜单 migration 或配置时额外运行:
```bash
docker compose config
```
## 待确认产品项
1. 是否需要补签。当前架构明确首版不支持。
2. 第 7 天之后是否循环回第 1 天。当前架构按循环处理。
3. 中断后是否重置到第 1 天。当前架构按重置处理。
4. 是否需要领取记录导出。当前只设计分页查询。
5. 是否需要发奖成功后的 App 活动消息。当前只预留 outbox不默认投递。

View File

@ -9,7 +9,12 @@
| Capability | Owner | Notes |
| --- | --- | --- |
| 幸运礼物规则配置 | `activity-service` | 概率表、奖档、启停、版本 |
| 三层奖池 | `activity-service` | 平台、房间、礼物水位 |
| 三层奖池 | `activity-service` | 平台、房间、礼物资金水位 |
| 体验池 | `activity-service` | 新手、中级、高级候选奖档和权重 |
| 全站 RTP 控制窗口 | `activity-service` | 控全站基础返奖总成本 |
| 礼物 RTP 子窗口 | `activity-service` | 控单个幸运礼物基础返奖成本 |
| 房间气氛池 | `activity-service` | 控房间爆点、共享奖励和表现预算 |
| 活动预算池 | `activity-service` | 控限时活动补贴,不污染基础 RTP |
| 抽奖决策 | `activity-service` | 先过滤奖档,再执行随机 |
| 风控阈值 | `activity-service` | 用户、设备、房间、主播、活动周期 |
| 金币扣费 | `wallet-service` | `room-service` 送礼前同步扣费 |
@ -98,8 +103,12 @@ message ExecuteLuckyGiftDrawRequest {
message RewardTierResult {
string tier_id = 1;
int64 reward_coins = 2;
string tier_type = 3;
int64 base_reward_coins = 2;
int64 room_atmosphere_reward_coins = 3;
int64 activity_subsidy_coins = 4;
int64 effective_reward_coins = 5;
string tier_type = 6;
string budget_sources_json = 7;
}
message ExecuteLuckyGiftDrawResponse {
@ -127,8 +136,12 @@ message CreditLuckyRewardRequest {
string draw_id = 3;
string room_id = 4;
int64 user_id = 5;
int64 reward_coins = 6;
string reason = 7;
int64 effective_reward_coins = 6;
int64 base_reward_coins = 7;
int64 room_atmosphere_reward_coins = 8;
int64 activity_subsidy_coins = 9;
string reason = 10;
string budget_sources_json = 11;
}
message CreditLuckyRewardResponse {
@ -153,6 +166,13 @@ message CreditLuckyRewardResponse {
| `gift_price` | bigint | single gift price in coins |
| `pool_rate_ppm` | int | pool inflow rate, ppm |
| `target_payout_rate_ppm` | int | expected long-term payout |
| `target_rtp_ppm` | int | controlled RTP target, e.g. 950000 for 95% |
| `control_window_draws` | int | paid draws per RTP control window |
| `gift_rtp_window_draws` | int | paid draws per gift-level RTP sub-window |
| `novice_draw_limit` | bigint | inclusive max paid draw count for novice pool |
| `intermediate_draw_limit` | bigint | inclusive max paid draw count for intermediate pool |
| `room_atmosphere_rule_json` | json | room burst, shared reward, and presentation policy |
| `activity_budget_id` | varchar | optional active subsidy budget |
| `max_single_payout` | bigint | max reward per draw |
| `max_user_daily_payout` | bigint | user daily cap |
| `max_room_hourly_payout` | bigint | room hourly cap |
@ -170,14 +190,39 @@ message CreditLuckyRewardResponse {
| `id` | bigint | primary key |
| `gift_id` | varchar | gift id |
| `rule_version` | varchar | rule version |
| `experience_pool` | varchar | novice, intermediate, advanced |
| `tier_id` | varchar | none, small_1x, medium_5x |
| `reward_type` | varchar | none, fixed, multiplier |
| `reward_value` | bigint | fixed coins or multiplier base |
| `probability_ppm` | int | base probability |
| `base_weight` | int | relative selection weight inside the experience pool |
| `budget_source` | varchar | base_rtp, room_atmosphere, activity_subsidy, presentation_only |
| `presentation_type` | varchar | none, room_message, floating_screen, shared_drop |
| `shared_reward_policy_json` | json | optional shared reward split for room atmosphere |
| `min_pool_balance` | bigint | activation waterline |
| `enabled` | bool | tier switch |
All tier probabilities in one rule version must sum to `1000000` ppm.
All tier weights in one `(gift_id, rule_version, experience_pool)` group should be positive and internally normalizable. They are not the source of truth for total RTP; they only shape the visible distribution inside the RTP control window.
### `lucky_rtp_windows`
保存 RTP 控制窗口。窗口行必须在抽奖事务内和三层奖池一起锁定,避免并发抽奖同时消费同一段目标支出。
| Column | Type | Notes |
| --- | --- | --- |
| `scope_type` | varchar | global, gift |
| `scope_id` | varchar | global or gift_id |
| `window_index` | bigint | monotonic index by paid draw count |
| `target_rtp_ppm` | int | immutable target for this window |
| `control_window_draws` | bigint | expected paid draws in this window |
| `paid_draws` | bigint | paid draws already assigned to this window |
| `wager_coins` | bigint | accumulated cost in this window |
| `target_payout_coins` | bigint | integer payout target for this window |
| `actual_base_payout_coins` | bigint | base RTP rewards selected in this window |
| `carry_ppm` | bigint | fractional target remainder carried into next window |
| `status` | varchar | open, closed, underpaid |
| `updated_at_ms` | bigint | update time |
The primary key is `(scope_type, scope_id, window_index)`.
### `lucky_pools`
@ -196,6 +241,41 @@ All tier probabilities in one rule version must sum to `1000000` ppm.
The primary key is `(scope_type, scope_id)`.
### `lucky_room_atmosphere_pools`
保存房间气氛预算。它控制房间爆点和共享奖励,不拥有 Room Cell 核心状态。
| Column | Type | Notes |
| --- | --- | --- |
| `room_id` | varchar | room id |
| `balance` | bigint | available room atmosphere coins |
| `reserve_floor` | bigint | minimum balance before shared rewards stop |
| `lucky_heat` | bigint | non-coin room atmosphere score |
| `burst_level` | int | current presentation intensity |
| `hourly_shared_payout_limit` | bigint | hourly shared reward cap |
| `daily_shared_payout_limit` | bigint | daily shared reward cap |
| `version` | bigint | optimistic version |
| `updated_at_ms` | bigint | update time |
The primary key is `room_id`.
### `lucky_activity_budgets`
保存活动补贴预算。活动补贴提高用户看到的 `effective_rtp`,但不计入基础 RTP。
| Column | Type | Notes |
| --- | --- | --- |
| `budget_id` | varchar | primary key |
| `activity_id` | varchar | activity id |
| `gift_id` | varchar | optional gift scope, empty means all lucky gifts |
| `budget_coins` | bigint | total subsidy budget |
| `spent_coins` | bigint | granted subsidy coins |
| `daily_spend_limit` | bigint | daily subsidy cap |
| `status` | varchar | draft, active, paused, exhausted, retired |
| `effective_from_ms` | bigint | UTC start time |
| `effective_to_ms` | bigint | UTC end time |
| `updated_at_ms` | bigint | update time |
### `lucky_draw_records`
保存每次抽奖事实。
@ -213,11 +293,22 @@ The primary key is `(scope_type, scope_id)`.
| `coin_spent` | bigint | paid coins |
| `heat_value` | bigint | room heat contributed by this gift |
| `rule_version` | varchar | immutable rule version |
| `experience_pool` | varchar | novice, intermediate, advanced |
| `rtp_scope_type` | varchar | scope that controlled this draw |
| `rtp_scope_id` | varchar | concrete scope id |
| `rtp_window_index` | bigint | RTP control window |
| `gift_rtp_window_index` | bigint | gift-level RTP sub-window |
| `candidate_tiers_json` | json | tiers after filtering |
| `selected_tier_id` | varchar | final selected tier |
| `reward_coins` | bigint | reward amount |
| `base_reward_coins` | bigint | reward counted in global and gift RTP |
| `room_atmosphere_reward_coins` | bigint | shared or room-burst reward outside base RTP |
| `activity_subsidy_coins` | bigint | activity subsidy outside base RTP |
| `effective_reward_coins` | bigint | total user-visible coin reward |
| `budget_sources_json` | json | exact budget sources consumed by this draw |
| `random_digest` | varchar | audit digest |
| `pool_snapshot_json` | json | balances before draw |
| `rtp_snapshot_json` | json | global and gift RTP window snapshots |
| `budget_snapshot_json` | json | room atmosphere and activity budget snapshots |
| `risk_snapshot_json` | json | counters before draw |
| `reward_status` | varchar | none, pending, granted, failed |
| `credit_receipt_id` | varchar | wallet credit receipt |
@ -258,23 +349,45 @@ The primary key is `(scope_type, scope_id)`.
## Draw Algorithm
All money-like values use integer coins. Percentages use ppm to avoid floating point drift.
All money-like values use integer coins. Percentages use ppm to avoid floating point drift. V2 separates base RTP payout from entertainment subsidies.
### 1. Experience Pool
```text
gift_value = heat_value
pool_in = floor(gift_value * pool_rate_ppm / 1000000)
paid_draw_number = user_lucky_paid_draw_count + 1
if paid_draw_number <= novice_draw_limit:
experience_pool = novice
else if paid_draw_number <= intermediate_draw_limit:
experience_pool = intermediate
else:
experience_pool = advanced
```
Stage feedback is calculated from the same paid draw number. It is a presentation state unless the selected candidate explicitly carries `base_rtp` or `activity_subsidy` budget.
```text
novice_feedback_marks = [1, novice_draw_limit * 25%, novice_draw_limit * 50%, novice_draw_limit * 75%, novice_draw_limit]
stage_feedback_enabled = paid_draw_number in novice_feedback_marks
```
Feedback cannot credit coins after selection. If a milestone should pay coins, that payout must be present in the pre-random candidate set and must pass all RTP, pool, and risk filters.
### 2. Pool Inflow And Capacity
```text
pool_in = floor(coin_spent * pool_rate_ppm / 1000000)
platform_in = floor(pool_in * platform_weight_ppm / 1000000)
room_in = floor(pool_in * room_weight_ppm / 1000000)
gift_in = pool_in - platform_in - room_in
```
Available payout is the minimum safe capacity across all layers and risk caps:
```text
platform_capacity = platform_balance + platform_in - platform_reserve_floor
room_capacity = room_balance + room_in - room_reserve_floor
gift_capacity = gift_balance + gift_in - gift_reserve_floor
```
Risk remains a hard cap:
```text
risk_capacity = min(
max_single_payout,
user_daily_remaining,
@ -283,21 +396,124 @@ risk_capacity = min(
anchor_daily_remaining,
activity_period_remaining
)
available_payout = min(platform_capacity, room_capacity, gift_capacity, risk_capacity)
```
Tier filtering rules:
Time-window caps must be configured above the expected payout for that window. Otherwise the transaction can satisfy the user risk cap or the RTP target, but not both.
```text
expected_window_payout = paid_draw_capacity
* gift_price
* target_rtp_ppm
/ 1000000
hourly_cap >= expected_hourly_payout * volatility_factor
daily_cap >= expected_daily_payout * volatility_factor
```
Use a larger `volatility_factor` for higher variance pools. The default demo model uses `2.0` for hourly and `1.5` for daily caps.
### 3. Global And Gift RTP Windows
Every draw enters two RTP windows:
```text
global_window_target = floor((global_window_wager * global_target_rtp_ppm + global_carry_ppm) / 1000000)
gift_window_target = floor((gift_window_wager * gift_target_rtp_ppm + gift_carry_ppm) / 1000000)
global_remaining_payout = global_window_target - global_window_base_payout
gift_remaining_payout = gift_window_target - gift_window_base_payout
global_remaining_draws = global_window_draws - global_window_paid_draws
gift_remaining_draws = gift_window_draws - gift_window_paid_draws
```
High multiplier tiers are disabled until both RTP windows have positive payout headroom large enough to pay the tier:
```text
rtp_headroom = remaining_payout
- remaining_draws * gift_price * target_rtp_ppm / 1000000
high_multiplier_enabled = global_rtp_headroom >= tier_reward
&& gift_rtp_headroom >= tier_reward
```
The full production filter must additionally require platform, room, and gift pool headroom above the same tier reward plus reserve floors.
The base payout capacity is the strict minimum:
```text
base_capacity = min(
platform_capacity,
room_capacity,
gift_capacity,
risk_capacity,
global_remaining_payout,
gift_remaining_payout
)
```
### 4. Candidate Construction
Build candidates from three independent sources:
| Source | Candidate Fields | Accounting |
| --- | --- | --- |
| `base_rtp` | tier id, base reward, experience pool weight | counts in global and gift RTP windows |
| `room_atmosphere` | room burst, shared reward, presentation | consumes room atmosphere budget only |
| `activity_subsidy` | subsidy reward, activity id, display tag | consumes activity budget only |
Base candidates must preserve both RTP windows:
```text
base_reward_coins <= base_capacity
global_remaining_payout - base_reward_coins
<= global_min_future_max_reward * (global_remaining_draws - 1)
gift_remaining_payout - base_reward_coins
<= gift_min_future_max_reward * (gift_remaining_draws - 1)
```
Room atmosphere and activity candidates cannot be used to satisfy these formulas. They only increase `effective_reward_coins`.
### 5. Selection
```text
candidate_set = filtered_base_candidates
+ filtered_room_atmosphere_candidates
+ filtered_activity_subsidy_candidates
selected = weighted_random(candidate_set, secure_random)
```
Selection rules:
| Rule | Behavior |
| --- | --- |
| `reward_coins > available_payout` | tier disabled for this draw |
| `tier.min_pool_balance > current_pool_balance` | tier disabled for this draw |
| `tier.enabled=false` | tier disabled for this draw |
| disabled tier probability | moved to `none` tier |
| base reward exceeds base capacity | base tier disabled |
| base reward breaks either RTP window feasibility | base tier disabled |
| high multiplier lacks global, gift, or pool headroom | high tier disabled and its weight moves to `none` |
| room atmosphere reward exceeds room atmosphere budget | room candidate disabled |
| activity subsidy exceeds active budget or daily limit | activity candidate disabled |
| risk cap would be exceeded | coin-paying candidate disabled |
| disabled tier weight | moved to `none` only inside the same source |
The final random selection runs only on the filtered table. It must never select a tier that cannot be paid.
### 6. Accounting
```text
base_reward_coins = selected.base_reward_coins
room_atmosphere_reward_coins = selected.room_atmosphere_reward_coins
activity_subsidy_coins = selected.activity_subsidy_coins
effective_reward_coins = base_reward_coins
+ room_atmosphere_reward_coins
+ activity_subsidy_coins
```
Only `base_reward_coins` updates global and gift RTP windows. Room atmosphere and activity subsidies update their own budgets and are reported separately in metrics.
## Transaction Boundary
`ExecuteLuckyGiftDraw` uses one local MySQL transaction for the draw truth.
@ -305,15 +521,26 @@ The final random selection runs only on the filtered table. It must never select
```text
1. read idempotency by command_id
2. lock lucky_gift_rules active version
3. lock platform, room, gift pool rows with SELECT FOR UPDATE
4. load or lock risk counters for current windows
5. calculate pool inflow and available payout
6. filter tiers and execute random selection
7. update pool balances
8. update risk counters
9. insert lucky_draw_records
10. insert lucky_outbox if reward_coins > 0
11. commit
3. lock global lucky_rtp_windows row
4. lock gift lucky_rtp_windows row
5. lock platform, room, gift pool rows with SELECT FOR UPDATE
6. lock lucky_room_atmosphere_pools row
7. lock active lucky_activity_budgets row when configured
8. load or lock risk counters for current windows
9. choose novice/intermediate/advanced experience pool
10. calculate stage feedback state from the user's paid draw number
11. calculate base RTP capacity, room atmosphere capacity, and activity subsidy capacity
12. calculate single, hourly, daily, device, room, anchor, and activity risk remaining
13. calculate high multiplier headroom from RTP windows and three-layer pools
14. build and filter base, room atmosphere, and activity subsidy candidates
15. execute weighted random selection once
16. update platform, room, gift pool balances for base reward only
17. update global and gift RTP windows for base_reward_coins only
18. update room atmosphere and activity budget spending separately
19. update risk counters for all coin-paying rewards
20. insert lucky_draw_records with separated reward fields and snapshots
21. insert lucky_outbox if effective_reward_coins > 0 or room presentation is required
22. commit
```
After commit, `activity-service` may synchronously dispatch the wallet credit for low latency. If dispatch fails, it must keep the outbox pending and retry.
@ -336,7 +563,7 @@ Do not call `wallet-service` while holding MySQL pool locks. External RPC inside
## Randomness and Audit
V1 should use server-side cryptographically secure randomness. Every draw record must keep enough audit material to prove the result came from the recorded candidate table.
V2 should use server-side cryptographically secure randomness. Every draw record must keep enough audit material to prove the result came from the recorded candidate table.
Recommended audit fields:
@ -383,6 +610,14 @@ services/activity-service/
pool/
repository.go
allocator.go
rtp/
window.go
controller.go
atmosphere/
room_pool.go
presenter.go
budget/
activity_budget.go
reward/
wallet_client.go
dispatcher.go
@ -441,9 +676,16 @@ Required tests:
| Test | Assertion |
| --- | --- |
| probability table validation | ppm sum equals `1000000` |
| low-water filtering | unavailable tier probability moves to `none` |
| experience pool validation | novice, intermediate, advanced tier weights are positive and usable |
| RTP window validation | `100000` paid draws deviate from target RTP by `<1%` |
| RTP million validation | `1000000` paid draws deviate from target RTP by `<0.5%` |
| gift RTP sub-window validation | one gift cannot overpay while global RTP is still healthy |
| subsidy isolation | room atmosphere and activity subsidy do not update base RTP windows |
| low-water filtering | unavailable tier weight moves to `none` |
| pool transaction | platform, room, gift balances update atomically |
| RTP transaction | concurrent draws cannot double-spend the same window remaining payout |
| room atmosphere budget | shared rewards stop when room atmosphere budget reaches floor |
| activity budget exhaustion | subsidy candidates are disabled after budget or daily cap is exhausted |
| user daily cap | reward tier over cap is filtered before draw |
| idempotent draw | duplicate `command_id` returns same result |
| wallet credit retry | pending reward eventually becomes granted |
@ -453,6 +695,31 @@ Required tests:
Load tests should verify the hottest room path because room pool row locks can become the first bottleneck.
Production simulation must cover the full online control surface before service implementation starts:
```bash
go run ./tools/lucky-gift-v2-demo \
-users 1000 \
-devices 900 \
-rooms 50 \
-draws 100000 \
-base-rtp 95 \
-initial 1000000000 \
-cost 500 \
-global-window 100000 \
-gift-window 100000
```
The expected pass condition is:
| Check | Required Result |
| --- | --- |
| `base_rtp` | remains within the configured global and gift RTP windows |
| `effective_rtp` | may be higher than `base_rtp` only through room atmosphere or activity subsidy |
| platform, room, gift pools | no pool pays below its reserve floor |
| user, device, room, anchor caps | all coin-paying candidates are filtered before selection |
| high multiplier | disabled unless RTP windows and three-layer pools have enough high-water headroom |
## Rollout
V1 rollout order:

View File

@ -36,6 +36,8 @@
## Three Pool Model
三层奖池仍然是资金水位和风险敞口模型,不能被新手、中级、高级体验池替代。体验池只决定同一次抽奖可见的奖档分布,实际能不能发奖仍然由平台、房间、礼物三层水位以及全站/礼物基础 RTP 窗口共同决定。
### Platform Pool
平台总奖池控制全站幸运礼物的整体返还率。
@ -75,6 +77,123 @@
低价礼物适合高频小奖,高价礼物适合低频大奖。不同礼物必须使用独立概率表和独立水位,不要共用一个不可解释的全局概率。
## Five Layer Control Model
幸运礼物 V2 使用五层控制模型。三层资金水位继续负责“能不能付”,五层控制负责“钱怎么算、体验怎么呈现、补贴归谁”。
| Layer | Product Role | Cost Accounting | Hard Boundary |
| --- | --- | --- | --- |
| 全站 RTP 控制窗口 | 保证全站基础返奖成本 | 计入 `base_reward_coins`,进入全站基础 RTP | 不能被房间气氛、活动补贴污染 |
| 礼物 RTP 子窗口 | 控制单个幸运礼物成本结构 | 同一笔 `base_reward_coins` 同时进入礼物基础 RTP | 单礼物不能长期借其他礼物的 RTP |
| 用户体验池 | 新手、中级、高级奖档和节奏 | 不单独记成本,只选择基础候选奖档 | 不承诺单用户 RTP |
| 房间气氛池 | 控制房间爆点、共享奖励、飘屏强度 | 计入 `room_atmosphere_reward_coins` 或表现预算 | 不回写 Room Cell 核心状态 |
| 活动预算池 | 节日、榜单、运营补贴 | 计入 `activity_subsidy_coins` | 独立预算,不能冲抵基础 RTP |
用户看到的总返奖拆成三类:
```text
effective_reward_coins = base_reward_coins
+ room_atmosphere_reward_coins
+ activity_subsidy_coins
```
成本口径必须分开:
| Metric | Formula | Meaning |
| --- | --- | --- |
| `base_rtp` | `sum(base_reward_coins) / sum(coin_spent)` | 平台承诺和风控使用的基础 RTP |
| `gift_base_rtp` | `sum(base_reward_coins by gift_id) / sum(coin_spent by gift_id)` | 单礼物成本健康度 |
| `effective_rtp` | `sum(effective_reward_coins) / sum(coin_spent)` | 用户实际感受到的总返还 |
| `subsidy_cost` | `sum(room_atmosphere_reward_coins + activity_subsidy_coins)` | 运营和房间气氛额外成本 |
运营要提高娱乐性时,优先调整体验池、房间气氛池和活动预算池;不要直接把基础 RTP 拉高。基础 RTP 是平台成本底线,不是用户体感的唯一来源。
## Experience Pools
新手池、中级池、高级池是同一个幸运礼物规则下的三个体验配置。它们不单独拥有金币余额,不直接绕过平台、房间、礼物三层奖池,不承诺单个用户固定 RTP。
| Pool | Entry Rule | Product Shape | Payout Boundary |
| --- | --- | --- | --- |
| `novice` | 用户当前幸运礼物累计付费抽数较低,或处于新手保护期 | 高频小奖、少量返还、禁止超高倍大奖 | 单次奖励上限最低,避免新用户误解为高波动博彩 |
| `intermediate` | 用户已经完成基础体验但未进入高频阶段 | 小奖和中等奖混合,中奖频率下降 | 允许中等奖,不允许消耗高级奖池预算 |
| `advanced` | 高频幸运礼物用户或高价幸运礼物 | 低频高倍、强调飘屏和房间表现 | 单次奖励上限最高,但必须被总 RTP、三层水位和风控同时约束 |
默认分池建议按付费抽数切换,服务端只使用 UTC 计数,不使用客户端本地时间:
| Pool | Default Draw Range | Notes |
| --- | ---: | --- |
| `novice` | `[1, 2000]` | 首次体验阶段,奖档更密但倍数更低 |
| `intermediate` | `[2001, 20000]` | 常规用户阶段,保留中等奖表现 |
| `advanced` | `[20001, +∞)` | 高频阶段,大奖概率更低但上限更高 |
分池只影响基础候选奖档和表现节奏。总返还率必须由全站 RTP 控制窗口和礼物 RTP 子窗口共同收敛,不能依赖三个体验池各自“碰运气”。
### Stage Feedback
阶段反馈是体验反馈,不是单用户保底。默认在新手池内按付费抽数进度触发:`1``25%``50%``75%``100%` of `novice_draw_limit`
| Feedback Type | Cost Accounting | Allowed Behavior |
| --- | --- | --- |
| 进度反馈 | `presentation_only` | 展示阶段、进度、动画、房间幸运值,不发金币 |
| 小额反馈奖 | `base_rtp` or `activity_subsidy` | 如果发金币,必须抽前进入基础候选或活动预算候选 |
| 阶段完成提示 | `presentation_only` | 告知用户已离开新手池或进入常规池 |
阶段反馈不能绕过 RTP、三层水位和风控上限。任何“第 N 抽必给金币”的设计都必须先占用明确预算,否则会把基础 RTP 控制窗口打穿。
## RTP Control Model
单纯固定概率表不能保证 `10w` 次上下浮动不超过 `1%``100w` 次上下浮动不超过 `0.5%`,尤其是高级池包含高倍奖时。幸运礼物必须使用“概率体验 + 整数 RTP 控制窗口”的组合模型。
控制规则:
| Rule | Decision |
| --- | --- |
| 控制口径 | 每次抽奖同时进入全站 RTP 窗口和礼物 RTP 子窗口 |
| 控制窗口 | 默认 `global_window_draws=100000`,礼物子窗口可按流量配置更大窗口 |
| RTP 输入 | 使用 `target_rtp_ppm` 保存,例如 `95% = 950000` |
| 目标支出 | `window_target_payout = floor((window_wager * target_rtp_ppm + carry_ppm) / 1000000)` |
| 余数处理 | 除不尽的 ppm 余数进入下一个窗口的 `carry_ppm` |
| 候选过滤 | 每次抽奖前过滤三层水位、风控、体验池奖档和窗口剩余可支付额 |
| 随机范围 | 只在过滤后的候选奖档中随机,随机后不允许后台改结果 |
| 窗口补偿 | 全站或礼物窗口剩余抽数不足以自然追平目标时,抽前启用基础 RTP 补偿候选奖档 |
窗口内每次抽奖都维护两个整数:
```text
global_remaining_draws = global_window_draws - global_window_paid_draws
global_remaining_payout = global_window_target_payout - global_window_base_payout
gift_remaining_draws = gift_window_draws - gift_window_paid_draws
gift_remaining_payout = gift_window_target_payout - gift_window_base_payout
```
候选奖档必须满足:
```text
base_reward_coins <= global_remaining_payout
base_reward_coins <= gift_remaining_payout
global_remaining_payout - base_reward_coins
<= global_min_future_max_reward * (global_remaining_draws - 1)
gift_remaining_payout - base_reward_coins
<= gift_min_future_max_reward * (gift_remaining_draws - 1)
```
其中 `*_min_future_max_reward` 是对应窗口后续任意体验池至少能支付的单次最大基础返奖。这个约束保证窗口不会因为前面连续未中奖而在最后几抽无法追平基础 RTP。
达成条件:
| Sample Size | Required Result | Control Requirement |
| ---: | --- | --- |
| `100000` paid draws | 全站基础 RTP 与目标 RTP 绝对偏差 `< 1%` | 一个完整全站控制窗口内目标支出按整数配额结清 |
| `1000000` paid draws | 全站基础 RTP 与目标 RTP 绝对偏差 `< 0.5%` | 十个完整全站控制窗口累计偏差只剩整数余数 |
| 单礼物流量达到配置窗口 | 礼物基础 RTP 收敛到礼物目标 | 礼物子窗口独立结清,不能长期靠全站窗口兜底 |
这个保证只适用于控制口径内的总 paid draws不适用于单个用户、单个房间或单个很小流量分桶。单个用户仍然可能亏损或盈利平台只能承诺全站和礼物基础 RTP 收敛。
如果三层奖池水位、风控硬上限或钱包入账失败导致基础返奖无法支付,系统必须记录 `rtp_underpay` 风险事件并降低基础 RTP不能透支奖池或抽后篡改结果。房间气氛和活动补贴也不能用于“补平”基础 RTP只能提高用户看到的 `effective_rtp`
## Risk Layer
风控层不在抽奖后篡改结果,只在抽奖前决定是否允许参与、是否降低可用奖档、是否进入人工观察。
@ -89,6 +208,59 @@
| `max_activity_period_payout` | 活动周期最大奖励金币 |
| `max_user_hit_count_window` | 单用户窗口期最大命中次数 |
上限制定依据:
```text
expected_payout(window) = paid_draw_capacity(window)
* gift_price
* target_rtp_ppm
/ 1000000
single_cap <= min(gift_sub_window_target_payout, single_event_risk_budget)
hourly_cap >= expected_payout(user_hour_window) * volatility_factor
daily_cap >= expected_payout(user_day_window) * volatility_factor
```
默认建议:
| Limit | Base Formula | Why |
| --- | --- | --- |
| 单次上限 | `gift_price * max_allowed_multiplier` | 控制单事件最大曝光,直接决定是否允许 100x/500x |
| 小时上限 | `expected_hourly_payout * 1.5~3.0` | 覆盖正常波动,同时限制短时异常套利 |
| 日上限 | `expected_daily_payout * 1.2~2.0` | 覆盖高活跃用户正常体验,同时保护单日预算 |
| 高水位开高倍 | `min(global_headroom, gift_headroom, pool_headroom) >= high_reward` | 只有此前少付或奖池积累足够时,才允许高倍进入候选 |
如果小时或日上限低于对应窗口的期望基础返奖,系统将无法同时满足用户上限和全站 RTP。此时必须降低目标 RTP、降低用户可抽容量、提高上限或把额外体验放到活动预算池而不是让基础 RTP 窗口透支。
高水位定义:
```text
rtp_headroom = remaining_base_payout
- remaining_draws * gift_price * target_rtp_ppm / 1000000
high_multiplier_enabled = global_rtp_headroom >= tier_reward
&& gift_rtp_headroom >= tier_reward
&& platform_pool_headroom >= tier_reward
&& room_pool_headroom >= tier_reward
&& gift_pool_headroom >= tier_reward
```
基础最小版本暂时只验证 `global_rtp_headroom``gift_rtp_headroom`。接入真实奖池后,必须同时校验平台、房间、礼物三层水位。
线上标准强制口径:
| Control | Required Behavior |
| --- | --- |
| 全站 RTP 窗口 | 每一笔付费抽奖都进入,`base_reward_coins` 必须按窗口整数配额结清 |
| 礼物 RTP 子窗口 | 同一笔基础返奖同时进入礼物窗口,单礼物不能借其他礼物的成本 |
| 平台/房间/礼物三层池 | 每抽先按 `pool_rate_ppm` 入池,再按责任权重支付基础返奖;任一层低于 reserve floor 时过滤对应奖档 |
| 新手/中级/高级体验池 | 只控制候选奖档和节奏,不持有预算,不承诺单用户 RTP |
| 单次/小时/日/设备/房间/主播上限 | 抽前过滤所有会超过剩余额度的付费候选,抽后只更新计数 |
| 高水位高倍 | 只有全站窗口、礼物窗口、三层池水位同时满足高倍奖励时100x/500x 才能进候选 |
| 房间气氛池 | 独立预算,只提升房间爆点和共享奖励,不写入基础 RTP |
| 活动预算池 | 独立补贴,只提高 `effective_rtp`,不能冲抵基础 RTP 欠付 |
| 钱包和房间表现 | 扣费必须先成功;返奖和房间表现通过 outbox 重试,不能在持有奖池锁时调用外部 RPC |
触发阈值后的产品动作:
| Action | When |
@ -109,6 +281,12 @@
| `gift_price` | 单个礼物金币价格 |
| `pool_rate` | 每笔礼物进入幸运奖池的比例 |
| `target_payout_rate` | 长期目标返还率 |
| `target_rtp_ppm` | RTP 控制窗口使用的目标返还率 |
| `control_window_draws` | RTP 控制窗口 paid draws 数 |
| `gift_rtp_window_draws` | 单礼物 RTP 子窗口 paid draws 数 |
| `experience_pool_rules` | 新手、中级、高级分池进入规则 |
| `room_atmosphere_rule` | 房间爆点、共享奖励和表现规则 |
| `activity_budget_id` | 当前可用活动补贴预算 ID |
| `reserve_floor` | 最低安全水位 |
| `max_single_payout` | 单次最高奖励金币 |
| `max_user_daily_payout` | 单用户每日最高奖励金币 |
@ -116,16 +294,90 @@
| `probability_table_version` | 概率表版本 |
| `enabled_reward_tiers` | 当前启用奖档 |
示例概率表只表达结构,不作为正式运营参数:
示例权重表只表达结构,不作为正式运营参数。体验池权重用于表现节奏,不直接决定总 RTP。
| Tier | Reward | Base Probability | Availability |
| ----------- | ----------------: | ---------------: | ----------------------------- |
| `none` | `0` | `88.7000%` | always |
| `small_1x` | `gift_price * 1` | `10.0000%` | pool safe |
| `medium_5x` | `gift_price * 5` | `1.0000%` | pool safe and user limit safe |
| `large_50x` | `gift_price * 50` | `0.3000%` | high water only |
| Pool | Tier | Reward | Base Weight | Availability |
| --- | --- | ---: | ---: | --- |
| `novice` | `none` | `0` | `270000` | always |
| `novice` | `rebate_0_5x` | `gift_price * 0.5` | `200000` | pool safe |
| `novice` | `rebate_1x` | `gift_price * 1` | `350000` | pool safe |
| `novice` | `small_2x` | `gift_price * 2` | `140000` | pool safe |
| `novice` | `small_5x` | `gift_price * 5` | `35000` | pool safe |
| `novice` | `small_10x` | `gift_price * 10` | `5000` | pool safe |
| `intermediate` | `none` | `0` | `760000` | always |
| `intermediate` | `rebate_1x` | `gift_price * 1` | `100000` | pool safe |
| `intermediate` | `small_2x` | `gift_price * 2` | `80000` | pool safe |
| `intermediate` | `medium_5x` | `gift_price * 5` | `45000` | pool safe and user limit safe |
| `intermediate` | `medium_20x` | `gift_price * 20` | `13000` | high water only |
| `intermediate` | `medium_50x` | `gift_price * 50` | `2000` | high water only |
| `advanced` | `none` | `0` | `900000` | always |
| `advanced` | `small_2x` | `gift_price * 2` | `50000` | pool safe |
| `advanced` | `medium_5x` | `gift_price * 5` | `30000` | pool safe |
| `advanced` | `large_20x` | `gift_price * 20` | `15000` | high water only |
| `advanced` | `large_100x` | `gift_price * 100` | `4500` | high water only |
| `advanced` | `jackpot_500x` | `gift_price * 500` | `500` | high water and risk safe |
奖池低水位时,如果 `large_50x` 不可用,它的 `0.3000%` 必须进入 `none`,不能转给 `small_1x``medium_5x`
奖池低水位时,如果 `large_100x` 不可用,它的权重必须进入 `none`不能转给其他奖档。RTP 控制窗口需要追平目标时,只能在抽前选择当前可支付的补偿奖档,不能在随机后把未中奖改成中奖。
## V2 Draw Algorithm
V2 抽奖算法把“基础返奖”和“娱乐补贴”分开算。基础返奖负责 RTP房间气氛和活动补贴负责体验。
```text
input:
command_id, user_id, room_id, gift_id, gift_price, paid_at_ms
state:
global_rtp_window
gift_rtp_window(gift_id)
platform_pool, room_pool(room_id), gift_pool(gift_id)
user_experience_state(user_id, gift_id)
room_atmosphere_pool(room_id)
activity_budget(optional)
risk_counters
```
标准算法:
```text
1. 用 command_id 做幂等查询,已有结果直接返回
2. 选择用户体验池novice / intermediate / advanced
3. 计算阶段反馈点;反馈只产生展示事件或已预算候选,不直接发未预算金币
4. 锁定全站 RTP 窗口和礼物 RTP 子窗口
5. 锁定平台、房间、礼物三层资金水位
6. 锁定房间气氛池和可用活动预算
7. 读取风险计数和用户窗口状态
8. 计算基础返奖容量:
global_base_capacity
gift_base_capacity
platform_capacity
room_capacity
gift_capacity
risk_capacity
9. 计算单次、小时、日、设备、房间、主播和活动周期剩余额度
10. 计算高倍奖档水位;只有全站、礼物和三层奖池高水位都足够时才开放高倍
11. 从体验池生成基础候选奖档
12. 过滤所有无法同时满足全站 RTP、礼物 RTP、三层水位和风控的基础奖档
13. 生成房间气氛候选:飘屏、共享红包、房间幸运值、表现强化
14. 生成活动补贴候选:限时加奖、榜单补贴、节日额外返奖
15. 合并候选并执行一次随机
16. 写入抽奖事实、窗口快照、预算快照和随机审计材料
17. 更新全站 RTP 窗口和礼物 RTP 子窗口,仅写入 base_reward_coins
18. 更新房间气氛池和活动预算,仅写入各自补贴支出
19. 提交事务
20. 事务后通过 wallet-service 发金币,通过 room-service/腾讯云 IM 做房间表现
```
候选奖档必须带预算来源:
| Budget Source | Pays Coins | Counts In Base RTP | Typical Use |
| --- | --- | --- | --- |
| `base_rtp` | yes | yes | 常规小奖、中奖、大奖 |
| `room_atmosphere` | optional | no | 房间红包、共享奖励、飘屏强化 |
| `activity_subsidy` | yes | no | 节日加奖、榜单补贴、限时返利 |
| `presentation_only` | no | no | 动画、称号、房间幸运值 |
随机前必须完成过滤。随机后只能落账、补偿或返回 pending不能把结果改成另一个奖档。
## Draw Flow
@ -133,10 +385,13 @@
```text
读取规则版本
按用户累计付费抽数选择 novice/intermediate/advanced 体验池
读取平台、房间、礼物奖池水位
读取或创建全站 RTP 窗口和礼物 RTP 子窗口
读取房间气氛池和活动预算
读取用户、设备、房间、主播、活动风控计数
过滤不可支付或不可触发奖档
生成候选奖档
过滤不可支付、不可触发或会破坏基础 RTP 窗口追平能力的基础奖档
生成基础返奖、房间气氛、活动补贴候选奖档
执行随机
写入抽奖结果日志
发放金币奖励
@ -181,9 +436,15 @@
| Metric | Reason |
| ----------------------- | -------------------- |
| `lucky_gift_gmv` | 幸运礼物金币消耗 |
| `lucky_reward_payout` | 金币奖励支出 |
| `actual_payout_rate` | 实际返还率 |
| `lucky_base_reward_payout` | 基础 RTP 金币奖励支出 |
| `room_atmosphere_payout` | 房间气氛和共享奖励支出 |
| `activity_subsidy_payout` | 活动补贴支出 |
| `base_rtp` | 基础返还率,平台成本口径 |
| `effective_rtp` | 用户可见返还率,含补贴 |
| `gift_base_rtp` | 单礼物基础返还率 |
| `pool_balance_by_scope` | 平台、房间、礼物水位 |
| `room_atmosphere_balance` | 房间气氛预算水位 |
| `activity_budget_balance` | 活动补贴预算水位 |
| `hit_rate_by_tier` | 各奖档命中率 |
| `reward_credit_latency` | 金币到账延迟 |
| `risk_reject_count` | 风控拒绝次数 |

View File

@ -0,0 +1,183 @@
# 幸运礼物线上落地文档
本文档定义当前幸运礼物在 `activity-service` 的线上落地边界、后台参数、抽奖事务和验证方式。幸运礼物只拥有活动决策、RTP 窗口、奖池和抽奖事实;金币扣费和返奖入账仍归 `wallet-service`,房间状态和房间表现仍归 `room-service`
## Current Scope
本次落地包含:
| Scope | Status | Notes |
| --- | --- | --- |
| `LuckyGiftService.CheckLuckyGift` | done | 房间送礼前检查礼物是否启用,并返回当前体验池 |
| `LuckyGiftService.ExecuteLuckyGiftDraw` | done | 扣费成功后执行抽奖决策,写抽奖事实和 outbox |
| `AdminLuckyGiftService.GetLuckyGiftConfig` | done | 后台读取全局幸运礼物规则;未配置时返回默认草稿 |
| `AdminLuckyGiftService.UpsertLuckyGiftConfig` | done | 后台发布全局幸运礼物规则版本 |
| `AdminLuckyGiftService.ListLuckyGiftDraws` | done | 后台审计抽奖记录 |
| 三层基础奖池 | done | platform / room / gift |
| RTP 双窗口 | done | global + gift |
| 风控计数 | done | user hour/day、device day、room hour、anchor day |
| 房间气氛池 | done | 独立预算,不污染基础 RTP |
| 活动预算池 | done | 独立补贴,不污染基础 RTP |
暂不在 `activity-service` 事务内调用钱包或房间 RPC。抽奖事务只写事实和 `activity_outbox`;后续由 outbox dispatcher 或房间链路消费结果。
## Call Flow
```text
room-service SendGift
1. CheckLuckyGift(meta, user_id, room_id, gift_id)
2. wallet-service 扣费成功
3. ExecuteLuckyGiftDraw(command_id, user_id, device_id, room_id, anchor_id, gift_id, coin_spent, paid_at_ms)
4. activity-service 本地事务写 draw fact + outbox
5. 事务后由钱包/房间表现链路处理 reward_status=pending 的结果
```
`command_id` 是抽奖幂等键。重复 `ExecuteLuckyGiftDraw` 会返回同一条 `lucky_draw_records`,不会重复消耗 RTP、奖池或预算。
后台只维护一份全局幸运礼物规则,实际抽奖仍记录用户发送的真实 `gift_id`。后台不暴露单抽消耗;服务端保存固定内部换算单位,不同幸运礼物实际消耗不同,抽奖事务按实际 `coin_spent` 等比缩放基础奖档,并按实际 `coin_spent` 累计 RTP 目标和奖池入账。
## Transaction Boundary
`ExecuteLuckyGiftDraw` 使用一个 MySQL 本地事务,事务内必须完成:
1. 按 `command_id` 锁定幂等记录。
2. 锁定 `lucky_gift_rules` 全局规则。
3. 锁定或创建用户体验状态 `lucky_user_states`
4. 锁定或创建全站 RTP 窗口、幸运礼物整体子窗口。
5. 锁定或创建平台、房间、幸运礼物整体三层基础奖池。
6. 锁定或创建房间气氛池。
7. 锁定或创建活动总预算桶和 UTC 日预算桶。
8. 锁定用户、设备、房间、主播风控计数。
9. 抽前过滤所有不可支付候选。
10. 执行一次服务端安全随机。
11. 更新 RTP 窗口、奖池、预算、风控计数、用户状态。
12. 插入 `lucky_draw_records`
13. 必要时插入 `activity_outbox`
事务内不得调用 `wallet-service``room-service` 或腾讯云 IM避免外部 RPC 卡住奖池行锁。
## RTP Accounting
基础返奖只使用 `base_reward_coins`
```text
base_rtp = sum(base_reward_coins) / sum(coin_spent)
```
用户可见返奖使用:
```text
effective_reward_coins = base_reward_coins
+ room_atmosphere_reward_coins
+ activity_subsidy_coins
```
房间气氛池和活动预算只能提高 `effective_rtp`,不能用于补平基础 RTP。
## Admin Parameters
后台参数分为 6 类。
后台 HTTP 入口:
| Method | Path | Permission | Meaning |
| --- | --- | --- | --- |
| `GET` | `/api/v1/admin/activity/lucky-gifts/config` | `lucky-gift:view` | 读取全局幸运礼物规则;未配置返回默认草稿 |
| `PUT` | `/api/v1/admin/activity/lucky-gifts/config` | `lucky-gift:update` | 发布新的全局幸运礼物规则版本 |
| `GET` | `/api/v1/admin/activity/lucky-gifts/draws` | `lucky-gift:view` | 查询抽奖审计记录 |
| Group | Fields | Meaning |
| --- | --- | --- |
| RTP 成本 | `target_rtp_ppm`, `pool_rate_ppm`, `global_window_draws`, `gift_window_draws` | 控制基础 RTP 和收敛窗口 |
| 三层池 | `platform_pool_weight_ppm`, `room_pool_weight_ppm`, `gift_pool_weight_ppm`, `initial_*_pool`, `*_reserve` | 控制平台、房间、礼物各自承担的基础返奖能力 |
| 体验池 | `novice_draw_limit`, `intermediate_draw_limit`, `tiers` | 控制新手/中级/高级奖档结构 |
| 高倍 | `high_multiplier`, `high_water_pool_multiple`, `large_tier_enabled`, tier `high_water_only` | 控制 100x/500x 等高倍是否可进入候选 |
| 风控 | `max_single_payout`, `user_hourly_payout_cap`, `user_daily_payout_cap`, `device_daily_payout_cap`, `room_hourly_payout_cap`, `anchor_daily_payout_cap` | 控制单用户、设备、房间、主播关联风险敞口 |
| 娱乐补贴 | `room_atmosphere_rate_ppm`, `room_atmosphere_initial`, `room_atmosphere_reserve`, `activity_budget`, `activity_daily_limit` | 控制房间爆点和活动补贴 |
调整方向:
| Goal | Primary Change |
| --- | --- |
| 降成本 | 降 `target_rtp_ppm`,降补贴预算,提高高水位倍数 |
| 增刺激 | 增房间气氛和活动预算,提高中高倍权重 |
| 新手更友好 | 提高新手小奖权重,延长 `novice_draw_limit` |
| 防套利 | 降低用户/设备/房间/主播上限,关闭高倍 |
| RTP 更稳 | 缩小 RTP 窗口 |
| 体验更自然 | 放大 RTP 窗口,但接受短周期波动 |
## Data Tables
| Table | Owner Data |
| --- | --- |
| `lucky_gift_rules` | 全局幸运礼物规则和后台可调参数;规则作用域固定为 `__global__` |
| `lucky_rtp_windows` | 全站/幸运礼物整体 RTP 窗口 |
| `lucky_pools` | platform / room / lucky-gift-global 三层基础奖池 |
| `lucky_room_atmosphere_pools` | 房间气氛预算 |
| `lucky_activity_budgets` | 活动补贴预算;`budget_day=__total__` 控总额UTC 日期行控日释放 |
| `lucky_user_states` | 用户在幸运礼物整体上的累计付费抽数 |
| `lucky_risk_counters` | 风控窗口计数 |
| `lucky_draw_records` | 每次抽奖事实和审计快照 |
## Verification
Proto 变更后必须运行:
```bash
PATH=/tmp/codex-protoc/bin:$HOME/go/bin:$PATH make proto
```
服务编译验证:
```bash
go test ./services/activity-service/...
```
模型验证:
```bash
go run ./tools/lucky-gift-v2-demo \
-users 1000 \
-devices 900 \
-rooms 50 \
-draws-min 0 \
-draws-max 100000 \
-unique-draws \
-base-rtp 95 \
-initial 1000000000 \
-cost 500 \
-global-window 100000 \
-gift-window 100000
```
验收口径:
| Check | Requirement |
| --- | --- |
| `100000` paid draws | 基础 RTP 偏差 `< 1%` |
| `1000000` paid draws | 基础 RTP 偏差 `< 0.5%` |
| lucky gift RTP | 幸运礼物整体子窗口独立收敛 |
| pool safety | 三层池不低于 reserve |
| subsidy isolation | 房间气氛/活动补贴不进入基础 RTP |
| idempotency | 重复 command_id 返回同一 draw |
## Rollout
建议上线顺序:
1. 后台创建规则草稿,`enabled=false`
2. 跑模型模拟并审核参数。
3. 发布 `enabled=true` 到全局幸运礼物配置。
4. 只开放内部房间或白名单房间。
5. 观察 `base_rtp``effective_rtp`、pool balance、risk reject、outbox pending。
6. 稳定后逐步开放更多房间和礼物。
必须保留的开关:
| Switch | Effect |
| --- | --- |
| `enabled=false` | 全站幸运礼物停止抽奖 |
| `large_tier_enabled=false` | 关闭高倍奖档 |
| `activity_budget=0` | 关闭活动补贴 |
| `room_atmosphere_rate_ppm=0` | 关闭房间气氛补贴 |
| 降低各风控 cap | 快速压缩风险敞口 |

View File

@ -0,0 +1,590 @@
# 语音房宝箱功能架构
本文定义语音房宝箱的产品规则、服务边界、App 接口、IM 事件、后台配置、奖励结算和验收边界。宝箱是房间内送礼驱动的互动玩法:用户在语音房送礼积攒能量,当前等级宝箱能量满后进入倒计时,倒计时结束时按房间快照抽奖并发放奖励。所有周期、重置和统计口径统一使用 UTC。
## Goals
| 目标 | 规则 |
| --- | --- |
| 礼物积攒能量 | 只有 `SendGift` 扣费成功并落 Room Cell 后才增加宝箱能量 |
| 7 级宝箱 | 每个 UTC 自然日从 1 级开始,最多开启 7 个等级 |
| 满能量倒计时开启 | 能量达到当前等级阈值后锁定本轮,按后台 `open_delay_ms` 设置 `open_at_ms` |
| 满能量播报 | 满能量后立即或按后台延迟发起播报,播报范围支持 `none/region/global` |
| 开箱抽奖 | `open_at_ms` 到达后快照房间在线用户、点火人和贡献第一,生成奖励结算事实 |
| 自动发奖 | 奖励由 activity-service 幂等抽奖并调用 wallet-service 发放资源组,不需要 App 手动领取 |
| UTC 重置 | 进度、贡献和已开启等级按 UTC 日期重置,不使用服务器本地时区或客户端时区 |
## Non-Goals
| 不做 | 原因 |
| --- | --- |
| 客户端提交能量、奖励或中奖结果 | 能量和奖励必须来自服务端扣费、配置和随机结果 |
| 在 room-service 保存钱包余额或发奖账本 | 钱包资产和资源发放仍由 wallet-service 拥有 |
| 由 activity-service 判断谁在房 | 房间 presence 和开箱快照必须由 Room Cell 产出 |
| 只用 Redis 保存宝箱进度 | MySQL snapshot/command log/宝箱表才是恢复来源 |
| 宝箱发奖失败时提前展示已到账 | 客户端只能展示 wallet-service 已成功发放的奖励 |
## Missing Interfaces
你的描述已经覆盖了核心 App 查询和 3 类 IM但还缺这些接口和后台能力。
| 类型 | 接口 | 必要性 |
| --- | --- | --- |
| App 查询 | `GET /api/v1/rooms/{room_id}/treasure-chest` | 房间页主动同步宝箱配置、当前进度、倒计时、重置时间和最近结算 |
| App 查询 | `GET /api/v1/rooms/{room_id}/treasure-chest/rewards?round_id=...&scope=mine\|public` | 用户重连或错过 IM 后查询自己奖励、公开奖励摘要和发奖状态 |
| App 查询 | `JoinRoom` / `RoomDetail` 可内嵌 `treasure_chest_summary` | 避免进房首屏先空白,再等单独接口返回 |
| App 命令响应 | `SendGiftResponse` 增加 `treasure_chest_delta` | 送礼人成功送礼后立即更新能量和倒计时,不只依赖异步 IM |
| Admin 配置 | `GET/PUT /api/v1/admin/room-treasure/config` | 配置开关、7 级阈值、物料、倒计时、播报范围、奖励池和生效版本 |
| Admin 审计 | `GET /api/v1/admin/room-treasure/rounds` | 排查某房某天宝箱进度、点火人、贡献第一和状态 |
| Admin 审计 | `GET /api/v1/admin/room-treasure/reward-records` | 查询用户中奖、发奖、失败和重试记录 |
| Admin 补偿 | `POST /api/v1/admin/room-treasure/rounds/{round_id}/retry-settlement` | 对抽奖或 wallet 发放失败的轮次做幂等补偿 |
| Internal room | `ProcessDueTreasureChests` | cron 或 room-service worker 触发到期开箱,不能依赖单机内存 timer |
| Internal room | `ApplyTreasureChestSettlement` | activity-service 发奖完成后回写结算摘要,由 room-service 发房间发奖 IM |
| Internal activity | `ExecuteTreasureChestSettlement` 或消费 `RoomTreasureChestOpened` outbox | 按 Room Cell 快照做抽奖、写奖励记录、调用 wallet 发奖 |
| Internal wallet | 复用 `GrantResourceGroup`,后续补 `BatchGrantResourceGroup` | 每个在房用户都可能获奖,首版可逐个幂等发放,大房间需要批量优化 |
如果坚持“所有奖励完全自动到账”,不需要 App `claim` 接口;如果产品改成手动领取,必须新增 `POST /api/v1/rooms/{room_id}/treasure-chest/rewards/{reward_id}/claim`,并重新定义过期和未领取回收规则。
## Ownership
| 模块 | 拥有 | 不拥有 |
| --- | --- | --- |
| `room-service` | 宝箱进度、等级、状态机、点火人、贡献榜、开箱在线用户快照、房间宝箱 IM | 奖励实际入账、用户完整资料、全局/区域播报群成员 |
| `activity-service` | 宝箱奖励抽奖、奖励记录、发奖重试、全局/区域播报 outbox | Room Cell presence、房间核心状态 |
| `wallet-service` | 资源组发放、金币/钻石入账、钱包 outbox、幂等账本 | 宝箱进度和中奖资格 |
| `gateway-service` | App HTTP envelope、鉴权、聚合 room/activity/wallet 读模型 | 能量、抽奖和发奖事实 |
| `server/admin` | 后台权限、表单校验、审计、调用 owner service 管理 RPC | 直接写 room/activity/wallet 业务表 |
宝箱进度必须经过 Room Cell 命令链路。activity-service 可以消费 room outbox 做奖励结算和播报,但不能自己判断房间内用户、不能直接改宝箱进度。
## State Machine
```mermaid
stateDiagram-v2
[*] --> disabled
disabled --> charging: config enabled
charging --> countdown: energy >= threshold
countdown --> opening: now >= open_at_ms
opening --> settling: RoomTreasureChestOpened outbox committed
settling --> settled: rewards terminal
settled --> charging: next level exists
settled --> exhausted: level 7 settled
exhausted --> charging: next UTC day
charging --> charging: UTC daily reset
```
| 状态 | 语义 |
| --- | --- |
| `disabled` | 后台关闭,不累计能量,不发宝箱 IM |
| `charging` | 当前等级可累积能量 |
| `countdown` | 当前等级已满,`open_at_ms` 已确定,本轮进度、点火人和配置版本锁定 |
| `opening` | 到期开箱命令正在 Room Cell 内提交,快照参与用户 |
| `settling` | activity-service 正在抽奖和调用 wallet 发奖 |
| `settled` | 本轮奖励达到终态,房间已发结算 IM |
| `exhausted` | 当天 7 级都已开启,等下一个 UTC 自然日 |
`round_id` 建议稳定生成:
```text
treasure:<room_id>:<cycle_day>:<level>:<sequence>
```
`cycle_day` 固定为 UTC `YYYY-MM-DD`。同一房间同一 UTC 日同一等级只允许一个 active round。
## Energy Rules
| 规则 | 决策 |
| --- | --- |
| 能量来源 | 默认使用 `DebitGiftResponse.gift_point_added`;后台可配置 gift_id 级别倍率或覆盖值 |
| 生效条件 | wallet 扣费成功、room-service 已提交 `RoomGiftSent`、礼物能量值大于 0 |
| 幂等 | 同一 `SendGift.command_id` 只能增加一次宝箱能量 |
| 点火人 | 第一笔让当前等级 `progress >= threshold` 的送礼用户 |
| 贡献第一 | 当前 round 内累计能量最高用户;同分按首次达到该分值时间,再按 user_id 升序 |
| 溢出能量 | 单笔礼物超过当前等级剩余阈值时,只计入补满当前等级所需能量,多余能量作废 |
| 倒计时期间送礼 | 当前等级已经满能量,继续送礼不再增加宝箱能量,也不会排队到下一等级 |
| 7 级后送礼 | `exhausted` 后当天不再累计宝箱能量,礼物仍正常扣费、热度和榜单照常 |
溢出和倒计时期间的无效能量只用于审计,不计入当前 round 贡献、不计入下一等级进度、不改变点火人或贡献第一。送礼本身仍正常扣费、热度和房间礼物榜照常更新。
## Opening Rules
| 场景 | 处理 |
| --- | --- |
| 满能量 | Room Cell 把 round 切到 `countdown`,写 `RoomTreasureChestCountdownStarted` outbox |
| 播报 | activity-service 消费 countdown 事件,按配置发 `region/global` 播报;失败只重试播报,不回滚宝箱 |
| 到期开箱 | room-service worker 或 cron-service 触发 `ProcessDueTreasureChests`,由 Room Cell 提交开箱命令 |
| 开箱快照 | 以开箱命令执行时的 `OnlineUsers` 为准生成在房奖励参与人 |
| 房间关闭 | 如果已有 `countdown` roundCloseRoom 必须先提交开箱快照或取消策略;推荐先快照结算,再关闭房间 |
| 房间无人 | 在房奖励参与人为空;点火人和贡献第一仍按 round 贡献事实抽奖 |
点火人和贡献第一不要求开箱时仍在房或在线。只要用户在本 round 已经形成点火人或贡献第一事实,即使开箱前离房、断线或下线,也必须保留对应角色奖励;在房奖励仍只发给开箱快照里的 `OnlineUsers`
## Reward Rules
| 奖励槽位 | 资格 | 默认是否可与其他槽位叠加 |
| --- | --- | --- |
| `in_room` | 开箱时 Room Cell `OnlineUsers` 内的每个用户 | 可叠加 |
| `top1` | 当前 round 贡献第一用户 | 可叠加 |
| `igniter` | 当前 round 点火人 | 可叠加 |
奖励配置按等级拆分,每个槽位是一个加权奖励池。奖励项推荐引用 `resource_group_id`,由 wallet-service 按资源组快照发放金币、钻石或资源权益。
```json
{
"level": 3,
"slot": "top1",
"items": [
{
"reward_item_id": "lv3_top1_a",
"resource_group_id": 12003,
"weight": 9000,
"display_name": "Top1 Reward",
"icon_url": "https://cdn.example/reward.png"
}
]
}
```
发奖幂等键:
```text
treasure_reward:<round_id>:<slot>:<user_id>
```
activity-service 对每个奖励记录先写 `treasure_chest_reward_records`,再调用 wallet-service。wallet 成功后才能把该记录标记为 `granted`。失败进入 `retryable/failed`,后台补偿必须复用同一幂等键。
## App Interfaces
### Get Treasure Chest
```http
GET /api/v1/rooms/{room_id}/treasure-chest
Authorization: Bearer <access_token>
```
Response `data`:
```json
{
"enabled": true,
"server_time_ms": 1779120000000,
"reset_at_ms": 1779148800000,
"config_version": 12,
"current": {
"cycle_day": "2026-05-19",
"round_id": "treasure:lalu_room_1:2026-05-19:3:1",
"level": 3,
"status": "charging",
"progress": 3600,
"threshold": 10000,
"progress_ppm": 360000,
"opened_level_count": 2,
"open_at_ms": 0,
"igniter_user_id": "0",
"top1_user_id": "10001"
},
"levels": [
{
"level": 1,
"threshold": 3000,
"cover_url": "https://cdn.example/chest/lv1/cover.png",
"animation_url": "https://cdn.example/chest/lv1/idle.svga",
"opening_animation_url": "https://cdn.example/chest/lv1/opening.svga",
"opened_image_url": "https://cdn.example/chest/lv1/opened.png",
"rewards": {
"in_room": [],
"top1": [],
"igniter": []
}
}
],
"latest_round": {
"round_id": "treasure:lalu_room_1:2026-05-19:2:1",
"level": 2,
"status": "settled",
"opened_at_ms": 1779116400000,
"settled_at_ms": 1779116401200,
"my_rewards": []
}
}
```
规则:
- `levels` 固定返回 7 个等级的物料和奖励展示配置。
- `reset_at_ms` 是下一个 UTC 零点,不是用户本地零点。
- `progress_ppm = floor(progress * 1000000 / threshold)`,客户端不自行除浮点。
- 倒计时期间送礼和单笔溢出的无效能量不返回为下一等级进度;客户端只展示服务端返回的当前等级进度。
### Get Treasure Rewards
```http
GET /api/v1/rooms/{room_id}/treasure-chest/rewards?round_id=treasure:...&scope=mine
Authorization: Bearer <access_token>
```
Response `data`:
```json
{
"round_id": "treasure:lalu_room_1:2026-05-19:3:1",
"level": 3,
"status": "settled",
"reward_status": "granted",
"items": [
{
"reward_record_id": "treasure_reward:treasure:lalu_room_1:2026-05-19:3:1:in_room:10001",
"slot": "in_room",
"user_id": "10001",
"resource_group_id": 12001,
"display_name": "Room Reward",
"icon_url": "https://cdn.example/reward.png",
"grant_status": "granted",
"granted_at_ms": 1779120012000
}
],
"next_page_token": ""
}
```
`scope=public` 只返回公开摘要,例如点火人、贡献第一和最多 N 条可展示奖励;不要把所有用户的私派奖励大列表塞进 IM payload。
## IM Contracts
房间内宝箱 IM 仍走腾讯云房间群 `TIMCustomElem`,公共字段保持稳定:
```json
{
"event_id": "room_treasure:...",
"event_type": "room_treasure_progress_changed",
"room_id": "lalu_room_1",
"room_version": 102,
"round_id": "treasure:lalu_room_1:2026-05-19:3:1",
"level": 3,
"cycle_day": "2026-05-19",
"server_time_ms": 1779120000000
}
```
### Progress Changed
`event_type=room_treasure_progress_changed`
```json
{
"sender_user_id": "10001",
"gift_id": "rose",
"gift_count": 10,
"energy_added": 500,
"invalid_energy": 0,
"progress_before": 3100,
"progress_after": 3600,
"threshold": 10000,
"status": "charging",
"top1_user_id": "10001",
"reset_at_ms": 1779148800000
}
```
### Countdown Started
`event_type=room_treasure_countdown_started`
```json
{
"progress_after": 10000,
"threshold": 10000,
"igniter_user_id": "10002",
"top1_user_id": "10001",
"full_at_ms": 1779120000000,
"open_at_ms": 1779120030000,
"broadcast_scope": "region"
}
```
该事件进入 room outbox 后activity-service 按配置生成全局或区域播报:
```json
{
"event_id": "treasure_broadcast:treasure:lalu_room_1:2026-05-19:3:1",
"broadcast_type": "room_treasure_ready",
"scope": "region",
"region_id": 1001,
"room_id": "lalu_room_1",
"round_id": "treasure:lalu_room_1:2026-05-19:3:1",
"level": 3,
"open_at_ms": 1779120030000,
"action": {
"type": "enter_room",
"room_id": "lalu_room_1"
}
}
```
### Chest Opened
`event_type=room_treasure_opened`
```json
{
"opened_at_ms": 1779120030000,
"participant_count": 86,
"igniter_user_id": "10002",
"top1_user_id": "10001",
"settlement_status": "pending"
}
```
### Rewards Settled
`event_type=room_treasure_rewards_settled`
```json
{
"settled_at_ms": 1779120031200,
"reward_status": "granted",
"participant_reward_count": 86,
"top1_user_id": "10001",
"igniter_user_id": "10002",
"public_rewards": [
{
"slot": "top1",
"user_id": "10001",
"display_name": "Top1 Reward",
"icon_url": "https://cdn.example/reward.png"
},
{
"slot": "igniter",
"user_id": "10002",
"display_name": "Igniter Reward",
"icon_url": "https://cdn.example/reward.png"
}
],
"full_result_required": true
}
```
群 IM 只承载公开摘要。用户自己的完整发奖结果通过 `GET /treasure-chest/rewards?scope=mine` 查询;后续接 notice-service 时,可以再补私有单聊或 inbox 通知。
## Admin Config
后台配置必须带 `config_version`,保存后只影响新 round已经进入 `countdown/opening/settling` 的 round 使用锁定的配置快照。
| 字段 | 含义 |
| --- | --- |
| `enabled` | 总开关 |
| `energy_source` | 默认 `gift_point_added` |
| `open_delay_ms` | 满能量到开箱的倒计时 |
| `broadcast_enabled` | 是否满能量后播报 |
| `broadcast_scope` | `none/region/global` |
| `broadcast_delay_ms` | 满能量后多久发播报,默认 0 |
| `reward_stack_policy` | `allow_stack``priority_only` |
| `levels` | 7 个等级配置,包含阈值、物料 URL 和奖励池 |
| `gift_energy_rules` | gift_id 或 gift_type_code 的能量倍率、覆盖值和排除规则 |
| `effective_from_ms` | 配置开始生效时间UTC epoch ms |
| `updated_by_admin_id` | 审计字段 |
每个等级必须配置:
| 字段 | 规则 |
| --- | --- |
| `level` | 1 到 7不能缺级 |
| `threshold` | 大于 0建议单调递增 |
| `cover_url` | 静态封面 |
| `animation_url` | 未开启待机动效 |
| `opening_animation_url` | 开启倒计时或开箱动效 |
| `opened_image_url` | 开启后静态图 |
| `in_room_rewards` | 在房用户奖励池 |
| `top1_rewards` | 贡献第一奖励池 |
| `igniter_rewards` | 点火人奖励池 |
奖励池保存时应校验:
- `resource_group_id` 必须存在且 active。
- 同一槽位权重总和必须大于 0。
- 展示字段可以保存快照,但发放仍以 wallet-service 资源组事实为准。
- 配置不能直接填写金币数量绕过资源组,除非新增明确的钱包奖励配置类型。
## Internal Events
建议在 `api/proto/events/room/v1/events.proto` 增加:
| Event | 生产方 | 消费方 | 用途 |
| --- | --- | --- | --- |
| `RoomTreasureChestProgressChanged` | room-service | Tencent IM/activity audit | 房间内进度 IM、审计 |
| `RoomTreasureChestCountdownStarted` | room-service | Tencent IM/activity broadcast | 房间倒计时 IM、区域/全局播报 |
| `RoomTreasureChestOpened` | room-service | activity-service | 奖励抽奖和发放输入,包含参与人快照和奖励配置快照 |
| `RoomTreasureChestSettlementApplied` | room-service | Tencent IM/audit | 发奖完成后的房间 IM |
`RoomTreasureChestOpened` 必须包含:
```text
round_id
cycle_day
level
config_version
room_id
visible_region_id
opened_at_ms
participant_user_ids
igniter_user_id
top1_user_id
contribution_top
reward_config_snapshot_json
```
activity-service 不反查 Room Cell 判断参与人,避免开奖时与房间状态发生竞态。
## Data Model Sketch
room-service 持久化:
```sql
room_treasure_rounds(
app_code,
room_id,
cycle_day,
round_id,
level,
status,
progress,
threshold,
invalid_energy,
config_version,
igniter_user_id,
top1_user_id,
participant_count,
full_at_ms,
open_at_ms,
opened_at_ms,
settled_at_ms,
reset_at_ms,
created_at_ms,
updated_at_ms
);
room_treasure_contributions(
app_code,
round_id,
user_id,
energy,
first_contributed_at_ms,
last_contributed_at_ms
);
```
activity-service 持久化:
```sql
treasure_chest_settlements(
app_code,
settlement_id,
round_id,
room_id,
level,
status,
reward_config_snapshot_json,
participant_count,
created_at_ms,
updated_at_ms
);
treasure_chest_reward_records(
app_code,
reward_record_id,
settlement_id,
round_id,
slot,
user_id,
reward_item_id,
resource_group_id,
grant_command_id,
grant_status,
grant_id,
retry_count,
last_error,
created_at_ms,
updated_at_ms
);
```
Room Cell snapshot 也要包含当前宝箱轻量状态,保证节点恢复后不依赖 Redis 重建进度。表结构用于查询、due scan 和后台审计command log 仍是写命令恢复的关键事实。
## Daily Reset
| 规则 | 决策 |
| --- | --- |
| 日期来源 | `time.Now().UTC()``cycle_day=YYYY-MM-DD` |
| 重置时间 | 每天 UTC `00:00:00.000` |
| 时间区间 | `[day_start_ms, next_day_start_ms)` |
| charging round | 到达新 UTC 日时清空进度、贡献和 queued energy创建新 day level 1 round |
| countdown/opening/settling round | 已满能量的 round 继续开箱和发奖,使用原 `cycle_day` |
| exhausted | 下一个 UTC 日恢复 level 1 |
必须补 UTC 边界测试:`day_start_ms``next_day_start_ms - 1``next_day_start_ms`。不要重新引入 `time.Local``task_timezone` 或客户端时区。
## Implementation Flow
```mermaid
sequenceDiagram
participant C as Client
participant G as gateway-service
participant R as room-service
participant W as wallet-service
participant A as activity-service
participant IM as Tencent IM
C->>G: POST /api/v1/rooms/gift/send
G->>R: SendGift(command_id)
R->>W: DebitGift(command_id)
W-->>R: gift_point_added, receipt
R->>R: Room Cell updates heat/rank/treasure
R-->>C: SendGiftResponse + treasure summary
R-->>IM: room_treasure_progress_changed
alt energy full
R-->>IM: room_treasure_countdown_started
R-->>A: room outbox RoomTreasureChestCountdownStarted
A-->>IM: region/global room_treasure_ready broadcast
end
R->>R: ProcessDueTreasureChests at open_at_ms
R-->>IM: room_treasure_opened
R-->>A: room outbox RoomTreasureChestOpened
A->>A: draw rewards idempotently
A->>W: GrantResourceGroup per reward
W-->>A: grant result
A->>R: ApplyTreasureChestSettlement(summary)
R-->>IM: room_treasure_rewards_settled
```
`ApplyTreasureChestSettlement` 是 gRPC/protobuf 边界,不允许 activity-service import room-service `internal` 包。它只提交结算摘要和可展示公共结果,不让 activity-service 改写宝箱进度。
## Failure Handling
| 失败 | 处理 |
| --- | --- |
| wallet DebitGift 失败 | 不增加能量,不写宝箱进度,不发宝箱 IM |
| room outbox 投递 IM 失败 | 房间状态已提交outbox 重试或死信App 可用 GET 接口补状态 |
| broadcast 失败 | activity broadcast outbox 重试,不影响开箱 |
| due open worker 宕机 | MySQL `open_at_ms/status` 可被下一轮 worker/cron 重新扫描 |
| activity 抽奖失败 | settlement 保持 `retryable`,不发“已发奖” IM |
| wallet 发奖部分失败 | 成功记录保持 granted失败记录重试房间 IM 标记 `partial` 或等待全部终态后再发 |
| App 错过 IM | `GET /treasure-chest``GET /treasure-chest/rewards` 补齐 |
## Tests
必须覆盖:
| 场景 | 验证 |
| --- | --- |
| 送礼幂等 | 同一 `command_id` 重试不重复加能量 |
| 阈值边界 | `progress = threshold - 1` 不点火,`progress = threshold` 点火一次 |
| 溢出能量 | 单笔礼物超过阈值时只补满当前等级,剩余能量作废且不进入下一等级 |
| 倒计时送礼 | countdown/opening/settling 期间送礼不增加宝箱能量,不改变点火人和 top1 |
| 倒计时开箱 | `open_at_ms` 前不开,等于或超过时只开一次 |
| 在线快照 | 开箱时在线用户获得 in_room 抽奖资格,离房用户不获得 in_room 奖励 |
| top1/igniter | 同分 tie-break 固定;离房、断线或下线后仍结算角色奖励 |
| UTC 重置 | 覆盖 UTC 日开始、结束前一毫秒、结束边界 |
| room close | countdown 后关房不会丢掉已满宝箱奖励 |
| 发奖幂等 | reward_record 重试不重复发资源组 |
| 播报范围 | `none/region/global` 分别不发、发区域、发全局 |
| IM 补偿 | outbox 重试不会产生不同 event_id 的重复客户端消息 |

View File

@ -39,6 +39,7 @@ import (
hostorgmodule "hyapp-admin-server/internal/modules/hostorg"
jobmodule "hyapp-admin-server/internal/modules/job"
levelconfigmodule "hyapp-admin-server/internal/modules/levelconfig"
luckygiftmodule "hyapp-admin-server/internal/modules/luckygift"
menumodule "hyapp-admin-server/internal/modules/menu"
paymentmodule "hyapp-admin-server/internal/modules/payment"
rbacmodule "hyapp-admin-server/internal/modules/rbac"
@ -47,6 +48,7 @@ import (
resourcemodule "hyapp-admin-server/internal/modules/resource"
roomadminmodule "hyapp-admin-server/internal/modules/roomadmin"
searchmodule "hyapp-admin-server/internal/modules/search"
sevendaycheckinmodule "hyapp-admin-server/internal/modules/sevendaycheckin"
uploadmodule "hyapp-admin-server/internal/modules/upload"
"hyapp-admin-server/internal/platform/logging"
"hyapp-admin-server/internal/platform/tencentcos"
@ -222,6 +224,7 @@ func main() {
HostOrg: hostorgmodule.New(userclient.NewGRPC(userConn), walletclient.NewGRPC(walletConn), userDB, walletDB, sqlDB, auditHandler),
Job: jobmodule.New(store, cfg, auditHandler),
LevelConfig: levelconfigmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
LuckyGift: luckygiftmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
Menu: menumodule.New(store, auditHandler),
Payment: paymentmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
RBAC: rbacmodule.New(store, auditHandler),
@ -230,6 +233,7 @@ func main() {
Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, auditHandler),
RoomAdmin: roomadminmodule.New(roomDB, userDB, roomclient.NewGRPC(roomConn), auditHandler),
Search: searchmodule.New(store),
SevenDayCheckIn: sevendaycheckinmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
Upload: uploadmodule.New(objectUploader, cfg.TencentCOS.ObjectPrefix, auditHandler),
}
engine := router.New(cfg, auth, handlers)

View File

@ -18,6 +18,12 @@ type Client interface {
GetRegistrationRewardConfig(ctx context.Context, req *activityv1.GetRegistrationRewardConfigRequest) (*activityv1.GetRegistrationRewardConfigResponse, error)
UpdateRegistrationRewardConfig(ctx context.Context, req *activityv1.UpdateRegistrationRewardConfigRequest) (*activityv1.UpdateRegistrationRewardConfigResponse, error)
ListRegistrationRewardClaims(ctx context.Context, req *activityv1.ListRegistrationRewardClaimsRequest) (*activityv1.ListRegistrationRewardClaimsResponse, error)
GetSevenDayCheckInConfig(ctx context.Context, req *activityv1.GetSevenDayCheckInConfigRequest) (*activityv1.GetSevenDayCheckInConfigResponse, error)
UpdateSevenDayCheckInConfig(ctx context.Context, req *activityv1.UpdateSevenDayCheckInConfigRequest) (*activityv1.UpdateSevenDayCheckInConfigResponse, error)
ListSevenDayCheckInClaims(ctx context.Context, req *activityv1.ListSevenDayCheckInClaimsRequest) (*activityv1.ListSevenDayCheckInClaimsResponse, error)
GetLuckyGiftConfig(ctx context.Context, req *activityv1.GetLuckyGiftConfigRequest) (*activityv1.GetLuckyGiftConfigResponse, error)
UpsertLuckyGiftConfig(ctx context.Context, req *activityv1.UpsertLuckyGiftConfigRequest) (*activityv1.UpsertLuckyGiftConfigResponse, error)
ListLuckyGiftDraws(ctx context.Context, req *activityv1.ListLuckyGiftDrawsRequest) (*activityv1.ListLuckyGiftDrawsResponse, error)
RemoveRegionBroadcastMember(ctx context.Context, req *activityv1.RemoveRegionBroadcastMemberRequest) (*activityv1.RemoveRegionBroadcastMemberResponse, error)
ListLevelConfig(ctx context.Context, req *activityv1.ListLevelConfigRequest) (*activityv1.ListLevelConfigResponse, error)
UpsertLevelTrack(ctx context.Context, req *activityv1.UpsertLevelTrackRequest) (*activityv1.UpsertLevelTrackResponse, error)
@ -29,6 +35,8 @@ type GRPCClient struct {
taskClient activityv1.AdminTaskServiceClient
achievementClient activityv1.AdminAchievementServiceClient
registrationRewardClient activityv1.AdminRegistrationRewardServiceClient
sevenDayCheckInClient activityv1.AdminSevenDayCheckInServiceClient
luckyGiftClient activityv1.AdminLuckyGiftServiceClient
broadcastClient activityv1.BroadcastServiceClient
growthClient activityv1.AdminGrowthLevelServiceClient
}
@ -38,6 +46,8 @@ func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
taskClient: activityv1.NewAdminTaskServiceClient(conn),
achievementClient: activityv1.NewAdminAchievementServiceClient(conn),
registrationRewardClient: activityv1.NewAdminRegistrationRewardServiceClient(conn),
sevenDayCheckInClient: activityv1.NewAdminSevenDayCheckInServiceClient(conn),
luckyGiftClient: activityv1.NewAdminLuckyGiftServiceClient(conn),
broadcastClient: activityv1.NewBroadcastServiceClient(conn),
growthClient: activityv1.NewAdminGrowthLevelServiceClient(conn),
}
@ -75,6 +85,30 @@ func (c *GRPCClient) ListRegistrationRewardClaims(ctx context.Context, req *acti
return c.registrationRewardClient.ListRegistrationRewardClaims(ctx, req)
}
func (c *GRPCClient) GetSevenDayCheckInConfig(ctx context.Context, req *activityv1.GetSevenDayCheckInConfigRequest) (*activityv1.GetSevenDayCheckInConfigResponse, error) {
return c.sevenDayCheckInClient.GetSevenDayCheckInConfig(ctx, req)
}
func (c *GRPCClient) UpdateSevenDayCheckInConfig(ctx context.Context, req *activityv1.UpdateSevenDayCheckInConfigRequest) (*activityv1.UpdateSevenDayCheckInConfigResponse, error) {
return c.sevenDayCheckInClient.UpdateSevenDayCheckInConfig(ctx, req)
}
func (c *GRPCClient) ListSevenDayCheckInClaims(ctx context.Context, req *activityv1.ListSevenDayCheckInClaimsRequest) (*activityv1.ListSevenDayCheckInClaimsResponse, error) {
return c.sevenDayCheckInClient.ListSevenDayCheckInClaims(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) ListLuckyGiftDraws(ctx context.Context, req *activityv1.ListLuckyGiftDrawsRequest) (*activityv1.ListLuckyGiftDrawsResponse, error) {
return c.luckyGiftClient.ListLuckyGiftDraws(ctx, req)
}
func (c *GRPCClient) RemoveRegionBroadcastMember(ctx context.Context, req *activityv1.RemoveRegionBroadcastMemberRequest) (*activityv1.RemoveRegionBroadcastMemberResponse, error) {
return c.broadcastClient.RemoveRegionBroadcastMember(ctx, req)
}

View File

@ -21,9 +21,11 @@ type Client interface {
UpdateResourceGroup(ctx context.Context, req *walletv1.UpdateResourceGroupRequest) (*walletv1.ResourceGroupResponse, error)
SetResourceGroupStatus(ctx context.Context, req *walletv1.SetResourceGroupStatusRequest) (*walletv1.ResourceGroupResponse, error)
ListGiftConfigs(ctx context.Context, req *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error)
ListGiftTypeConfigs(ctx context.Context, req *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error)
CreateGiftConfig(ctx context.Context, req *walletv1.CreateGiftConfigRequest) (*walletv1.GiftConfigResponse, error)
UpdateGiftConfig(ctx context.Context, req *walletv1.UpdateGiftConfigRequest) (*walletv1.GiftConfigResponse, error)
SetGiftConfigStatus(ctx context.Context, req *walletv1.SetGiftConfigStatusRequest) (*walletv1.GiftConfigResponse, error)
UpsertGiftTypeConfig(ctx context.Context, req *walletv1.UpsertGiftTypeConfigRequest) (*walletv1.GiftTypeConfigResponse, error)
GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error)
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error)
ListResourceGrants(ctx context.Context, req *walletv1.ListResourceGrantsRequest) (*walletv1.ListResourceGrantsResponse, error)
@ -88,6 +90,10 @@ func (c *GRPCClient) ListGiftConfigs(ctx context.Context, req *walletv1.ListGift
return c.client.ListGiftConfigs(ctx, req)
}
func (c *GRPCClient) ListGiftTypeConfigs(ctx context.Context, req *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error) {
return c.client.ListGiftTypeConfigs(ctx, req)
}
func (c *GRPCClient) CreateGiftConfig(ctx context.Context, req *walletv1.CreateGiftConfigRequest) (*walletv1.GiftConfigResponse, error) {
return c.client.CreateGiftConfig(ctx, req)
}
@ -100,6 +106,10 @@ func (c *GRPCClient) SetGiftConfigStatus(ctx context.Context, req *walletv1.SetG
return c.client.SetGiftConfigStatus(ctx, req)
}
func (c *GRPCClient) UpsertGiftTypeConfig(ctx context.Context, req *walletv1.UpsertGiftTypeConfigRequest) (*walletv1.GiftTypeConfigResponse, error) {
return c.client.UpsertGiftTypeConfig(ctx, req)
}
func (c *GRPCClient) GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error) {
return c.client.GrantResource(ctx, req)
}

View File

@ -92,19 +92,23 @@ func (AppConfig) TableName() string {
}
type AppBanner struct {
ID uint `gorm:"primaryKey" json:"id"`
AppCode string `gorm:"size:32;index:idx_admin_app_banners_app_sort,not null;default:lalu" json:"appCode"`
CoverURL string `gorm:"size:1024;not null" json:"coverUrl"`
BannerType string `gorm:"size:16;not null" json:"bannerType"`
Param string `gorm:"size:2048" json:"param"`
Status string `gorm:"size:24;index:idx_admin_app_banners_app_sort,not null;default:active" json:"status"`
Platform string `gorm:"size:24;index:idx_admin_app_banners_scope,not null" json:"platform"`
SortOrder int `gorm:"index:idx_admin_app_banners_app_sort,not null;default:0" json:"sortOrder"`
RegionID int64 `gorm:"index:idx_admin_app_banners_scope,not null;default:0" json:"regionId"`
CountryCode string `gorm:"size:8;index:idx_admin_app_banners_scope" json:"countryCode"`
Description string `gorm:"size:255" json:"description"`
CreatedAtMS int64 `gorm:"column:created_at_ms;autoCreateTime:milli" json:"createdAtMs"`
UpdatedAtMS int64 `gorm:"column:updated_at_ms;autoUpdateTime:milli" json:"updatedAtMs"`
ID uint `gorm:"primaryKey" json:"id"`
AppCode string `gorm:"size:32;index:idx_admin_app_banners_app_sort,not null;default:lalu" json:"appCode"`
CoverURL string `gorm:"size:1024;not null" json:"coverUrl"`
RoomSmallImageURL string `gorm:"column:room_small_image_url;size:1024;not null;default:'';comment:房间内小屏图 URL" json:"roomSmallImageUrl"`
BannerType string `gorm:"size:16;not null" json:"bannerType"`
DisplayScope string `gorm:"size:32;index:idx_admin_app_banners_display,not null;default:home;comment:显示范围:首页、房间内或充值页" json:"displayScope"`
Param string `gorm:"size:2048" json:"param"`
Status string `gorm:"size:24;index:idx_admin_app_banners_app_sort,not null;default:active" json:"status"`
Platform string `gorm:"size:24;index:idx_admin_app_banners_scope,not null" json:"platform"`
SortOrder int `gorm:"index:idx_admin_app_banners_app_sort,not null;default:0" json:"sortOrder"`
RegionID int64 `gorm:"index:idx_admin_app_banners_scope,not null;default:0" json:"regionId"`
CountryCode string `gorm:"size:8;index:idx_admin_app_banners_scope" json:"countryCode"`
Description string `gorm:"size:255" json:"description"`
StartsAtMS int64 `gorm:"column:starts_at_ms;not null;default:0;comment:投放开始时间UTC epoch ms" json:"startsAtMs"`
EndsAtMS int64 `gorm:"column:ends_at_ms;not null;default:0;comment:投放结束时间UTC epoch ms" json:"endsAtMs"`
CreatedAtMS int64 `gorm:"column:created_at_ms;autoCreateTime:milli" json:"createdAtMs"`
UpdatedAtMS int64 `gorm:"column:updated_at_ms;autoUpdateTime:milli" json:"updatedAtMs"`
}
func (AppBanner) TableName() string {

View File

@ -50,11 +50,12 @@ func (h *Handler) UpdateH5Links(c *gin.Context) {
func (h *Handler) ListBanners(c *gin.Context) {
options := shared.ListOptions(c)
items, err := h.service.ListBanners(appctx.FromContext(c.Request.Context()), repository.AppBannerListOptions{
Keyword: options.Keyword,
Status: options.Status,
Platform: strings.TrimSpace(c.Query("platform")),
RegionID: parseOptionalInt64(c.Query("regionId"), c.Query("region_id")),
Country: firstQuery(c, "countryCode", "country_code", "country"),
Keyword: options.Keyword,
Status: options.Status,
DisplayScope: firstQuery(c, "displayScope", "display_scope"),
Platform: strings.TrimSpace(c.Query("platform")),
RegionID: parseOptionalInt64(c.Query("regionId"), c.Query("region_id")),
Country: firstQuery(c, "countryCode", "country_code", "country"),
})
if err != nil {
response.ServerError(c, "获取 BANNER 配置失败")

View File

@ -10,15 +10,19 @@ type h5LinkPayload struct {
}
type bannerRequest struct {
CoverURL string `json:"coverUrl" binding:"required"`
BannerType string `json:"bannerType" binding:"required"`
Param string `json:"param"`
Status string `json:"status" binding:"required"`
Platform string `json:"platform" binding:"required"`
SortOrder int `json:"sortOrder"`
RegionID int64 `json:"regionId"`
CountryCode string `json:"countryCode"`
Description string `json:"description"`
CoverURL string `json:"coverUrl" binding:"required"`
RoomSmallImageURL string `json:"roomSmallImageUrl"`
BannerType string `json:"bannerType" binding:"required"`
DisplayScope string `json:"displayScope"`
Param string `json:"param"`
Status string `json:"status" binding:"required"`
Platform string `json:"platform" binding:"required"`
SortOrder int `json:"sortOrder"`
RegionID int64 `json:"regionId"`
CountryCode string `json:"countryCode"`
Description string `json:"description"`
StartsAtMs int64 `json:"startsAtMs"`
EndsAtMs int64 `json:"endsAtMs"`
}
type appVersionRequest struct {

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"unicode"
"unicode/utf8"
@ -14,6 +15,16 @@ import (
const h5LinkGroup = "h5-links"
const (
bannerDisplayScopeHome = "home"
bannerDisplayScopeRoom = "room"
bannerDisplayScopeRecharge = "recharge"
bannerStatusActive = "active"
bannerStatusDisabled = "disabled"
bannerStatusExpired = "expired"
)
var h5LinkDefinitions = []H5LinkDefinition{
{Key: "host-center", Label: "Host Center"},
{Key: "bd-center", Label: "BD Center"},
@ -39,19 +50,23 @@ type H5Link struct {
}
type AppBanner struct {
ID uint `json:"id"`
AppCode string `json:"appCode"`
CoverURL string `json:"coverUrl"`
BannerType string `json:"bannerType"`
Param string `json:"param"`
Status string `json:"status"`
Platform string `json:"platform"`
SortOrder int `json:"sortOrder"`
RegionID int64 `json:"regionId"`
CountryCode string `json:"countryCode"`
Description string `json:"description"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
ID uint `json:"id"`
AppCode string `json:"appCode"`
CoverURL string `json:"coverUrl"`
RoomSmallImageURL string `json:"roomSmallImageUrl"`
BannerType string `json:"bannerType"`
DisplayScope string `json:"displayScope"`
Param string `json:"param"`
Status string `json:"status"`
Platform string `json:"platform"`
SortOrder int `json:"sortOrder"`
RegionID int64 `json:"regionId"`
CountryCode string `json:"countryCode"`
Description string `json:"description"`
StartsAtMs int64 `json:"startsAtMs"`
EndsAtMs int64 `json:"endsAtMs"`
CreatedAtMs int64 `json:"createdAtMs"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
type AppVersion struct {
@ -136,9 +151,15 @@ func (s *AppConfigService) UpdateH5Links(request updateH5LinksRequest) ([]H5Link
func (s *AppConfigService) ListBanners(appCode string, options repository.AppBannerListOptions) ([]AppBanner, error) {
options.AppCode = appctx.Normalize(appCode)
options.Status = normalizeBannerStatus(options.Status)
if strings.TrimSpace(options.DisplayScope) != "" {
options.DisplayScope = normalizeBannerDisplayScope(options.DisplayScope)
}
options.Platform = normalizeBannerPlatform(options.Platform)
options.Country = normalizeCountryCode(options.Country)
options.Keyword = strings.TrimSpace(options.Keyword)
if err := s.store.ExpireAppBanners(options.AppCode, time.Now().UTC().UnixMilli()); err != nil {
return nil, err
}
items, err := s.store.ListAppBanners(options)
if err != nil {
return nil, err
@ -171,7 +192,9 @@ func (s *AppConfigService) UpdateBanner(appCode string, id uint, req bannerReque
return AppBanner{}, err
}
item.CoverURL = updated.CoverURL
item.RoomSmallImageURL = updated.RoomSmallImageURL
item.BannerType = updated.BannerType
item.DisplayScope = updated.DisplayScope
item.Param = updated.Param
item.Status = updated.Status
item.Platform = updated.Platform
@ -179,6 +202,8 @@ func (s *AppConfigService) UpdateBanner(appCode string, id uint, req bannerReque
item.RegionID = updated.RegionID
item.CountryCode = updated.CountryCode
item.Description = updated.Description
item.StartsAtMS = updated.StartsAtMS
item.EndsAtMS = updated.EndsAtMS
if err := s.store.UpdateAppBanner(&item); err != nil {
return AppBanner{}, err
}
@ -265,20 +290,41 @@ func validateH5URL(value string) error {
func bannerModelFromRequest(appCode string, req bannerRequest) (model.AppBanner, error) {
item := model.AppBanner{
AppCode: appctx.Normalize(appCode),
CoverURL: strings.TrimSpace(req.CoverURL),
BannerType: normalizeBannerType(req.BannerType),
Param: strings.TrimSpace(req.Param),
Status: normalizeBannerStatus(req.Status),
Platform: normalizeBannerPlatform(req.Platform),
SortOrder: req.SortOrder,
RegionID: req.RegionID,
CountryCode: normalizeCountryCode(req.CountryCode),
Description: strings.TrimSpace(req.Description),
AppCode: appctx.Normalize(appCode),
CoverURL: strings.TrimSpace(req.CoverURL),
RoomSmallImageURL: strings.TrimSpace(req.RoomSmallImageURL),
BannerType: normalizeBannerType(req.BannerType),
DisplayScope: normalizeBannerDisplayScope(req.DisplayScope),
Param: strings.TrimSpace(req.Param),
Status: normalizeBannerStatus(req.Status),
Platform: normalizeBannerPlatform(req.Platform),
SortOrder: req.SortOrder,
RegionID: req.RegionID,
CountryCode: normalizeCountryCode(req.CountryCode),
Description: strings.TrimSpace(req.Description),
StartsAtMS: req.StartsAtMs,
EndsAtMS: req.EndsAtMs,
}
if item.CoverURL == "" || len(item.CoverURL) > 1024 {
return model.AppBanner{}, errors.New("banner cover is invalid")
}
if containsWhitespace(item.CoverURL) {
return model.AppBanner{}, errors.New("banner cover cannot contain whitespace")
}
if item.DisplayScope != bannerDisplayScopeHome && item.DisplayScope != bannerDisplayScopeRoom && item.DisplayScope != bannerDisplayScopeRecharge {
return model.AppBanner{}, errors.New("banner display scope is invalid")
}
if item.DisplayScope == bannerDisplayScopeRoom {
if item.RoomSmallImageURL == "" || len(item.RoomSmallImageURL) > 1024 {
return model.AppBanner{}, errors.New("room small image is required")
}
if containsWhitespace(item.RoomSmallImageURL) {
return model.AppBanner{}, errors.New("room small image cannot contain whitespace")
}
} else {
// 非房间内投放不保留房间小屏图,避免客户端误用其它显示范围的专属素材。
item.RoomSmallImageURL = ""
}
if item.BannerType != "h5" && item.BannerType != "app" {
return model.AppBanner{}, errors.New("banner type is invalid")
}
@ -293,9 +339,18 @@ func bannerModelFromRequest(appCode string, req bannerRequest) (model.AppBanner,
if len(item.Param) > 2048 {
return model.AppBanner{}, errors.New("banner param is too long")
}
if item.Status != "active" && item.Status != "disabled" {
if item.Status != bannerStatusActive && item.Status != bannerStatusDisabled && item.Status != bannerStatusExpired {
return model.AppBanner{}, errors.New("banner status is invalid")
}
if item.StartsAtMS < 0 || item.EndsAtMS < 0 {
return model.AppBanner{}, errors.New("banner delivery time is invalid")
}
if item.StartsAtMS > 0 && item.EndsAtMS > 0 && item.StartsAtMS >= item.EndsAtMS {
return model.AppBanner{}, errors.New("banner delivery time range is invalid")
}
if item.Status == bannerStatusActive && item.EndsAtMS > 0 && item.EndsAtMS <= time.Now().UTC().UnixMilli() {
item.Status = bannerStatusExpired
}
if item.Platform != "android" && item.Platform != "ios" {
return model.AppBanner{}, errors.New("banner platform is invalid")
}
@ -313,19 +368,23 @@ func bannerModelFromRequest(appCode string, req bannerRequest) (model.AppBanner,
func appBannerFromModel(item model.AppBanner) AppBanner {
return AppBanner{
ID: item.ID,
AppCode: item.AppCode,
CoverURL: item.CoverURL,
BannerType: item.BannerType,
Param: item.Param,
Status: item.Status,
Platform: item.Platform,
SortOrder: item.SortOrder,
RegionID: item.RegionID,
CountryCode: item.CountryCode,
Description: item.Description,
CreatedAtMs: item.CreatedAtMS,
UpdatedAtMs: item.UpdatedAtMS,
ID: item.ID,
AppCode: item.AppCode,
CoverURL: item.CoverURL,
RoomSmallImageURL: item.RoomSmallImageURL,
BannerType: item.BannerType,
DisplayScope: item.DisplayScope,
Param: item.Param,
Status: item.Status,
Platform: item.Platform,
SortOrder: item.SortOrder,
RegionID: item.RegionID,
CountryCode: item.CountryCode,
Description: item.Description,
StartsAtMs: item.StartsAtMS,
EndsAtMs: item.EndsAtMS,
CreatedAtMs: item.CreatedAtMS,
UpdatedAtMs: item.UpdatedAtMS,
}
}
@ -384,11 +443,27 @@ func normalizeBannerType(value string) string {
func normalizeBannerStatus(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" {
return "active"
return bannerStatusActive
}
return value
}
func normalizeBannerDisplayScope(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
switch value {
case "":
return bannerDisplayScopeHome
case bannerDisplayScopeHome, "首页":
return bannerDisplayScopeHome
case bannerDisplayScopeRoom, "房间内":
return bannerDisplayScopeRoom
case bannerDisplayScopeRecharge, "充值页":
return bannerDisplayScopeRecharge
default:
return value
}
}
func normalizeBannerPlatform(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
@ -401,6 +476,15 @@ func normalizeCountryCode(value string) string {
return strings.ToUpper(strings.TrimSpace(value))
}
func containsWhitespace(value string) bool {
for _, char := range value {
if unicode.IsSpace(char) {
return true
}
}
return false
}
func validCountryCode(value string) bool {
if len(value) < 2 || len(value) > 3 {
return false

View File

@ -0,0 +1,67 @@
package appconfig
import (
"testing"
"time"
)
func TestBannerModelFromRequestRequiresRoomSmallImageForRoomScope(t *testing.T) {
_, err := bannerModelFromRequest("lalu", bannerRequest{
CoverURL: "https://cdn.example.com/banner.png",
BannerType: "h5",
DisplayScope: bannerDisplayScopeRoom,
Param: "https://h5.example.com/activity",
Status: bannerStatusActive,
Platform: "android",
EndsAtMs: time.Now().UTC().Add(time.Hour).UnixMilli(),
})
if err == nil {
t.Fatal("expected room scope without room small image to fail")
}
}
func TestBannerModelFromRequestKeepsRoomScopeScheduleFields(t *testing.T) {
startsAtMs := time.Now().UTC().Add(time.Hour).UnixMilli()
endsAtMs := startsAtMs + int64(time.Hour/time.Millisecond)
item, err := bannerModelFromRequest("lalu", bannerRequest{
CoverURL: "https://cdn.example.com/banner.png",
RoomSmallImageURL: "https://cdn.example.com/banner-small.png",
BannerType: "h5",
DisplayScope: bannerDisplayScopeRoom,
Param: "https://h5.example.com/activity",
Status: bannerStatusActive,
Platform: "android",
StartsAtMs: startsAtMs,
EndsAtMs: endsAtMs,
})
if err != nil {
t.Fatalf("banner model should be valid: %v", err)
}
if item.DisplayScope != bannerDisplayScopeRoom || item.RoomSmallImageURL == "" {
t.Fatalf("room display fields mismatch: %+v", item)
}
if item.StartsAtMS != startsAtMs || item.EndsAtMS != endsAtMs || item.Status != bannerStatusActive {
t.Fatalf("schedule fields mismatch: %+v", item)
}
}
func TestBannerModelFromRequestExpiresEndedActiveBanner(t *testing.T) {
item, err := bannerModelFromRequest("lalu", bannerRequest{
CoverURL: "https://cdn.example.com/banner.png",
RoomSmallImageURL: "https://cdn.example.com/ignored.png",
BannerType: "app",
DisplayScope: bannerDisplayScopeHome,
Status: bannerStatusActive,
Platform: "ios",
EndsAtMs: 1,
})
if err != nil {
t.Fatalf("banner model should accept ended active banner: %v", err)
}
if item.Status != bannerStatusExpired {
t.Fatalf("ended active banner must become expired: %+v", item)
}
if item.RoomSmallImageURL != "" {
t.Fatalf("non-room banner must not retain room small image: %+v", item)
}
}

View File

@ -0,0 +1,311 @@
package luckygift
import (
"strings"
"time"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/activityclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
activityv1 "hyapp.local/api/proto/activity/v1"
"github.com/gin-gonic/gin"
)
type Handler struct {
activity activityclient.Client
audit shared.OperationLogger
}
func New(activity activityclient.Client, audit shared.OperationLogger) *Handler {
return &Handler{activity: activity, audit: audit}
}
type configRequest struct {
Enabled bool `json:"enabled"`
GiftPrice int64 `json:"gift_price"`
TargetRTPPPM int64 `json:"target_rtp_ppm"`
PoolRatePPM int64 `json:"pool_rate_ppm"`
GlobalWindowDraws int64 `json:"global_window_draws"`
GiftWindowDraws int64 `json:"gift_window_draws"`
NoviceDrawLimit int64 `json:"novice_draw_limit"`
IntermediateDrawLimit int64 `json:"intermediate_draw_limit"`
HighMultiplier int64 `json:"high_multiplier"`
HighWaterPoolMultiple int64 `json:"high_water_pool_multiple"`
PlatformPoolWeightPPM int64 `json:"platform_pool_weight_ppm"`
RoomPoolWeightPPM int64 `json:"room_pool_weight_ppm"`
GiftPoolWeightPPM int64 `json:"gift_pool_weight_ppm"`
InitialPlatformPool int64 `json:"initial_platform_pool"`
InitialGiftPool int64 `json:"initial_gift_pool"`
InitialRoomPool int64 `json:"initial_room_pool"`
PlatformReserve int64 `json:"platform_reserve"`
GiftReserve int64 `json:"gift_reserve"`
RoomReserve int64 `json:"room_reserve"`
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"`
RoomAtmosphereRatePPM int64 `json:"room_atmosphere_rate_ppm"`
RoomAtmosphereInitial int64 `json:"room_atmosphere_initial"`
RoomAtmosphereReserve int64 `json:"room_atmosphere_reserve"`
ActivityBudget int64 `json:"activity_budget"`
ActivityDailyLimit int64 `json:"activity_daily_limit"`
LargeTierEnabled bool `json:"large_tier_enabled"`
Tiers []tierDTO `json:"tiers"`
}
type configDTO struct {
configRequest
AppCode string `json:"app_code"`
GiftID string `json:"gift_id"`
RuleVersion int64 `json:"rule_version"`
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type tierDTO struct {
Pool string `json:"pool"`
TierID string `json:"tier_id"`
RewardCoins int64 `json:"reward_coins"`
Weight int64 `json:"weight"`
HighWaterOnly bool `json:"high_water_only"`
Enabled bool `json:"enabled"`
}
type drawDTO struct {
DrawID string `json:"draw_id"`
CommandID string `json:"command_id"`
GiftID string `json:"gift_id"`
RuleVersion int64 `json:"rule_version"`
ExperiencePool string `json:"experience_pool"`
SelectedTierID string `json:"selected_tier_id"`
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"`
}
func (h *Handler) GetLuckyGiftConfig(c *gin.Context) {
resp, err := h.activity.GetLuckyGiftConfig(c.Request.Context(), &activityv1.GetLuckyGiftConfigRequest{
Meta: h.meta(c),
})
if err != nil {
response.ServerError(c, "获取幸运礼物配置失败")
return
}
response.OK(c, configFromProto(resp.GetConfig()))
}
func (h *Handler) UpsertLuckyGiftConfig(c *gin.Context) {
var req configRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "幸运礼物配置参数不正确")
return
}
resp, err := h.activity.UpsertLuckyGiftConfig(c.Request.Context(), &activityv1.UpsertLuckyGiftConfigRequest{
Meta: h.meta(c),
Config: configToProto(req),
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := configFromProto(resp.GetConfig())
shared.OperationLogWithResourceID(c, h.audit, "upsert-lucky-gift-config", "lucky_gift_rules", "global", "success", "")
response.OK(c, item)
}
func (h *Handler) ListLuckyGiftDraws(c *gin.Context) {
options := shared.ListOptions(c)
resp, err := h.activity.ListLuckyGiftDraws(c.Request.Context(), &activityv1.ListLuckyGiftDrawsRequest{
Meta: h.meta(c),
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),
})
if err != nil {
response.ServerError(c, "获取幸运礼物抽奖记录失败")
return
}
items := make([]drawDTO, 0, len(resp.GetDraws()))
for _, draw := range resp.GetDraws() {
items = append(items, drawFromProto(draw))
}
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
return &activityv1.RequestMeta{
RequestId: middleware.CurrentRequestID(c),
Caller: "admin-server",
AppCode: appctx.FromContext(c.Request.Context()),
SentAtMs: time.Now().UTC().UnixMilli(),
}
}
func configToProto(req configRequest) *activityv1.LuckyGiftConfig {
tiers := make([]*activityv1.LuckyGiftTier, 0, len(req.Tiers))
for _, tier := range req.Tiers {
tiers = append(tiers, &activityv1.LuckyGiftTier{
Pool: tier.Pool,
TierId: tier.TierID,
RewardCoins: tier.RewardCoins,
Weight: tier.Weight,
HighWaterOnly: tier.HighWaterOnly,
Enabled: tier.Enabled,
})
}
return &activityv1.LuckyGiftConfig{
Enabled: req.Enabled,
GiftPrice: req.GiftPrice,
TargetRtpPpm: req.TargetRTPPPM,
PoolRatePpm: req.PoolRatePPM,
GlobalWindowDraws: req.GlobalWindowDraws,
GiftWindowDraws: req.GiftWindowDraws,
NoviceDrawLimit: req.NoviceDrawLimit,
IntermediateDrawLimit: req.IntermediateDrawLimit,
HighMultiplier: req.HighMultiplier,
HighWaterPoolMultiple: req.HighWaterPoolMultiple,
PlatformPoolWeightPpm: req.PlatformPoolWeightPPM,
RoomPoolWeightPpm: req.RoomPoolWeightPPM,
GiftPoolWeightPpm: req.GiftPoolWeightPPM,
InitialPlatformPool: req.InitialPlatformPool,
InitialGiftPool: req.InitialGiftPool,
InitialRoomPool: req.InitialRoomPool,
PlatformReserve: req.PlatformReserve,
GiftReserve: req.GiftReserve,
RoomReserve: req.RoomReserve,
MaxSinglePayout: req.MaxSinglePayout,
UserHourlyPayoutCap: req.UserHourlyPayoutCap,
UserDailyPayoutCap: req.UserDailyPayoutCap,
DeviceDailyPayoutCap: req.DeviceDailyPayoutCap,
RoomHourlyPayoutCap: req.RoomHourlyPayoutCap,
AnchorDailyPayoutCap: req.AnchorDailyPayoutCap,
RoomAtmosphereRatePpm: req.RoomAtmosphereRatePPM,
RoomAtmosphereInitial: req.RoomAtmosphereInitial,
RoomAtmosphereReserve: req.RoomAtmosphereReserve,
ActivityBudget: req.ActivityBudget,
ActivityDailyLimit: req.ActivityDailyLimit,
LargeTierEnabled: req.LargeTierEnabled,
Tiers: tiers,
}
}
func configFromProto(config *activityv1.LuckyGiftConfig) configDTO {
if config == nil {
return configDTO{}
}
tiers := make([]tierDTO, 0, len(config.GetTiers()))
for _, tier := range config.GetTiers() {
tiers = append(tiers, tierDTO{
Pool: tier.GetPool(),
TierID: tier.GetTierId(),
RewardCoins: tier.GetRewardCoins(),
Weight: tier.GetWeight(),
HighWaterOnly: tier.GetHighWaterOnly(),
Enabled: tier.GetEnabled(),
})
}
return configDTO{
AppCode: config.GetAppCode(),
GiftID: config.GetGiftId(),
RuleVersion: config.GetRuleVersion(),
UpdatedByAdminID: config.GetUpdatedByAdminId(),
CreatedAtMS: config.GetCreatedAtMs(),
UpdatedAtMS: config.GetUpdatedAtMs(),
configRequest: configRequest{
Enabled: config.GetEnabled(),
GiftPrice: config.GetGiftPrice(),
TargetRTPPPM: config.GetTargetRtpPpm(),
PoolRatePPM: config.GetPoolRatePpm(),
GlobalWindowDraws: config.GetGlobalWindowDraws(),
GiftWindowDraws: config.GetGiftWindowDraws(),
NoviceDrawLimit: config.GetNoviceDrawLimit(),
IntermediateDrawLimit: config.GetIntermediateDrawLimit(),
HighMultiplier: config.GetHighMultiplier(),
HighWaterPoolMultiple: config.GetHighWaterPoolMultiple(),
PlatformPoolWeightPPM: config.GetPlatformPoolWeightPpm(),
RoomPoolWeightPPM: config.GetRoomPoolWeightPpm(),
GiftPoolWeightPPM: config.GetGiftPoolWeightPpm(),
InitialPlatformPool: config.GetInitialPlatformPool(),
InitialGiftPool: config.GetInitialGiftPool(),
InitialRoomPool: config.GetInitialRoomPool(),
PlatformReserve: config.GetPlatformReserve(),
GiftReserve: config.GetGiftReserve(),
RoomReserve: config.GetRoomReserve(),
MaxSinglePayout: config.GetMaxSinglePayout(),
UserHourlyPayoutCap: config.GetUserHourlyPayoutCap(),
UserDailyPayoutCap: config.GetUserDailyPayoutCap(),
DeviceDailyPayoutCap: config.GetDeviceDailyPayoutCap(),
RoomHourlyPayoutCap: config.GetRoomHourlyPayoutCap(),
AnchorDailyPayoutCap: config.GetAnchorDailyPayoutCap(),
RoomAtmosphereRatePPM: config.GetRoomAtmosphereRatePpm(),
RoomAtmosphereInitial: config.GetRoomAtmosphereInitial(),
RoomAtmosphereReserve: config.GetRoomAtmosphereReserve(),
ActivityBudget: config.GetActivityBudget(),
ActivityDailyLimit: config.GetActivityDailyLimit(),
LargeTierEnabled: config.GetLargeTierEnabled(),
Tiers: tiers,
},
}
}
func drawFromProto(draw *activityv1.LuckyGiftDrawResult) drawDTO {
if draw == nil {
return drawDTO{}
}
return drawDTO{
DrawID: draw.GetDrawId(),
CommandID: draw.GetCommandId(),
GiftID: draw.GetGiftId(),
RuleVersion: draw.GetRuleVersion(),
ExperiencePool: draw.GetExperiencePool(),
SelectedTierID: draw.GetSelectedTierId(),
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(),
}
}
func parseOptionalInt64(value string) int64 {
value = strings.TrimSpace(value)
if value == "" {
return 0
}
var result int64
for _, char := range value {
if char < '0' || char > '9' {
return 0
}
result = result*10 + int64(char-'0')
}
return result
}

View File

@ -0,0 +1,17 @@
package luckygift
import (
"hyapp-admin-server/internal/middleware"
"github.com/gin-gonic/gin"
)
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
if h == nil {
return
}
protected.GET("/admin/activity/lucky-gifts/config", middleware.RequirePermission("lucky-gift:view"), h.GetLuckyGiftConfig)
protected.PUT("/admin/activity/lucky-gifts/config", middleware.RequirePermission("lucky-gift:update"), h.UpsertLuckyGiftConfig)
protected.GET("/admin/activity/lucky-gifts/draws", middleware.RequirePermission("lucky-gift:view"), h.ListLuckyGiftDraws)
}

View File

@ -80,6 +80,19 @@ type giftDTO struct {
RegionIDs []int64 `json:"regionIds"`
}
type giftTypeDTO struct {
AppCode string `json:"appCode"`
TabKey string `json:"tabKey"`
DisplayName string `json:"displayName"`
TabName string `json:"tabName"`
Status string `json:"status"`
SortOrder int32 `json:"sortOrder"`
CreatedByUserID int64 `json:"createdByUserId"`
UpdatedByUserID int64 `json:"updatedByUserId"`
CreatedAtMS int64 `json:"createdAtMs"`
UpdatedAtMS int64 `json:"updatedAtMs"`
}
type emojiPackDTO struct {
AppCode string `json:"appCode"`
ResourceID int64 `json:"resourceId"`
@ -246,6 +259,24 @@ func giftFromProto(gift *walletv1.GiftConfig) giftDTO {
}
}
func giftTypeFromProto(item *walletv1.GiftTypeConfig) giftTypeDTO {
if item == nil {
return giftTypeDTO{}
}
return giftTypeDTO{
AppCode: item.GetAppCode(),
TabKey: item.GetTypeCode(),
DisplayName: item.GetName(),
TabName: item.GetTabKey(),
Status: item.GetStatus(),
SortOrder: item.GetSortOrder(),
CreatedByUserID: item.GetCreatedByUserId(),
UpdatedByUserID: item.GetUpdatedByUserId(),
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
}
}
func emojiPackFromProto(item *walletv1.Resource) emojiPackDTO {
if item == nil {
return emojiPackDTO{}

View File

@ -298,6 +298,43 @@ func (h *Handler) ListGifts(c *gin.Context) {
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}
func (h *Handler) ListGiftTypes(c *gin.Context) {
resp, err := h.wallet.ListGiftTypeConfigs(c.Request.Context(), &walletv1.ListGiftTypeConfigsRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
})
if err != nil {
response.ServerError(c, "获取礼物类型失败")
return
}
items := make([]giftTypeDTO, 0, len(resp.GetGiftTypes()))
for _, item := range resp.GetGiftTypes() {
items = append(items, giftTypeFromProto(item))
}
response.OK(c, items)
}
func (h *Handler) UpdateGiftType(c *gin.Context) {
typeCode := strings.TrimSpace(c.Param("type_code"))
if typeCode == "" {
response.BadRequest(c, "类型编码不正确")
return
}
var req giftTypeRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "礼物类型参数不正确")
return
}
resp, err := h.wallet.UpsertGiftTypeConfig(c.Request.Context(), req.upsertProto(c, typeCode))
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := giftTypeFromProto(resp.GetGiftType())
h.auditLog(c, "update-gift-type", "gift_type_configs", item.TabKey, "success", item.TabName)
response.OK(c, item)
}
func (h *Handler) CreateGift(c *gin.Context) {
var req giftRequest
if err := c.ShouldBindJSON(&req); err != nil {

View File

@ -95,6 +95,13 @@ type giftRequest struct {
RegionIDs []int64 `json:"regionIds"`
}
type giftTypeRequest struct {
DisplayName string `json:"displayName"`
TabName string `json:"tabName"`
Status string `json:"status"`
SortOrder int32 `json:"sortOrder"`
}
type grantResourceRequest struct {
CommandID string `json:"commandId"`
TargetUserID int64 `json:"targetUserId"`
@ -276,6 +283,19 @@ func (r giftRequest) updateProto(c *gin.Context, giftID string) *walletv1.Update
}
}
func (r giftTypeRequest) upsertProto(c *gin.Context, typeCode string) *walletv1.UpsertGiftTypeConfigRequest {
return &walletv1.UpsertGiftTypeConfigRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
TypeCode: strings.TrimSpace(typeCode),
Name: strings.TrimSpace(r.DisplayName),
TabKey: strings.TrimSpace(r.TabName),
Status: strings.TrimSpace(r.Status),
SortOrder: r.SortOrder,
OperatorUserId: actorID(c),
}
}
func managerGrantEnabledOrDefault(value *bool) *bool {
enabled := true
if value != nil {

View File

@ -31,6 +31,8 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.POST("/admin/resource-groups/:group_id/disable", middleware.RequirePermission("resource-group:update"), h.DisableResourceGroup)
protected.GET("/admin/gifts", middleware.RequirePermission("gift:view"), h.ListGifts)
protected.GET("/admin/gift-types", middleware.RequirePermission("gift:view"), h.ListGiftTypes)
protected.PUT("/admin/gift-types/:type_code", middleware.RequirePermission("gift:update"), h.UpdateGiftType)
protected.POST("/admin/gifts", middleware.RequirePermission("gift:create"), h.CreateGift)
protected.PUT("/admin/gifts/:gift_id", middleware.RequirePermission("gift:update"), h.UpdateGift)
protected.POST("/admin/gifts/:gift_id/enable", middleware.RequirePermission("gift:status"), h.EnableGift)

View File

@ -0,0 +1,347 @@
package sevendaycheckin
import (
"context"
"database/sql"
"strings"
"time"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/activityclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
activityv1 "hyapp.local/api/proto/activity/v1"
"github.com/gin-gonic/gin"
)
type Handler struct {
activity activityclient.Client
userDB *sql.DB
audit shared.OperationLogger
}
func New(activity activityclient.Client, userDB *sql.DB, audit shared.OperationLogger) *Handler {
return &Handler{activity: activity, userDB: userDB, audit: audit}
}
type configRequest struct {
Enabled bool `json:"enabled"`
Rewards []rewardDTO `json:"rewards"`
}
type configDTO struct {
AppCode string `json:"app_code"`
Enabled bool `json:"enabled"`
ConfigVersion int64 `json:"config_version"`
Rewards []rewardDTO `json:"rewards"`
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type rewardDTO struct {
DayIndex int32 `json:"day_index"`
ResourceGroupID int64 `json:"resource_group_id"`
}
type claimDTO struct {
ClaimID string `json:"claim_id"`
CommandID string `json:"command_id"`
UserID int64 `json:"user_id"`
User *userDTO `json:"user,omitempty"`
CycleNo int64 `json:"cycle_no"`
CheckinDay string `json:"checkin_day"`
DayIndex int32 `json:"day_index"`
ConfigVersion int64 `json:"config_version"`
ResourceGroupID int64 `json:"resource_group_id"`
WalletCommandID string `json:"wallet_command_id"`
WalletGrantID string `json:"wallet_grant_id"`
Status string `json:"status"`
FailureReason string `json:"failure_reason"`
SignedAtMS int64 `json:"signed_at_ms"`
GrantedAtMS int64 `json:"granted_at_ms"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type userDTO struct {
UserID int64 `json:"user_id"`
DisplayUserID string `json:"display_user_id"`
Username string `json:"username"`
Avatar string `json:"avatar"`
}
func (h *Handler) GetSevenDayCheckInConfig(c *gin.Context) {
resp, err := h.activity.GetSevenDayCheckInConfig(c.Request.Context(), &activityv1.GetSevenDayCheckInConfigRequest{Meta: h.meta(c)})
if err != nil {
response.ServerError(c, "获取七日签到配置失败")
return
}
response.OK(c, configFromProto(resp.GetConfig()))
}
func (h *Handler) UpdateSevenDayCheckInConfig(c *gin.Context) {
var req configRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "七日签到配置参数不正确")
return
}
rewards := make([]*activityv1.SevenDayCheckInReward, 0, len(req.Rewards))
for _, reward := range req.Rewards {
rewards = append(rewards, &activityv1.SevenDayCheckInReward{
DayIndex: reward.DayIndex,
ResourceGroupId: reward.ResourceGroupID,
})
}
resp, err := h.activity.UpdateSevenDayCheckInConfig(c.Request.Context(), &activityv1.UpdateSevenDayCheckInConfigRequest{
Meta: h.meta(c),
Enabled: req.Enabled,
Rewards: rewards,
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := configFromProto(resp.GetConfig())
shared.OperationLogWithResourceID(c, h.audit, "update-seven-day-checkin", "seven_day_checkin_configs", item.AppCode, "success", "")
response.OK(c, item)
}
func (h *Handler) ListSevenDayCheckInClaims(c *gin.Context) {
options := shared.ListOptions(c)
userID, matched, ok := h.resolveClaimUserID(c.Request.Context(), strings.TrimSpace(options.Keyword))
if !ok {
response.ServerError(c, "查询用户信息失败")
return
}
if options.Keyword != "" && !matched {
response.OK(c, response.Page{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
return
}
dayIndex := parseOptionalInt(c.Query("day_index"))
if dayIndex < 0 || dayIndex > 7 {
response.BadRequest(c, "签到天数参数不正确")
return
}
resp, err := h.activity.ListSevenDayCheckInClaims(c.Request.Context(), &activityv1.ListSevenDayCheckInClaimsRequest{
Meta: h.meta(c),
Status: strings.TrimSpace(options.Status),
UserId: userID,
CheckinDay: strings.TrimSpace(firstQuery(c, "checkin_day", "checkinDay")),
DayIndex: int32(dayIndex),
Page: int32(options.Page),
PageSize: int32(options.PageSize),
})
if err != nil {
response.ServerError(c, "获取七日签到领取记录失败")
return
}
items := make([]claimDTO, 0, len(resp.GetClaims()))
for _, claim := range resp.GetClaims() {
items = append(items, claimFromProto(claim))
}
if err := h.fillClaimUsers(c.Request.Context(), items); err != nil {
response.ServerError(c, "补全用户信息失败")
return
}
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
return &activityv1.RequestMeta{
RequestId: middleware.CurrentRequestID(c),
Caller: "admin-server",
AppCode: appctx.FromContext(c.Request.Context()),
SentAtMs: time.Now().UnixMilli(),
}
}
func (h *Handler) resolveClaimUserID(ctx context.Context, keyword string) (int64, bool, bool) {
if keyword == "" {
return 0, false, true
}
if h.userDB == nil {
return 0, false, true
}
appCode := appctx.FromContext(ctx)
rows, err := h.userDB.QueryContext(ctx, `
SELECT user_id
FROM users
WHERE app_code = ?
AND (
CAST(user_id AS CHAR) = ?
OR current_display_user_id = ?
OR username LIKE ?
)
ORDER BY
CASE
WHEN CAST(user_id AS CHAR) = ? THEN 0
WHEN current_display_user_id = ? THEN 1
ELSE 2
END,
user_id DESC
LIMIT 1`,
appCode, keyword, keyword, "%"+keyword+"%", keyword, keyword,
)
if err != nil {
return 0, false, false
}
defer rows.Close()
if !rows.Next() {
return 0, false, rows.Err() == nil
}
var userID int64
if err := rows.Scan(&userID); err != nil {
return 0, false, false
}
return userID, true, rows.Err() == nil
}
func (h *Handler) fillClaimUsers(ctx context.Context, claims []claimDTO) error {
if h.userDB == nil || len(claims) == 0 {
return nil
}
ids := collectClaimUserIDs(claims)
if len(ids) == 0 {
return nil
}
args := append([]any{appctx.FromContext(ctx)}, int64Args(ids)...)
rows, err := h.userDB.QueryContext(ctx, `
SELECT user_id, current_display_user_id, COALESCE(username, ''), COALESCE(avatar, '')
FROM users
WHERE app_code = ? AND user_id IN (`+placeholders(len(ids))+`)`,
args...,
)
if err != nil {
return err
}
defer rows.Close()
users := make(map[int64]userDTO, len(ids))
for rows.Next() {
var user userDTO
if err := rows.Scan(&user.UserID, &user.DisplayUserID, &user.Username, &user.Avatar); err != nil {
return err
}
users[user.UserID] = user
}
if err := rows.Err(); err != nil {
return err
}
for index := range claims {
if user, ok := users[claims[index].UserID]; ok {
claims[index].User = &user
continue
}
claims[index].User = &userDTO{UserID: claims[index].UserID}
}
return nil
}
func configFromProto(config *activityv1.SevenDayCheckInConfig) configDTO {
if config == nil {
return configDTO{Rewards: defaultRewards()}
}
rewards := make([]rewardDTO, 0, len(config.GetRewards()))
for _, reward := range config.GetRewards() {
rewards = append(rewards, rewardDTO{DayIndex: reward.GetDayIndex(), ResourceGroupID: reward.GetResourceGroupId()})
}
return configDTO{
AppCode: config.GetAppCode(),
Enabled: config.GetEnabled(),
ConfigVersion: config.GetConfigVersion(),
Rewards: rewards,
UpdatedByAdminID: config.GetUpdatedByAdminId(),
CreatedAtMS: config.GetCreatedAtMs(),
UpdatedAtMS: config.GetUpdatedAtMs(),
}
}
func claimFromProto(claim *activityv1.SevenDayCheckInClaim) claimDTO {
if claim == nil {
return claimDTO{}
}
return claimDTO{
ClaimID: claim.GetClaimId(),
CommandID: claim.GetCommandId(),
UserID: claim.GetUserId(),
CycleNo: claim.GetCycleNo(),
CheckinDay: claim.GetCheckinDay(),
DayIndex: claim.GetDayIndex(),
ConfigVersion: claim.GetConfigVersion(),
ResourceGroupID: claim.GetResourceGroupId(),
WalletCommandID: claim.GetWalletCommandId(),
WalletGrantID: claim.GetWalletGrantId(),
Status: claim.GetStatus(),
FailureReason: claim.GetFailureReason(),
SignedAtMS: claim.GetSignedAtMs(),
GrantedAtMS: claim.GetGrantedAtMs(),
CreatedAtMS: claim.GetCreatedAtMs(),
UpdatedAtMS: claim.GetUpdatedAtMs(),
}
}
func defaultRewards() []rewardDTO {
items := make([]rewardDTO, 0, 7)
for day := int32(1); day <= 7; day++ {
items = append(items, rewardDTO{DayIndex: day})
}
return items
}
func firstQuery(c *gin.Context, keys ...string) string {
for _, key := range keys {
if value := c.Query(key); value != "" {
return value
}
}
return ""
}
func parseOptionalInt(value string) int {
value = strings.TrimSpace(value)
if value == "" {
return 0
}
var result int
for _, char := range value {
if char < '0' || char > '9' {
return -1
}
result = result*10 + int(char-'0')
}
return result
}
func collectClaimUserIDs(claims []claimDTO) []int64 {
seen := make(map[int64]struct{}, len(claims))
ids := make([]int64, 0, len(claims))
for _, claim := range claims {
if claim.UserID <= 0 {
continue
}
if _, ok := seen[claim.UserID]; ok {
continue
}
seen[claim.UserID] = struct{}{}
ids = append(ids, claim.UserID)
}
return ids
}
func int64Args(ids []int64) []any {
args := make([]any, 0, len(ids))
for _, id := range ids {
args = append(args, id)
}
return args
}
func placeholders(count int) string {
if count <= 0 {
return ""
}
return strings.TrimRight(strings.Repeat("?,", count), ",")
}

View File

@ -0,0 +1,17 @@
package sevendaycheckin
import (
"hyapp-admin-server/internal/middleware"
"github.com/gin-gonic/gin"
)
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
if h == nil {
return
}
protected.GET("/admin/activity/seven-day-checkin/config", middleware.RequirePermission("seven-day-checkin:view"), h.GetSevenDayCheckInConfig)
protected.PUT("/admin/activity/seven-day-checkin/config", middleware.RequirePermission("seven-day-checkin:update"), h.UpdateSevenDayCheckInConfig)
protected.GET("/admin/activity/seven-day-checkin/claims", middleware.RequirePermission("seven-day-checkin:view"), h.ListSevenDayCheckInClaims)
}

View File

@ -7,12 +7,13 @@ import (
)
type AppBannerListOptions struct {
AppCode string
Keyword string
Status string
Platform string
RegionID int64
Country string
AppCode string
Keyword string
Status string
DisplayScope string
Platform string
RegionID int64
Country string
}
func (s *Store) ListAppBanners(options AppBannerListOptions) ([]model.AppBanner, error) {
@ -21,6 +22,9 @@ func (s *Store) ListAppBanners(options AppBannerListOptions) ([]model.AppBanner,
if options.Status != "" {
query = query.Where("status = ?", strings.TrimSpace(options.Status))
}
if options.DisplayScope != "" {
query = query.Where("display_scope = ?", strings.TrimSpace(options.DisplayScope))
}
if options.Platform != "" {
query = query.Where("platform = ?", strings.TrimSpace(options.Platform))
}
@ -32,12 +36,25 @@ func (s *Store) ListAppBanners(options AppBannerListOptions) ([]model.AppBanner,
}
if options.Keyword != "" {
like := "%" + strings.TrimSpace(options.Keyword) + "%"
query = query.Where("(description LIKE ? OR param LIKE ? OR country_code LIKE ?)", like, like, like)
query = query.Where("(description LIKE ? OR param LIKE ? OR country_code LIKE ? OR display_scope LIKE ? OR room_small_image_url LIKE ?)", like, like, like, like, like)
}
err := query.Order("sort_order ASC, id DESC").Find(&items).Error
return items, err
}
func (s *Store) ExpireAppBanners(appCode string, nowMs int64) error {
appCode = strings.TrimSpace(appCode)
if appCode == "" || nowMs <= 0 {
return nil
}
return s.db.Model(&model.AppBanner{}).
Where("app_code = ? AND status = ? AND ends_at_ms > 0 AND ends_at_ms <= ?", appCode, "active", nowMs).
Updates(map[string]any{
"status": "expired",
"updated_at_ms": nowMs,
}).Error
}
func (s *Store) GetAppBanner(appCode string, id uint) (model.AppBanner, error) {
var item model.AppBanner
err := s.db.Where("app_code = ? AND id = ?", strings.TrimSpace(appCode), id).First(&item).Error

View File

@ -92,6 +92,10 @@ var defaultPermissions = []model.Permission{
{Name: "成就配置更新", Code: "achievement:update", Kind: "button"},
{Name: "注册奖励查看", Code: "registration-reward:view", Kind: "menu"},
{Name: "注册奖励更新", Code: "registration-reward:update", Kind: "button"},
{Name: "七日签到查看", Code: "seven-day-checkin:view", Kind: "menu"},
{Name: "七日签到更新", Code: "seven-day-checkin:update", Kind: "button"},
{Name: "幸运礼物查看", Code: "lucky-gift:view", Kind: "menu"},
{Name: "幸运礼物更新", Code: "lucky-gift:update", Kind: "button"},
{Name: "角色查看", Code: "role:view", Kind: "menu"},
{Name: "角色创建", Code: "role:create", Kind: "button"},
{Name: "角色更新", Code: "role:update", Kind: "button"},
@ -221,10 +225,12 @@ func (s *Store) seedMenus() error {
{ParentID: &resourceID, Title: "表情包列表", Code: "emoji-pack-list", Path: "/emoji-packs", Icon: "image", PermissionCode: "emoji-pack:view", Sort: 71, Visible: true},
{ParentID: &operationsID, Title: "金币流水", Code: "operation-coin-ledger", Path: "/operations/coin-ledger", Icon: "receipt", PermissionCode: "coin-ledger:view", Sort: 68, Visible: true},
{ParentID: &operationsID, Title: "金币增减", Code: "operation-coin-adjustment", Path: "/operations/coin-adjustments", Icon: "wallet", PermissionCode: "coin-adjustment:view", Sort: 69, Visible: true},
{ParentID: &operationsID, Title: "幸运礼物", Code: "lucky-gift", Path: "/operations/lucky-gift", Icon: "redeem", PermissionCode: "lucky-gift:view", Sort: 70, Visible: true},
{ParentID: &paymentID, Title: "账单列表", Code: "payment-bill-list", Path: "/payment/bills", Icon: "receipt", PermissionCode: "payment-bill:view", Sort: 68, Visible: true},
{ParentID: &activityID, Title: "每日任务", Code: "daily-task-list", Path: "/activities/daily-tasks", Icon: "task", PermissionCode: "daily-task:view", Sort: 69, Visible: true},
{ParentID: &activityID, Title: "注册奖励", Code: "registration-reward", Path: "/activities/registration-reward", Icon: "gift", PermissionCode: "registration-reward:view", Sort: 70, Visible: true},
{ParentID: &activityID, Title: "成就配置", Code: "achievement-config", Path: "/activities/achievements", Icon: "military_tech", PermissionCode: "achievement:view", Sort: 71, Visible: true},
{ParentID: &activityID, Title: "七日签到", Code: "seven-day-checkin", Path: "/activities/seven-day-checkin", Icon: "event_available", PermissionCode: "seven-day-checkin:view", Sort: 72, Visible: true},
{ParentID: &gameID, Title: "游戏列表", Code: "game-list", Path: "/games", Icon: "sports_esports", PermissionCode: "game:view", Sort: 70, Visible: true},
{ParentID: &geoID, Title: "国家管理", Code: "host-org-countries", Path: "/host/countries", Icon: "public", PermissionCode: "country:view", Sort: 70, Visible: true},
{ParentID: &geoID, Title: "区域管理", Code: "host-org-regions", Path: "/host/regions", Icon: "map", PermissionCode: "region:view", Sort: 71, Visible: true},
@ -460,15 +466,17 @@ func defaultRolePermissionCodes(code string) []string {
"bd:view", "bd:create", "bd:update",
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit",
"coin-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "payment-bill:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
"lucky-gift:view", "lucky-gift:update",
"game:view", "game:create", "game:update", "game:status",
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
"achievement:view", "achievement:create", "achievement:update",
"seven-day-checkin:view", "seven-day-checkin:update",
"log:view",
"job:view", "job:cancel", "export:create",
"upload:create",
}
case "auditor":
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-ledger:view", "coin-adjustment:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "role:view", "permission:view", "job:view"}
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-ledger:view", "coin-adjustment:view", "lucky-gift:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "role:view", "permission:view", "job:view"}
case "readonly":
return []string{
"overview:view",
@ -494,11 +502,13 @@ func defaultRolePermissionCodes(code string) []string {
"coin-seller:view",
"coin-ledger:view",
"coin-adjustment:view",
"lucky-gift:view",
"payment-bill:view",
"payment-product:view",
"game:view",
"daily-task:view",
"achievement:view",
"seven-day-checkin:view",
"role:view",
"permission:view",
"menu:view",
@ -530,12 +540,14 @@ func defaultRolePermissionMigrationCodes(code string) []string {
"region:view", "region:create", "region:update", "region:status",
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit",
"coin-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "payment-bill:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
"lucky-gift:view", "lucky-gift:update",
"game:view", "game:create", "game:update", "game:status",
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
"achievement:view", "achievement:create", "achievement:update",
"seven-day-checkin:view", "seven-day-checkin:update",
}
case "auditor", "readonly":
return []string{"level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-seller:view", "coin-ledger:view", "coin-adjustment:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view"}
return []string{"level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-seller:view", "coin-ledger:view", "coin-adjustment:view", "lucky-gift:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view"}
default:
return nil
}

View File

@ -19,6 +19,7 @@ import (
"hyapp-admin-server/internal/modules/hostorg"
"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/payment"
"hyapp-admin-server/internal/modules/rbac"
@ -27,6 +28,7 @@ import (
resourcemodule "hyapp-admin-server/internal/modules/resource"
"hyapp-admin-server/internal/modules/roomadmin"
"hyapp-admin-server/internal/modules/search"
"hyapp-admin-server/internal/modules/sevendaycheckin"
"hyapp-admin-server/internal/modules/upload"
"hyapp-admin-server/internal/platform/logging"
"hyapp-admin-server/internal/service"
@ -51,6 +53,7 @@ type Handlers struct {
HostOrg *hostorg.Handler
Job *job.Handler
LevelConfig *levelconfig.Handler
LuckyGift *luckygift.Handler
Menu *menu.Handler
Payment *payment.Handler
RBAC *rbac.Handler
@ -59,6 +62,7 @@ type Handlers struct {
Resource *resourcemodule.Handler
RoomAdmin *roomadmin.Handler
Search *search.Handler
SevenDayCheckIn *sevendaycheckin.Handler
Upload *upload.Handler
}
@ -95,8 +99,10 @@ func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
payment.RegisterRoutes(protected, h.Payment)
job.RegisterRoutes(protected, h.Job)
levelconfig.RegisterRoutes(protected, h.LevelConfig)
luckygift.RegisterRoutes(protected, h.LuckyGift)
upload.RegisterRoutes(protected, h.Upload)
search.RegisterRoutes(protected, h.Search)
sevendaycheckin.RegisterRoutes(protected, h.SevenDayCheckIn)
return engine
}

View File

@ -1,135 +1,139 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
CREATE TABLE IF NOT EXISTS admin_users (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(80) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
team VARCHAR(80),
status VARCHAR(24) NOT NULL DEFAULT 'active',
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
last_login_at_ms BIGINT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
username VARCHAR(64) NOT NULL UNIQUE COMMENT '用户名',
name VARCHAR(80) NOT NULL COMMENT '名称',
password_hash VARCHAR(255) NOT NULL COMMENT '密码哈希值',
team VARCHAR(80) COMMENT '所属团队',
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT '业务状态',
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否启用多因素认证',
last_login_at_ms BIGINT 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',
INDEX idx_admin_users_status (status)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台用户表';
CREATE TABLE IF NOT EXISTS admin_roles (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(80) NOT NULL UNIQUE,
code VARCHAR(80) NOT NULL UNIQUE,
description VARCHAR(255),
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL
);
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
name VARCHAR(80) NOT NULL UNIQUE COMMENT '名称',
code VARCHAR(80) NOT NULL UNIQUE COMMENT '编码',
description VARCHAR(255) COMMENT '描述信息',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台角色表';
CREATE TABLE IF NOT EXISTS admin_permissions (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL UNIQUE,
kind VARCHAR(32) NOT NULL,
description VARCHAR(255),
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
name VARCHAR(100) NOT NULL COMMENT '名称',
code VARCHAR(100) NOT NULL UNIQUE COMMENT '编码',
kind VARCHAR(32) NOT NULL COMMENT '分类',
description VARCHAR(255) COMMENT '描述信息',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
INDEX idx_admin_permissions_kind (kind)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台权限表';
CREATE TABLE IF NOT EXISTS admin_user_roles (
user_id BIGINT UNSIGNED NOT NULL,
role_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL COMMENT '用户 ID',
role_id BIGINT UNSIGNED NOT NULL COMMENT '角色 ID',
PRIMARY KEY (user_id, role_id)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台用户角色表';
CREATE TABLE IF NOT EXISTS admin_role_permissions (
role_id BIGINT UNSIGNED NOT NULL,
permission_id BIGINT UNSIGNED NOT NULL,
role_id BIGINT UNSIGNED NOT NULL COMMENT '角色 ID',
permission_id BIGINT UNSIGNED NOT NULL COMMENT '权限 ID',
PRIMARY KEY (role_id, permission_id)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台角色权限表';
CREATE TABLE IF NOT EXISTS admin_menus (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
parent_id BIGINT UNSIGNED NULL,
title VARCHAR(80) NOT NULL,
code VARCHAR(100) NOT NULL UNIQUE,
path VARCHAR(160),
icon VARCHAR(64),
permission_code VARCHAR(100),
sort INT NOT NULL DEFAULT 0,
visible BOOLEAN NOT NULL DEFAULT TRUE,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
parent_id BIGINT UNSIGNED NULL COMMENT '父级 ID',
title VARCHAR(80) NOT NULL COMMENT '标题',
code VARCHAR(100) NOT NULL UNIQUE COMMENT '编码',
path VARCHAR(160) COMMENT '路由路径',
icon VARCHAR(64) COMMENT '图标名称',
permission_code VARCHAR(100) COMMENT '权限编码',
sort INT NOT NULL DEFAULT 0 COMMENT '排序值',
visible BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否可见',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
INDEX idx_admin_menus_parent_id (parent_id),
INDEX idx_admin_menus_permission_code (permission_code)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台菜单表';
CREATE TABLE IF NOT EXISTS admin_refresh_tokens (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
token_hash VARCHAR(128) NOT NULL UNIQUE,
user_agent VARCHAR(255),
ip VARCHAR(64),
expires_at_ms BIGINT NOT NULL,
revoked_at_ms BIGINT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
user_id BIGINT UNSIGNED NOT NULL COMMENT '用户 ID',
token_hash VARCHAR(128) NOT NULL UNIQUE COMMENT '令牌哈希值',
user_agent VARCHAR(255) COMMENT '用户代理',
ip VARCHAR(64) COMMENT 'IP',
expires_at_ms BIGINT NOT NULL COMMENT '过期时间UTC epoch ms',
revoked_at_ms BIGINT 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',
INDEX idx_admin_refresh_tokens_user_id (user_id),
INDEX idx_admin_refresh_tokens_expires_at (expires_at_ms)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台刷新令牌表';
CREATE TABLE IF NOT EXISTS admin_login_logs (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64),
user_id BIGINT UNSIGNED NULL,
ip VARCHAR(64),
user_agent VARCHAR(255),
status VARCHAR(32) NOT NULL,
message VARCHAR(255),
created_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
username VARCHAR(64) COMMENT '用户名',
user_id BIGINT UNSIGNED NULL COMMENT '用户 ID',
ip VARCHAR(64) COMMENT 'IP',
user_agent VARCHAR(255) COMMENT '用户代理',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
message VARCHAR(255) COMMENT '消息',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
INDEX idx_admin_login_logs_username (username),
INDEX idx_admin_login_logs_status (status)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台登录日志表';
CREATE TABLE IF NOT EXISTS admin_operation_logs (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
username VARCHAR(64) NOT NULL,
action VARCHAR(80) NOT NULL,
resource VARCHAR(120),
method VARCHAR(12),
path VARCHAR(180),
ip VARCHAR(64),
status VARCHAR(32) NOT NULL,
detail TEXT,
created_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
user_id BIGINT UNSIGNED NOT NULL COMMENT '用户 ID',
username VARCHAR(64) NOT NULL COMMENT '用户名',
action VARCHAR(80) NOT NULL COMMENT '操作',
resource VARCHAR(120) COMMENT '资源',
method VARCHAR(12) COMMENT '方法',
path VARCHAR(180) COMMENT '路由路径',
ip VARCHAR(64) COMMENT 'IP',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
detail TEXT COMMENT '详情',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
INDEX idx_admin_operation_logs_user_id (user_id),
INDEX idx_admin_operation_logs_action (action)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台操作日志表';
CREATE TABLE IF NOT EXISTS admin_notifications (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(120) NOT NULL,
content VARCHAR(500),
level VARCHAR(24) NOT NULL DEFAULT 'info',
read_at_ms BIGINT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
title VARCHAR(120) NOT NULL COMMENT '标题',
content VARCHAR(500) COMMENT '内容',
level VARCHAR(24) NOT NULL DEFAULT 'info' COMMENT '等级',
read_at_ms BIGINT 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',
INDEX idx_admin_notifications_level (level)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台通知表';
CREATE TABLE IF NOT EXISTS admin_service_assets (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(80) NOT NULL UNIQUE,
name VARCHAR(120) NOT NULL,
description VARCHAR(255),
status VARCHAR(24) NOT NULL,
deploy VARCHAR(80),
version VARCHAR(40),
port VARCHAR(32),
replicas VARCHAR(32),
response VARCHAR(32),
cpu INT NOT NULL DEFAULT 0,
memory INT NOT NULL DEFAULT 0,
trend_json TEXT,
logs_json TEXT,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
code VARCHAR(80) NOT NULL UNIQUE COMMENT '编码',
name VARCHAR(120) NOT NULL COMMENT '名称',
description VARCHAR(255) COMMENT '描述信息',
status VARCHAR(24) NOT NULL COMMENT '业务状态',
deploy VARCHAR(80) COMMENT '部署',
version VARCHAR(40) COMMENT '版本号',
port VARCHAR(32) COMMENT '端口',
replicas VARCHAR(32) COMMENT '副本数',
response VARCHAR(32) COMMENT '响应',
cpu INT NOT NULL DEFAULT 0 COMMENT 'CPU',
memory INT NOT NULL DEFAULT 0 COMMENT '内存',
trend_json TEXT COMMENT '趋势 JSON 配置或快照',
logs_json TEXT COMMENT '日志 JSON 配置或快照',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
INDEX idx_admin_service_assets_status (status)
);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台服务资产表';

View File

@ -1,30 +1,34 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
CREATE TABLE IF NOT EXISTS admin_data_scopes (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
role_id BIGINT UNSIGNED NOT NULL,
scope_type VARCHAR(40) NOT NULL,
scope_value VARCHAR(120) NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
role_id BIGINT UNSIGNED NOT NULL COMMENT '角色 ID',
scope_type VARCHAR(40) NOT NULL COMMENT '范围类型',
scope_value VARCHAR(120) NOT NULL COMMENT '范围值',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
UNIQUE KEY uk_admin_data_scopes_role_scope (role_id, scope_type, scope_value),
INDEX idx_admin_data_scopes_role_id (role_id),
INDEX idx_admin_data_scopes_scope (scope_type, scope_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台数据范围表';
CREATE TABLE IF NOT EXISTS admin_jobs (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
type VARCHAR(80) NOT NULL,
status VARCHAR(32) NOT NULL,
payload_json TEXT,
result_json TEXT,
error TEXT,
created_by BIGINT UNSIGNED NOT NULL,
created_by_name VARCHAR(64) NOT NULL,
started_at_ms BIGINT NULL,
finished_at_ms BIGINT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
type VARCHAR(80) NOT NULL COMMENT '类型',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
payload_json TEXT COMMENT '业务负载 JSON 快照',
result_json TEXT COMMENT '结果 JSON 配置或快照',
error TEXT COMMENT '错误',
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人',
created_by_name VARCHAR(64) NOT NULL COMMENT '创建人名称',
started_at_ms BIGINT NULL COMMENT '开始时间UTC epoch ms',
finished_at_ms BIGINT 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',
INDEX idx_admin_jobs_type (type),
INDEX idx_admin_jobs_status (status),
INDEX idx_admin_jobs_created_by (created_by),
INDEX idx_admin_jobs_created_by_name (created_by_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台任务表';

View File

@ -1,19 +1,23 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
ALTER TABLE admin_operation_logs
ADD COLUMN request_id VARCHAR(64) NOT NULL DEFAULT '' AFTER id,
ADD COLUMN resource_id VARCHAR(80) NOT NULL DEFAULT '' AFTER resource,
ADD COLUMN user_agent VARCHAR(255) NOT NULL DEFAULT '' AFTER ip,
ADD COLUMN http_status INT NOT NULL DEFAULT 0 AFTER status,
ADD COLUMN risk_level VARCHAR(24) NOT NULL DEFAULT 'normal' AFTER http_status,
ADD COLUMN latency_ms BIGINT NOT NULL DEFAULT 0 AFTER risk_level,
ADD COLUMN request_id VARCHAR(64) NOT NULL DEFAULT '' COMMENT '链路请求 ID用于排查问题' AFTER id,
ADD COLUMN resource_id VARCHAR(80) NOT NULL DEFAULT '' COMMENT '资源 ID' AFTER resource,
ADD COLUMN user_agent VARCHAR(255) NOT NULL DEFAULT '' COMMENT '用户代理' AFTER ip,
ADD COLUMN http_status INT NOT NULL DEFAULT 0 COMMENT 'HTTP 状态' AFTER status,
ADD COLUMN risk_level VARCHAR(24) NOT NULL DEFAULT 'normal' COMMENT '风险等级' AFTER http_status,
ADD COLUMN latency_ms BIGINT NOT NULL DEFAULT 0 COMMENT '耗时毫秒值' AFTER risk_level,
ADD INDEX idx_admin_operation_logs_request_id (request_id),
ADD INDEX idx_admin_operation_logs_resource_id (resource_id),
ADD INDEX idx_admin_operation_logs_risk_level (risk_level);
ALTER TABLE admin_jobs
ADD COLUMN attempt INT NOT NULL DEFAULT 0 AFTER error,
ADD COLUMN max_attempts INT NOT NULL DEFAULT 3 AFTER attempt,
ADD COLUMN locked_by VARCHAR(120) NOT NULL DEFAULT '' AFTER max_attempts,
ADD COLUMN locked_until_ms BIGINT NULL AFTER locked_by,
ADD COLUMN artifact_path VARCHAR(255) NOT NULL DEFAULT '' AFTER locked_until_ms,
ADD COLUMN attempt INT NOT NULL DEFAULT 0 COMMENT '尝试次数' AFTER error,
ADD COLUMN max_attempts INT NOT NULL DEFAULT 3 COMMENT '最大尝试次数' AFTER attempt,
ADD COLUMN locked_by VARCHAR(120) NOT NULL DEFAULT '' COMMENT '当前锁定 worker' AFTER max_attempts,
ADD COLUMN locked_until_ms BIGINT NULL COMMENT '锁定截止时间UTC epoch ms' AFTER locked_by,
ADD COLUMN artifact_path VARCHAR(255) NOT NULL DEFAULT '' COMMENT '产物路径' AFTER locked_until_ms,
ADD INDEX idx_admin_jobs_locked_by (locked_by),
ADD INDEX idx_admin_jobs_locked_until (locked_until_ms);

View File

@ -1,13 +1,17 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
ALTER TABLE admin_operation_logs
MODIFY COLUMN request_id VARCHAR(64) NOT NULL DEFAULT '',
MODIFY COLUMN resource_id VARCHAR(80) NOT NULL DEFAULT '',
MODIFY COLUMN user_agent VARCHAR(255) NOT NULL DEFAULT '',
MODIFY COLUMN http_status INT NOT NULL DEFAULT 0,
MODIFY COLUMN risk_level VARCHAR(24) NOT NULL DEFAULT 'normal',
MODIFY COLUMN latency_ms BIGINT NOT NULL DEFAULT 0;
MODIFY COLUMN request_id VARCHAR(64) NOT NULL DEFAULT '' COMMENT '链路请求 ID用于排查问题',
MODIFY COLUMN resource_id VARCHAR(80) NOT NULL DEFAULT '' COMMENT '资源 ID',
MODIFY COLUMN user_agent VARCHAR(255) NOT NULL DEFAULT '' COMMENT '用户代理',
MODIFY COLUMN http_status INT NOT NULL DEFAULT 0 COMMENT 'HTTP 状态',
MODIFY COLUMN risk_level VARCHAR(24) NOT NULL DEFAULT 'normal' COMMENT '风险等级',
MODIFY COLUMN latency_ms BIGINT NOT NULL DEFAULT 0 COMMENT '耗时毫秒值';
ALTER TABLE admin_jobs
MODIFY COLUMN attempt INT NOT NULL DEFAULT 0,
MODIFY COLUMN max_attempts INT NOT NULL DEFAULT 3,
MODIFY COLUMN locked_by VARCHAR(120) NOT NULL DEFAULT '',
MODIFY COLUMN artifact_path VARCHAR(255) NOT NULL DEFAULT '';
MODIFY COLUMN attempt INT NOT NULL DEFAULT 0 COMMENT '尝试次数',
MODIFY COLUMN max_attempts INT NOT NULL DEFAULT 3 COMMENT '最大尝试次数',
MODIFY COLUMN locked_by VARCHAR(120) NOT NULL DEFAULT '' COMMENT '当前锁定 worker',
MODIFY COLUMN artifact_path VARCHAR(255) NOT NULL DEFAULT '' COMMENT '产物路径';

View File

@ -1,3 +1,7 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
DELETE arp
FROM admin_role_permissions arp
JOIN admin_permissions p ON p.id = arp.permission_id

View File

@ -1,17 +1,21 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
CREATE TABLE IF NOT EXISTS admin_app_banners (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
cover_url VARCHAR(1024) NOT NULL,
banner_type VARCHAR(16) NOT NULL,
param VARCHAR(2048) NOT NULL DEFAULT '',
status VARCHAR(24) NOT NULL DEFAULT 'active',
platform VARCHAR(24) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
region_id BIGINT NOT NULL DEFAULT 0,
country_code VARCHAR(8) NOT NULL DEFAULT '',
description VARCHAR(255) NOT NULL DEFAULT '',
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
cover_url VARCHAR(1024) NOT NULL COMMENT '封面 URL',
banner_type VARCHAR(16) NOT NULL COMMENT 'Banner 类型',
param VARCHAR(2048) NOT NULL DEFAULT '' COMMENT '参数',
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT '业务状态',
platform VARCHAR(24) NOT NULL COMMENT '平台',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '区域 ID',
country_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '国家或地区编码',
description VARCHAR(255) NOT NULL DEFAULT '' COMMENT '描述信息',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
INDEX idx_admin_app_banners_app_sort (app_code, status, sort_order, id),
INDEX idx_admin_app_banners_scope (app_code, platform, region_id, country_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台应用 Banner 表';

View File

@ -1,14 +1,18 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
CREATE TABLE IF NOT EXISTS admin_app_versions (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
platform VARCHAR(24) NOT NULL,
version VARCHAR(40) NOT NULL,
build_number BIGINT NOT NULL,
force_update BOOLEAN NOT NULL DEFAULT FALSE,
download_url VARCHAR(2048) NOT NULL,
description TEXT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
platform VARCHAR(24) NOT NULL COMMENT '平台',
version VARCHAR(40) NOT NULL COMMENT '版本号',
build_number BIGINT NOT NULL COMMENT '构建编号',
force_update BOOLEAN NOT NULL DEFAULT FALSE COMMENT '强制更新',
download_url VARCHAR(2048) NOT NULL COMMENT '下载 URL',
description TEXT NULL COMMENT '描述信息',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
UNIQUE KEY uk_admin_app_versions_build (app_code, platform, build_number),
INDEX idx_admin_app_versions_latest (app_code, platform, build_number, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台应用版本表';

View File

@ -1,3 +1,7 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
DELETE arp
FROM admin_role_permissions arp
JOIN admin_permissions p ON p.id = arp.permission_id

View File

@ -1,6 +1,9 @@
-- Admin navigation is a production contract, not local demo data.
-- This migration keeps the current operations/activity/game menus reproducible
-- when bootstrap seed is disabled on production services.
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
-- 后台导航是生产契约,不是本地演示数据。
-- 该迁移保证生产环境关闭 bootstrap seed 时,运营、活动和游戏菜单仍可重复初始化。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES

View File

@ -1,5 +1,9 @@
-- Level configuration is owned by activity-service but exposed under User Management in admin.
-- The menu and permissions are seeded through migration so production can run without bootstrap.
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
-- 等级配置事实归 activity-service 所有,后台仅在用户管理下暴露运营入口。
-- 菜单和权限通过 migration 写入,保证生产环境不依赖 bootstrap。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES

View File

@ -1,4 +1,8 @@
-- Achievement definitions are owned by activity-service and exposed through admin as an operations configuration page.
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
-- 成就定义事实归 activity-service 所有,后台只提供运营配置入口。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES

View File

@ -1,4 +1,8 @@
-- Emoji packs are wallet resources with a dedicated admin surface for cover, animation and region scope.
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
-- 表情包属于 wallet-service 资源,后台只维护封面、动效和区域范围等运营入口。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES

View File

@ -1,4 +1,8 @@
-- Coin adjustments use wallet-service manual_credit ledger entries and expose only admin navigation/permissions here.
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
-- 金币调整通过 wallet-service manual_credit 账务流水落账,本迁移只写后台菜单和权限事实。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES

View File

@ -1,5 +1,9 @@
-- Region block is a user-service login risk policy maintained from User Management.
-- The migration seeds menu and RBAC facts for environments that do not run bootstrap.
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
-- 区域拦截是 user-service 登录风控策略,后台在用户管理下维护。
-- 该迁移写入菜单和 RBAC 事实,覆盖不运行 bootstrap 的环境。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES

View File

@ -0,0 +1,61 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
-- 七日签到事实归 activity-service 所有,后台只维护开关和资源组奖励映射。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
('七日签到查看', 'seven-day-checkin:view', 'menu', '', @now_ms, @now_ms),
('七日签到更新', 'seven-day-checkin:update', 'button', '', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
kind = VALUES(kind),
description = VALUES(description),
updated_at_ms = @now_ms;
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms) VALUES
(NULL, '活动管理', 'activities', '', 'campaign', '', 70, TRUE, @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
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 INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
SELECT parent.id, '七日签到', 'seven-day-checkin', '/activities/seven-day-checkin', 'event_available', 'seven-day-checkin:view', 72, TRUE, @now_ms, @now_ms
FROM admin_menus parent
WHERE parent.code = 'activities'
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
title = VALUES(title),
path = VALUES(path),
icon = VALUES(icon),
permission_code = VALUES(permission_code),
sort = VALUES(sort),
visible = VALUES(visible),
updated_at_ms = @now_ms;
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code = 'platform-admin'
AND admin_permission.code IN ('seven-day-checkin:view', 'seven-day-checkin:update');
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code = 'ops-admin'
AND admin_permission.code IN ('seven-day-checkin:view', 'seven-day-checkin:update');
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('auditor', 'readonly')
AND admin_permission.code = 'seven-day-checkin:view';

View File

@ -0,0 +1,11 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
-- Banner 显示范围和投放时间由后台维护gateway 只读取当前有效窗口。
ALTER TABLE admin_app_banners
ADD COLUMN room_small_image_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '房间内小屏图 URL' AFTER cover_url,
ADD COLUMN display_scope VARCHAR(32) NOT NULL DEFAULT 'home' COMMENT '显示范围:首页、房间内或充值页' AFTER banner_type,
ADD COLUMN starts_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '投放开始时间UTC epoch ms' AFTER status,
ADD COLUMN ends_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '投放结束时间UTC epoch ms' AFTER starts_at_ms,
ADD INDEX idx_admin_app_banners_display (app_code, display_scope, status, starts_at_ms, ends_at_ms, sort_order, id);

View File

@ -0,0 +1,59 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 幸运礼物后台只管理 activity-service 规则和抽奖审计,不直接改钱包或房间事实。
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
('幸运礼物查看', 'lucky-gift:view', 'menu', '', @now_ms, @now_ms),
('幸运礼物更新', 'lucky-gift:update', 'button', '', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
kind = VALUES(kind),
description = VALUES(description),
updated_at_ms = @now_ms;
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms) VALUES
(NULL, '运营管理', 'operations', '', 'operations', '', 68, TRUE, @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
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 INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
SELECT parent.id, '幸运礼物', 'lucky-gift', '/operations/lucky-gift', 'redeem', 'lucky-gift:view', 70, 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 admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code = 'platform-admin'
AND admin_permission.code IN ('lucky-gift:view', 'lucky-gift:update');
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code = 'ops-admin'
AND admin_permission.code IN ('lucky-gift:view', 'lucky-gift:update');
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT admin_role.id, admin_permission.id
FROM admin_roles admin_role
JOIN admin_permissions admin_permission
WHERE admin_role.code IN ('auditor', 'readonly')
AND admin_permission.code = 'lucky-gift:view';

View File

@ -23,8 +23,10 @@ import (
activityservice "hyapp/services/activity-service/internal/service/activity"
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
growthservice "hyapp/services/activity-service/internal/service/growth"
luckygiftservice "hyapp/services/activity-service/internal/service/luckygift"
messageservice "hyapp/services/activity-service/internal/service/message"
registrationrewardservice "hyapp/services/activity-service/internal/service/registrationreward"
sevendaycheckinservice "hyapp/services/activity-service/internal/service/sevendaycheckin"
taskservice "hyapp/services/activity-service/internal/service/task"
mysqlstorage "hyapp/services/activity-service/internal/storage/mysql"
grpcserver "hyapp/services/activity-service/internal/transport/grpc"
@ -82,8 +84,10 @@ func New(cfg config.Config) (*App, error) {
messageSvc := messageservice.New(messageservice.Config{NodeID: cfg.NodeID}, repository, messageservice.WithTargetSource(client.NewGRPCUserTargetSource(userConn)))
taskSvc := taskservice.New(repository, walletv1.NewWalletServiceClient(walletConn))
registrationRewardSvc := registrationrewardservice.New(repository, walletv1.NewWalletServiceClient(walletConn))
sevenDayCheckInSvc := sevendaycheckinservice.New(repository, walletv1.NewWalletServiceClient(walletConn))
growthSvc := growthservice.New(repository, walletv1.NewWalletServiceClient(walletConn))
achievementSvc := achievementservice.New(repository, walletv1.NewWalletServiceClient(walletConn))
luckyGiftSvc := luckygiftservice.New(repository)
var broadcastPublisher broadcastservice.Publisher
if cfg.TencentIM.Enabled {
tencentClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
@ -121,8 +125,12 @@ func New(cfg config.Config) (*App, error) {
activityv1.RegisterAdminGrowthLevelServiceServer(server, grpcserver.NewAdminGrowthLevelServer(growthSvc))
activityv1.RegisterAchievementServiceServer(server, grpcserver.NewAchievementServer(achievementSvc))
activityv1.RegisterAdminAchievementServiceServer(server, grpcserver.NewAdminAchievementServer(achievementSvc))
activityv1.RegisterLuckyGiftServiceServer(server, grpcserver.NewLuckyGiftServer(luckyGiftSvc))
activityv1.RegisterAdminLuckyGiftServiceServer(server, grpcserver.NewAdminLuckyGiftServer(luckyGiftSvc))
activityv1.RegisterRegistrationRewardServiceServer(server, grpcserver.NewRegistrationRewardServer(registrationRewardSvc))
activityv1.RegisterAdminRegistrationRewardServiceServer(server, grpcserver.NewAdminRegistrationRewardServer(registrationRewardSvc))
activityv1.RegisterSevenDayCheckInServiceServer(server, grpcserver.NewSevenDayCheckInServer(sevenDayCheckInSvc))
activityv1.RegisterAdminSevenDayCheckInServiceServer(server, grpcserver.NewAdminSevenDayCheckInServer(sevenDayCheckInSvc))
broadcastServer := grpcserver.NewBroadcastServer(broadcastSvc, growthSvc)
activityv1.RegisterBroadcastServiceServer(server, broadcastServer)
activityv1.RegisterRoomEventConsumerServiceServer(server, broadcastServer)

View File

@ -0,0 +1,134 @@
package luckygift
const (
GlobalConfigGiftID = "__global__"
StatusDraft = "draft"
StatusActive = "active"
StatusPaused = "paused"
StatusClosed = "closed"
StatusPending = "pending"
PoolNovice = "novice"
PoolIntermediate = "intermediate"
PoolAdvanced = "advanced"
SourceBaseRTP = "base_rtp"
SourceRoomAtmosphere = "room_atmosphere"
SourceActivity = "activity_subsidy"
SourcePresentation = "presentation_only"
)
// Tier 是单个体验池里的候选奖档;权重只塑造体验节奏,基础成本仍由 RTP 窗口兜住。
type Tier struct {
Pool string `json:"pool"`
TierID string `json:"tier_id"`
RewardCoins int64 `json:"reward_coins"`
Weight int64 `json:"weight"`
HighWaterOnly bool `json:"high_water_only"`
Enabled bool `json:"enabled"`
}
// Config 是幸运礼物线上规则快照。金额均为整数金币,比例均为 ppm。
type Config struct {
AppCode string `json:"app_code"`
GiftID string `json:"gift_id"`
Enabled bool `json:"enabled"`
RuleVersion int64 `json:"rule_version"`
GiftPrice int64 `json:"gift_price"`
TargetRTPPPM int64 `json:"target_rtp_ppm"`
PoolRatePPM int64 `json:"pool_rate_ppm"`
GlobalWindowDraws int64 `json:"global_window_draws"`
GiftWindowDraws int64 `json:"gift_window_draws"`
NoviceDrawLimit int64 `json:"novice_draw_limit"`
IntermediateDrawLimit int64 `json:"intermediate_draw_limit"`
HighMultiplier int64 `json:"high_multiplier"`
HighWaterPoolMultiple int64 `json:"high_water_pool_multiple"`
PlatformPoolWeightPPM int64 `json:"platform_pool_weight_ppm"`
RoomPoolWeightPPM int64 `json:"room_pool_weight_ppm"`
GiftPoolWeightPPM int64 `json:"gift_pool_weight_ppm"`
InitialPlatformPool int64 `json:"initial_platform_pool"`
InitialGiftPool int64 `json:"initial_gift_pool"`
InitialRoomPool int64 `json:"initial_room_pool"`
PlatformReserve int64 `json:"platform_reserve"`
GiftReserve int64 `json:"gift_reserve"`
RoomReserve int64 `json:"room_reserve"`
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"`
RoomAtmosphereRatePPM int64 `json:"room_atmosphere_rate_ppm"`
RoomAtmosphereInitial int64 `json:"room_atmosphere_initial"`
RoomAtmosphereReserve int64 `json:"room_atmosphere_reserve"`
ActivityBudget int64 `json:"activity_budget"`
ActivityDailyLimit int64 `json:"activity_daily_limit"`
LargeTierEnabled bool `json:"large_tier_enabled"`
Tiers []Tier `json:"tiers"`
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type CheckCommand struct {
GiftID string
UserID int64
RoomID string
}
type DrawCommand struct {
CommandID string
UserID int64
DeviceID string
RoomID string
AnchorID string
GiftID string
CoinSpent int64
PaidAtMS int64
}
type CheckResult struct {
Enabled bool
Reason string
GiftID string
GiftPrice int64
RuleVersion int64
TargetRTPPPM int64
ExperiencePool string
}
type DrawResult struct {
DrawID string
CommandID string
GiftID string
RuleVersion int64
ExperiencePool string
SelectedTierID string
BaseRewardCoins int64
RoomAtmosphereRewardCoins int64
ActivitySubsidyCoins int64
EffectiveRewardCoins int64
BudgetSourcesJSON string
RewardStatus string
RTPWindowIndex int64
GiftRTPWindowIndex int64
GlobalBaseRTPPPM int64
GiftBaseRTPPPM int64
StageFeedback bool
HighMultiplier bool
CreatedAtMS int64
}
type AdminConfigQuery struct {
GiftID string
}
type DrawQuery struct {
GiftID string
UserID int64
RoomID string
Status string
Page int32
PageSize int32
}

View File

@ -0,0 +1,113 @@
package sevendaycheckin
const (
DayCount = 7
StatusPending = "pending"
StatusGranted = "granted"
StatusFailed = "failed"
RewardStatusGranted = "granted"
RewardStatusAvailable = "available"
RewardStatusLocked = "locked"
RewardStatusPending = "pending"
RewardStatusFailed = "failed"
ReasonEligible = "eligible"
ReasonDisabled = "disabled"
ReasonNotConfigured = "not_configured"
ReasonAlreadySigned = "already_signed"
ReasonPendingReward = "pending_reward"
ReasonInvalidConfig = "invalid_config"
)
// Reward 是配置中的某一天资源组奖励;资源组内容仍由 wallet-service 拥有。
type Reward struct {
DayIndex int32
ResourceGroupID int64
}
// Config 是 activity-service 持有的七日签到当前配置。
type Config struct {
AppCode string
Enabled bool
ConfigVersion int64
Rewards []Reward
UpdatedByAdminID int64
CreatedAtMS int64
UpdatedAtMS int64
}
// Account 是用户连续签到 read model只在奖励发放成功后推进。
type Account struct {
AppCode string
UserID int64
CycleNo int64
CycleStartDay string
LastCheckinDay string
LastDayIndex int32
ConfigVersion int64
UpdatedAtMS int64
}
// Claim 是用户一次签到和资源组发放事实。
type Claim struct {
ClaimID string
CommandID string
UserID int64
CycleNo int64
CheckinDay string
DayIndex int32
ConfigVersion int64
ResourceGroupID int64
WalletCommandID string
WalletGrantID string
Status string
FailureReason string
SignedAtMS int64
GrantedAtMS int64
CreatedAtMS int64
UpdatedAtMS int64
}
// RewardStatus 是 App 面板中一天奖励的用户侧状态。
type RewardStatus struct {
Reward
Status string
ClaimID string
WalletGrantID string
GrantedAtMS int64
}
// StatusResult 是 App 查询七日签到面板的领域结果。
type StatusResult struct {
Enabled bool
CanSign bool
Reason string
CycleNo int64
CurrentDayIndex int32
NextDayIndex int32
CheckinDay string
LastCheckinDay string
ConfigVersion int64
Rewards []RewardStatus
ServerTimeMS int64
NextRefreshMS int64
}
// PrepareResult 表达签到 claim 是否需要继续调用 wallet-service 发放资源组。
type PrepareResult struct {
Claim Claim
Prepared bool
Reason string
}
// ConfigQuery 是后台领取记录分页筛选条件。
type ClaimQuery struct {
Status string
UserID int64
CheckinDay string
DayIndex int32
Page int32
PageSize int32
}

View File

@ -0,0 +1,134 @@
package luckygift
import (
"fmt"
"strings"
"hyapp/pkg/xerr"
domain "hyapp/services/activity-service/internal/domain/luckygift"
)
const ppmScale int64 = 1_000_000
// DefaultConfig 给后台首屏提供整体规则草稿;真正生效必须由后台显式 enabled。
func DefaultConfig(appCode string) domain.Config {
const giftPrice int64 = 10_000
return domain.Config{
AppCode: appCode,
GiftID: domain.GlobalConfigGiftID,
Enabled: false,
GiftPrice: giftPrice,
TargetRTPPPM: 950_000,
PoolRatePPM: 950_000,
GlobalWindowDraws: 100_000,
GiftWindowDraws: 100_000,
NoviceDrawLimit: 2_000,
IntermediateDrawLimit: 20_000,
HighMultiplier: 100,
HighWaterPoolMultiple: 2,
PlatformPoolWeightPPM: 200_000,
RoomPoolWeightPPM: 300_000,
GiftPoolWeightPPM: 500_000,
InitialPlatformPool: 9_500_000,
InitialGiftPool: 23_750_000,
InitialRoomPool: 5_000_000,
PlatformReserve: 1_000_000,
GiftReserve: 1_000_000,
RoomReserve: 100_000,
MaxSinglePayout: giftPrice * 500,
UserHourlyPayoutCap: 3_420_000,
UserDailyPayoutCap: 61_560_000,
DeviceDailyPayoutCap: 102_600_000,
RoomHourlyPayoutCap: 68_400_000,
AnchorDailyPayoutCap: 1_231_200_000,
RoomAtmosphereRatePPM: 10_000,
RoomAtmosphereInitial: 100_000,
RoomAtmosphereReserve: 10_000,
ActivityBudget: 0,
ActivityDailyLimit: 0,
LargeTierEnabled: true,
Tiers: defaultTiers(giftPrice),
}
}
func validateConfig(config domain.Config) error {
if config.GiftID != domain.GlobalConfigGiftID {
return xerr.New(xerr.InvalidArgument, "lucky gift config scope is invalid")
}
if config.GiftPrice <= 0 || config.TargetRTPPPM <= 0 || config.TargetRTPPPM > ppmScale {
return xerr.New(xerr.InvalidArgument, "gift price or target RTP is invalid")
}
if config.PoolRatePPM < config.TargetRTPPPM || config.PoolRatePPM > ppmScale {
return xerr.New(xerr.InvalidArgument, "pool rate must be between target RTP and 100%")
}
if config.GlobalWindowDraws <= 0 || config.GiftWindowDraws <= 0 {
return xerr.New(xerr.InvalidArgument, "RTP window draws must be positive")
}
if config.NoviceDrawLimit < 0 || config.IntermediateDrawLimit < config.NoviceDrawLimit {
return xerr.New(xerr.InvalidArgument, "experience pool draw limits are invalid")
}
if config.HighMultiplier <= 0 || config.HighWaterPoolMultiple <= 0 {
return xerr.New(xerr.InvalidArgument, "high multiplier config is invalid")
}
if config.PlatformPoolWeightPPM < 0 || config.RoomPoolWeightPPM < 0 || config.GiftPoolWeightPPM < 0 ||
config.PlatformPoolWeightPPM+config.RoomPoolWeightPPM+config.GiftPoolWeightPPM != ppmScale {
return xerr.New(xerr.InvalidArgument, "pool weights must sum to 1000000")
}
if config.MaxSinglePayout <= 0 || config.UserHourlyPayoutCap <= 0 || config.UserDailyPayoutCap <= 0 ||
config.DeviceDailyPayoutCap <= 0 || config.RoomHourlyPayoutCap <= 0 || config.AnchorDailyPayoutCap <= 0 {
return xerr.New(xerr.InvalidArgument, "risk payout caps must be positive")
}
if len(config.Tiers) == 0 {
return xerr.New(xerr.InvalidArgument, "reward tiers are required")
}
for _, tier := range config.Tiers {
if tier.Pool == "" || tier.TierID == "" || tier.Weight < 0 || tier.RewardCoins < 0 {
return xerr.New(xerr.InvalidArgument, fmt.Sprintf("invalid reward tier: %s", tier.TierID))
}
}
return nil
}
func normalizeTiers(tiers []domain.Tier, giftPrice int64) []domain.Tier {
if len(tiers) == 0 {
return defaultTiers(giftPrice)
}
out := make([]domain.Tier, 0, len(tiers))
for _, tier := range tiers {
tier.Pool = strings.TrimSpace(tier.Pool)
tier.TierID = strings.TrimSpace(tier.TierID)
if tier.Pool == "" || tier.TierID == "" {
continue
}
out = append(out, tier)
}
return out
}
func defaultTiers(cost int64) []domain.Tier {
return []domain.Tier{
{Pool: domain.PoolNovice, TierID: "none", RewardCoins: 0, Weight: 720_000, Enabled: true},
{Pool: domain.PoolNovice, TierID: "novice_feedback_0_2x", RewardCoins: cost / 5, Weight: 90_000, Enabled: true},
{Pool: domain.PoolNovice, TierID: "novice_rebate_0_5x", RewardCoins: cost / 2, Weight: 70_000, Enabled: true},
{Pool: domain.PoolNovice, TierID: "novice_rebate_1x", RewardCoins: cost, Weight: 70_000, Enabled: true},
{Pool: domain.PoolNovice, TierID: "novice_small_2x", RewardCoins: cost * 2, Weight: 35_000, Enabled: true},
{Pool: domain.PoolNovice, TierID: "novice_small_5x", RewardCoins: cost * 5, Weight: 12_000, Enabled: true},
{Pool: domain.PoolNovice, TierID: "novice_medium_10x", RewardCoins: cost * 10, Weight: 2_500, Enabled: true},
{Pool: domain.PoolNovice, TierID: "novice_cap_20x", RewardCoins: cost * 20, Weight: 500, Enabled: true},
{Pool: domain.PoolIntermediate, TierID: "none", RewardCoins: 0, Weight: 830_000, Enabled: true},
{Pool: domain.PoolIntermediate, TierID: "inter_rebate_0_5x", RewardCoins: cost / 2, Weight: 25_000, Enabled: true},
{Pool: domain.PoolIntermediate, TierID: "inter_rebate_1x", RewardCoins: cost, Weight: 40_000, Enabled: true},
{Pool: domain.PoolIntermediate, TierID: "inter_small_2x", RewardCoins: cost * 2, Weight: 45_000, Enabled: true},
{Pool: domain.PoolIntermediate, TierID: "inter_medium_5x", RewardCoins: cost * 5, Weight: 35_000, Enabled: true},
{Pool: domain.PoolIntermediate, TierID: "inter_large_20x", RewardCoins: cost * 20, Weight: 20_000, HighWaterOnly: true, Enabled: true},
{Pool: domain.PoolIntermediate, TierID: "inter_large_50x", RewardCoins: cost * 50, Weight: 5_000, HighWaterOnly: true, Enabled: true},
{Pool: domain.PoolAdvanced, TierID: "none", RewardCoins: 0, Weight: 900_000, Enabled: true},
{Pool: domain.PoolAdvanced, TierID: "adv_small_2x", RewardCoins: cost * 2, Weight: 30_000, Enabled: true},
{Pool: domain.PoolAdvanced, TierID: "adv_medium_5x", RewardCoins: cost * 5, Weight: 30_000, Enabled: true},
{Pool: domain.PoolAdvanced, TierID: "adv_large_20x", RewardCoins: cost * 20, Weight: 30_000, HighWaterOnly: true, Enabled: true},
{Pool: domain.PoolAdvanced, TierID: "adv_large_100x", RewardCoins: cost * 100, Weight: 9_000, HighWaterOnly: true, Enabled: true},
{Pool: domain.PoolAdvanced, TierID: "adv_jackpot_500x", RewardCoins: cost * 500, Weight: 1_000, HighWaterOnly: true, Enabled: true},
}
}

View File

@ -0,0 +1,44 @@
package luckygift
import (
"testing"
domain "hyapp/services/activity-service/internal/domain/luckygift"
)
func TestDefaultConfigPassesValidation(t *testing.T) {
config := DefaultConfig("hyapp")
config.Enabled = true
if err := validateConfig(config); err != nil {
t.Fatalf("default lucky gift config should be publishable: %v", err)
}
}
func TestValidateConfigRejectsGiftScopedRules(t *testing.T) {
config := DefaultConfig("hyapp")
config.GiftID = "gift_500"
if err := validateConfig(config); err == nil {
t.Fatalf("expected gift scoped lucky gift config to be rejected")
}
}
func TestNormalizeTiersPreservesDisabledOperationalTiers(t *testing.T) {
tiers := normalizeTiers([]domain.Tier{
{
Pool: domain.PoolAdvanced,
TierID: "adv_jackpot_500x",
RewardCoins: 250_000,
Weight: 1_000,
Enabled: false,
},
}, 500)
if len(tiers) != 1 {
t.Fatalf("expected one normalized tier, got %d", len(tiers))
}
if tiers[0].Enabled {
t.Fatalf("disabled tier was re-enabled during normalization")
}
}

View File

@ -0,0 +1,119 @@
package luckygift
import (
"context"
"strings"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
domain "hyapp/services/activity-service/internal/domain/luckygift"
)
// Repository 是幸运礼物唯一持久化边界RTP、奖池、风控计数必须在同一个 MySQL 事务内修改。
type Repository interface {
GetLuckyGiftConfig(ctx context.Context) (domain.Config, bool, error)
UpsertLuckyGiftConfig(ctx context.Context, config domain.Config, nowMS int64) (domain.Config, error)
CheckLuckyGift(ctx context.Context, cmd domain.CheckCommand) (domain.CheckResult, error)
ExecuteLuckyGiftDraw(ctx context.Context, cmd domain.DrawCommand, nowMS int64) (domain.DrawResult, error)
ListLuckyGiftDraws(ctx context.Context, query domain.DrawQuery) ([]domain.DrawResult, int64, error)
}
// Service 承载幸运礼物查询、抽奖命令和后台规则配置。
type Service struct {
repository Repository
now func() time.Time
}
func New(repository Repository) *Service {
return &Service{repository: repository, now: time.Now}
}
func (s *Service) SetClock(now func() time.Time) {
if now != nil {
s.now = now
}
}
func (s *Service) Check(ctx context.Context, cmd domain.CheckCommand) (domain.CheckResult, error) {
if err := s.requireRepository(); err != nil {
return domain.CheckResult{}, err
}
cmd.GiftID = strings.TrimSpace(cmd.GiftID)
cmd.RoomID = strings.TrimSpace(cmd.RoomID)
if cmd.GiftID == "" || cmd.UserID <= 0 || cmd.RoomID == "" {
return domain.CheckResult{}, xerr.New(xerr.InvalidArgument, "lucky gift check command is incomplete")
}
return s.repository.CheckLuckyGift(ctx, cmd)
}
func (s *Service) Draw(ctx context.Context, cmd domain.DrawCommand) (domain.DrawResult, error) {
if err := s.requireRepository(); err != nil {
return domain.DrawResult{}, err
}
cmd.CommandID = strings.TrimSpace(cmd.CommandID)
cmd.GiftID = strings.TrimSpace(cmd.GiftID)
cmd.DeviceID = strings.TrimSpace(cmd.DeviceID)
cmd.RoomID = strings.TrimSpace(cmd.RoomID)
cmd.AnchorID = strings.TrimSpace(cmd.AnchorID)
if cmd.CommandID == "" || cmd.UserID <= 0 || cmd.DeviceID == "" || cmd.RoomID == "" || cmd.AnchorID == "" || cmd.GiftID == "" || cmd.CoinSpent <= 0 {
return domain.DrawResult{}, xerr.New(xerr.InvalidArgument, "lucky gift draw command is incomplete")
}
if cmd.PaidAtMS <= 0 {
cmd.PaidAtMS = s.now().UTC().UnixMilli()
}
return s.repository.ExecuteLuckyGiftDraw(ctx, cmd, s.now().UTC().UnixMilli())
}
func (s *Service) GetConfig(ctx context.Context) (domain.Config, error) {
if err := s.requireRepository(); err != nil {
return domain.Config{}, err
}
config, exists, err := s.repository.GetLuckyGiftConfig(ctx)
if err != nil {
return domain.Config{}, err
}
if !exists {
return DefaultConfig(appcode.FromContext(ctx)), nil
}
return config, nil
}
func (s *Service) UpsertConfig(ctx context.Context, config domain.Config) (domain.Config, error) {
if err := s.requireRepository(); err != nil {
return domain.Config{}, err
}
config.AppCode = appcode.FromContext(ctx)
config.GiftID = domain.GlobalConfigGiftID
config.Tiers = normalizeTiers(config.Tiers, config.GiftPrice)
if err := validateConfig(config); err != nil {
return domain.Config{}, err
}
return s.repository.UpsertLuckyGiftConfig(ctx, config, s.now().UTC().UnixMilli())
}
func (s *Service) ListDraws(ctx context.Context, query domain.DrawQuery) ([]domain.DrawResult, int64, error) {
if err := s.requireRepository(); err != nil {
return nil, 0, err
}
query.GiftID = strings.TrimSpace(query.GiftID)
query.RoomID = strings.TrimSpace(query.RoomID)
query.Status = strings.TrimSpace(query.Status)
if query.Page <= 0 {
query.Page = 1
}
if query.PageSize <= 0 {
query.PageSize = 20
}
if query.PageSize > 100 {
query.PageSize = 100
}
return s.repository.ListLuckyGiftDraws(ctx, query)
}
func (s *Service) requireRepository() error {
if s == nil || s.repository == nil {
return xerr.New(xerr.Unavailable, "lucky gift repository is not configured")
}
return nil
}

View File

@ -0,0 +1,67 @@
package luckygift
import (
"context"
"testing"
"hyapp/pkg/appcode"
domain "hyapp/services/activity-service/internal/domain/luckygift"
)
func TestGetConfigReturnsGlobalDefault(t *testing.T) {
repository := &fakeLuckyGiftRepository{}
service := New(repository)
config, err := service.GetConfig(appcode.WithContext(context.Background(), "hyapp"))
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.GiftID != domain.GlobalConfigGiftID {
t.Fatalf("expected global config scope, got %q", config.GiftID)
}
}
func TestUpsertConfigForcesGlobalScope(t *testing.T) {
repository := &fakeLuckyGiftRepository{}
service := New(repository)
config := DefaultConfig("hyapp")
config.GiftID = "gift_500"
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.GiftID != domain.GlobalConfigGiftID {
t.Fatalf("expected global config scope, got %q", repository.upserted.GiftID)
}
}
type fakeLuckyGiftRepository struct {
getConfigCalls int
upserted domain.Config
}
func (r *fakeLuckyGiftRepository) GetLuckyGiftConfig(context.Context) (domain.Config, bool, error) {
r.getConfigCalls++
return domain.Config{}, false, nil
}
func (r *fakeLuckyGiftRepository) UpsertLuckyGiftConfig(_ context.Context, config domain.Config, _ int64) (domain.Config, error) {
r.upserted = config
return config, nil
}
func (r *fakeLuckyGiftRepository) CheckLuckyGift(context.Context, domain.CheckCommand) (domain.CheckResult, error) {
return domain.CheckResult{}, nil
}
func (r *fakeLuckyGiftRepository) ExecuteLuckyGiftDraw(context.Context, domain.DrawCommand, int64) (domain.DrawResult, error) {
return domain.DrawResult{}, nil
}
func (r *fakeLuckyGiftRepository) ListLuckyGiftDraws(context.Context, domain.DrawQuery) ([]domain.DrawResult, int64, error) {
return nil, 0, nil
}

View File

@ -0,0 +1,403 @@
package sevendaycheckin
import (
"context"
"fmt"
"sort"
"strings"
"time"
"google.golang.org/grpc"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
domain "hyapp/services/activity-service/internal/domain/sevendaycheckin"
)
const (
grantSource = "seven_day_checkin"
grantReason = "seven_day_checkin"
)
// Repository 是七日签到的唯一持久化边界;连续天数和 claim 幂等都必须在 activity MySQL。
type Repository interface {
GetSevenDayCheckInConfig(ctx context.Context) (domain.Config, bool, error)
GetSevenDayCheckInRewardsForVersion(ctx context.Context, version int64) ([]domain.Reward, error)
UpdateSevenDayCheckInConfig(ctx context.Context, config domain.Config, nowMS int64) (domain.Config, error)
GetSevenDayCheckInAccount(ctx context.Context, userID int64) (domain.Account, bool, error)
GetSevenDayCheckInClaimByDay(ctx context.Context, userID int64, checkinDay string) (domain.Claim, bool, error)
ListSevenDayCheckInClaimsForUserCycle(ctx context.Context, userID int64, cycleNo int64) ([]domain.Claim, error)
PrepareSevenDayCheckInClaim(ctx context.Context, userID int64, commandID string, checkinDay string, yesterday string, nowMS int64) (domain.PrepareResult, error)
MarkSevenDayCheckInClaimGranted(ctx context.Context, claimID string, walletGrantID string, grantedAtMS int64) (domain.Claim, error)
MarkSevenDayCheckInClaimFailed(ctx context.Context, claimID string, failureReason string, nowMS int64) error
ListSevenDayCheckInClaims(ctx context.Context, query domain.ClaimQuery) ([]domain.Claim, int64, error)
}
// WalletClient 是签到奖励的唯一外部副作用;资源组展开和钱包资产入账由 wallet-service 保证原子。
type WalletClient interface {
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest, opts ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error)
}
// Service 承载七日签到 App 查询、签到命令和后台配置管理用例。
type Service struct {
repository Repository
wallet WalletClient
now func() time.Time
}
func New(repository Repository, wallet WalletClient) *Service {
return &Service{repository: repository, wallet: wallet, now: time.Now}
}
func (s *Service) SetClock(now func() time.Time) {
if now != nil {
s.now = now
}
}
// GetStatus 只读配置、用户账户和本轮 claims不扫描钱包流水或资源赠送记录。
func (s *Service) GetStatus(ctx context.Context, userID int64) (domain.StatusResult, error) {
if err := s.requireRepository(); err != nil {
return domain.StatusResult{}, err
}
if userID <= 0 {
return domain.StatusResult{}, xerr.New(xerr.InvalidArgument, "user_id is required")
}
now := s.now()
cycle := dailyCycleOf(now)
base := domain.StatusResult{
Reason: domain.ReasonNotConfigured,
CheckinDay: cycle.Key,
ServerTimeMS: now.UnixMilli(),
NextRefreshMS: cycle.NextRefreshMS,
}
config, exists, err := s.repository.GetSevenDayCheckInConfig(ctx)
if err != nil {
return domain.StatusResult{}, err
}
if !exists {
return base, nil
}
account, accountExists, err := s.repository.GetSevenDayCheckInAccount(ctx, userID)
if err != nil {
return domain.StatusResult{}, err
}
todayClaim, todaySigned, err := s.repository.GetSevenDayCheckInClaimByDay(ctx, userID, cycle.Key)
if err != nil {
return domain.StatusResult{}, err
}
next := nextSlot(account, accountExists, config.ConfigVersion, cycle.Key, cycle.YesterdayKey)
displayVersion := next.ConfigVersion
displayCycle := next.CycleNo
if todaySigned {
displayVersion = todayClaim.ConfigVersion
displayCycle = todayClaim.CycleNo
}
rewards, err := s.rewardsForVersion(ctx, config, displayVersion)
if err != nil {
return domain.StatusResult{}, err
}
claims, err := s.repository.ListSevenDayCheckInClaimsForUserCycle(ctx, userID, displayCycle)
if err != nil {
return domain.StatusResult{}, err
}
result := base
result.Enabled = config.Enabled
result.ConfigVersion = displayVersion
result.CycleNo = displayCycle
result.CurrentDayIndex = currentDayIndex(account, accountExists, cycle.Key, cycle.YesterdayKey)
result.NextDayIndex = next.DayIndex
result.LastCheckinDay = account.LastCheckinDay
result.Rewards = rewardStatuses(rewards, claims, result.NextDayIndex)
result.CanSign, result.Reason = signEligibility(config, exists, rewards, todayClaim, todaySigned)
return result, nil
}
// Sign 创建或复用当天签到 claim只有 wallet-service 资源组发放成功后才推进连续签到账号。
func (s *Service) Sign(ctx context.Context, userID int64, commandID string) (domain.Claim, error) {
if err := s.requireRepository(); err != nil {
return domain.Claim{}, err
}
commandID = strings.TrimSpace(commandID)
if userID <= 0 || commandID == "" {
return domain.Claim{}, xerr.New(xerr.InvalidArgument, "checkin command is incomplete")
}
if s.wallet == nil {
return domain.Claim{}, xerr.New(xerr.Unavailable, "wallet client is not configured")
}
now := s.now()
cycle := dailyCycleOf(now)
prepared, err := s.repository.PrepareSevenDayCheckInClaim(ctx, userID, commandID, cycle.Key, cycle.YesterdayKey, now.UnixMilli())
if err != nil {
return domain.Claim{}, err
}
if !prepared.Prepared {
if prepared.Claim.ClaimID != "" {
return prepared.Claim, nil
}
return domain.Claim{}, checkinUnavailableError(prepared.Reason)
}
if prepared.Claim.Status == domain.StatusGranted {
return prepared.Claim, nil
}
claim := prepared.Claim
resp, err := s.wallet.GrantResourceGroup(ctx, &walletv1.GrantResourceGroupRequest{
CommandId: claim.WalletCommandID,
AppCode: appcode.FromContext(ctx),
TargetUserId: claim.UserID,
GroupId: claim.ResourceGroupID,
Reason: grantReason,
OperatorUserId: claim.UserID,
GrantSource: grantSource,
})
if err != nil {
// 账户不在失败分支推进;同一 command_id 重试会复用 claim 和 wallet_command_id。
_ = s.repository.MarkSevenDayCheckInClaimFailed(ctx, claim.ClaimID, xerr.MessageOf(err), s.now().UnixMilli())
return domain.Claim{}, err
}
walletGrantID := ""
if resp.GetGrant() != nil {
walletGrantID = resp.GetGrant().GetGrantId()
}
return s.repository.MarkSevenDayCheckInClaimGranted(ctx, claim.ClaimID, walletGrantID, s.now().UnixMilli())
}
func (s *Service) GetConfig(ctx context.Context) (domain.Config, error) {
if err := s.requireRepository(); err != nil {
return domain.Config{}, err
}
config, exists, err := s.repository.GetSevenDayCheckInConfig(ctx)
if err != nil {
return domain.Config{}, err
}
if !exists {
return domain.Config{AppCode: appcode.FromContext(ctx), Rewards: defaultRewards()}, nil
}
return config, nil
}
func (s *Service) UpdateConfig(ctx context.Context, config domain.Config) (domain.Config, error) {
if err := s.requireRepository(); err != nil {
return domain.Config{}, err
}
config.AppCode = appcode.FromContext(ctx)
config.Rewards = normalizeRewards(config.Rewards)
if err := validateConfig(config); err != nil {
return domain.Config{}, err
}
return s.repository.UpdateSevenDayCheckInConfig(ctx, config, s.now().UnixMilli())
}
func (s *Service) ListClaims(ctx context.Context, query domain.ClaimQuery) ([]domain.Claim, int64, error) {
if err := s.requireRepository(); err != nil {
return nil, 0, err
}
query.Status = strings.TrimSpace(query.Status)
query.CheckinDay = strings.TrimSpace(query.CheckinDay)
if query.DayIndex < 0 || query.DayIndex > domain.DayCount {
return nil, 0, xerr.New(xerr.InvalidArgument, "day_index is invalid")
}
if query.Page <= 0 {
query.Page = 1
}
if query.PageSize <= 0 {
query.PageSize = 20
}
if query.PageSize > 100 {
query.PageSize = 100
}
return s.repository.ListSevenDayCheckInClaims(ctx, query)
}
func (s *Service) requireRepository() error {
if s == nil || s.repository == nil {
return xerr.New(xerr.Unavailable, "seven day checkin repository is not configured")
}
return nil
}
func (s *Service) rewardsForVersion(ctx context.Context, config domain.Config, version int64) ([]domain.Reward, error) {
if version == config.ConfigVersion {
return config.Rewards, nil
}
rewards, err := s.repository.GetSevenDayCheckInRewardsForVersion(ctx, version)
if err != nil {
return nil, err
}
return rewards, nil
}
type dailyCycle struct {
Key string
YesterdayKey string
NextRefreshMS int64
}
func dailyCycleOf(t time.Time) dailyCycle {
utc := t.UTC()
start := time.Date(utc.Year(), utc.Month(), utc.Day(), 0, 0, 0, 0, time.UTC)
return dailyCycle{
Key: start.Format("2006-01-02"),
YesterdayKey: start.AddDate(0, 0, -1).Format("2006-01-02"),
NextRefreshMS: start.AddDate(0, 0, 1).UnixMilli(),
}
}
type slot struct {
CycleNo int64
DayIndex int32
ConfigVersion int64
}
func nextSlot(account domain.Account, exists bool, currentVersion int64, today string, yesterday string) slot {
if !exists || account.UserID <= 0 {
return slot{CycleNo: 1, DayIndex: 1, ConfigVersion: currentVersion}
}
if account.LastCheckinDay == today {
return slot{CycleNo: account.CycleNo, DayIndex: account.LastDayIndex, ConfigVersion: account.ConfigVersion}
}
if account.LastCheckinDay == yesterday && account.LastDayIndex > 0 && account.LastDayIndex < domain.DayCount {
return slot{CycleNo: account.CycleNo, DayIndex: account.LastDayIndex + 1, ConfigVersion: account.ConfigVersion}
}
nextCycle := account.CycleNo + 1
if nextCycle <= 0 {
nextCycle = 1
}
return slot{CycleNo: nextCycle, DayIndex: 1, ConfigVersion: currentVersion}
}
func currentDayIndex(account domain.Account, exists bool, today string, yesterday string) int32 {
if !exists || account.UserID <= 0 {
return 0
}
if account.LastCheckinDay == today || account.LastCheckinDay == yesterday {
return account.LastDayIndex
}
return 0
}
func signEligibility(config domain.Config, exists bool, rewards []domain.Reward, todayClaim domain.Claim, todaySigned bool) (bool, string) {
if !exists || len(rewards) != domain.DayCount {
return false, domain.ReasonNotConfigured
}
if !config.Enabled {
return false, domain.ReasonDisabled
}
if !validRewards(rewards) {
return false, domain.ReasonInvalidConfig
}
if todaySigned {
switch todayClaim.Status {
case domain.StatusGranted:
return false, domain.ReasonAlreadySigned
case domain.StatusPending, domain.StatusFailed:
return false, domain.ReasonPendingReward
default:
return false, domain.ReasonAlreadySigned
}
}
return true, domain.ReasonEligible
}
func rewardStatuses(rewards []domain.Reward, claims []domain.Claim, nextDay int32) []domain.RewardStatus {
claimsByDay := make(map[int32]domain.Claim, len(claims))
for _, claim := range claims {
claimsByDay[claim.DayIndex] = claim
}
items := make([]domain.RewardStatus, 0, len(rewards))
for _, reward := range rewards {
status := domain.RewardStatusLocked
if reward.DayIndex == nextDay {
status = domain.RewardStatusAvailable
}
item := domain.RewardStatus{Reward: reward, Status: status}
if claim, ok := claimsByDay[reward.DayIndex]; ok {
item.ClaimID = claim.ClaimID
item.WalletGrantID = claim.WalletGrantID
item.GrantedAtMS = claim.GrantedAtMS
switch claim.Status {
case domain.StatusGranted:
item.Status = domain.RewardStatusGranted
case domain.StatusPending:
item.Status = domain.RewardStatusPending
case domain.StatusFailed:
item.Status = domain.RewardStatusFailed
}
}
items = append(items, item)
}
sort.SliceStable(items, func(i, j int) bool { return items[i].DayIndex < items[j].DayIndex })
return items
}
func normalizeRewards(rewards []domain.Reward) []domain.Reward {
items := make([]domain.Reward, 0, len(rewards))
for _, reward := range rewards {
items = append(items, domain.Reward{DayIndex: reward.DayIndex, ResourceGroupID: reward.ResourceGroupID})
}
sort.SliceStable(items, func(i, j int) bool { return items[i].DayIndex < items[j].DayIndex })
return items
}
func validateConfig(config domain.Config) error {
if config.UpdatedByAdminID <= 0 {
return xerr.New(xerr.InvalidArgument, "operator_admin_id is required")
}
if len(config.Rewards) != domain.DayCount {
return xerr.New(xerr.InvalidArgument, "seven rewards are required")
}
seen := make(map[int32]struct{}, domain.DayCount)
for _, reward := range config.Rewards {
if reward.DayIndex < 1 || reward.DayIndex > domain.DayCount || reward.ResourceGroupID <= 0 {
return xerr.New(xerr.InvalidArgument, "checkin reward is invalid")
}
if _, ok := seen[reward.DayIndex]; ok {
return xerr.New(xerr.InvalidArgument, "checkin reward day is duplicated")
}
seen[reward.DayIndex] = struct{}{}
}
return nil
}
func validRewards(rewards []domain.Reward) bool {
if len(rewards) != domain.DayCount {
return false
}
seen := make(map[int32]struct{}, domain.DayCount)
for _, reward := range rewards {
if reward.DayIndex < 1 || reward.DayIndex > domain.DayCount || reward.ResourceGroupID <= 0 {
return false
}
if _, ok := seen[reward.DayIndex]; ok {
return false
}
seen[reward.DayIndex] = struct{}{}
}
return len(seen) == domain.DayCount
}
func checkinUnavailableError(reason string) error {
switch reason {
case domain.ReasonInvalidConfig:
return xerr.New(xerr.InvalidArgument, "seven day checkin rewards are invalid")
case domain.ReasonDisabled:
return xerr.New(xerr.RuleNotActive, "seven day checkin is disabled")
default:
return xerr.New(xerr.RuleNotActive, "seven day checkin is not configured")
}
}
func defaultRewards() []domain.Reward {
rewards := make([]domain.Reward, 0, domain.DayCount)
for day := int32(1); day <= domain.DayCount; day++ {
rewards = append(rewards, domain.Reward{DayIndex: day})
}
return rewards
}
func WalletCommandID(claimID string) string {
return fmt.Sprintf("wcheckin_%s", claimID)
}

View File

@ -0,0 +1,119 @@
package sevendaycheckin_test
import (
"context"
"testing"
"time"
"google.golang.org/grpc"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
domain "hyapp/services/activity-service/internal/domain/sevendaycheckin"
service "hyapp/services/activity-service/internal/service/sevendaycheckin"
"hyapp/services/activity-service/internal/testutil/mysqltest"
)
func TestSevenDayCheckInClaimCycleAndIdempotency(t *testing.T) {
repository := mysqltest.NewRepository(t)
wallet := &fakeCheckInWallet{}
svc := service.New(repository, wallet)
ctx := appcode.WithContext(context.Background(), "lalu")
now := time.Date(2026, 5, 16, 10, 0, 0, 0, time.FixedZone("CST", 8*60*60))
svc.SetClock(func() time.Time { return now })
if _, err := svc.UpdateConfig(ctx, domain.Config{
Enabled: true,
Rewards: checkInRewards(7000),
UpdatedByAdminID: 90001,
}); err != nil {
t.Fatalf("UpdateConfig failed: %v", err)
}
first, err := svc.Sign(ctx, 10001, "cmd-checkin-1")
if err != nil {
t.Fatalf("first Sign failed: %v", err)
}
if first.Status != domain.StatusGranted || first.DayIndex != 1 || first.ResourceGroupID != 7001 || first.WalletGrantID != "grant-1" {
t.Fatalf("first claim mismatch: %+v", first)
}
again, err := svc.Sign(ctx, 10001, "cmd-checkin-1")
if err != nil {
t.Fatalf("retry Sign failed: %v", err)
}
if again.ClaimID != first.ClaimID || wallet.calls != 1 {
t.Fatalf("idempotency mismatch: first=%+v again=%+v calls=%d", first, again, wallet.calls)
}
status, err := svc.GetStatus(ctx, 10001)
if err != nil {
t.Fatalf("GetStatus failed: %v", err)
}
if status.CanSign || status.Reason != domain.ReasonAlreadySigned || status.CurrentDayIndex != 1 || status.Rewards[0].Status != domain.RewardStatusGranted {
t.Fatalf("signed status mismatch: %+v", status)
}
now = now.AddDate(0, 0, 1)
second, err := svc.Sign(ctx, 10001, "cmd-checkin-2")
if err != nil {
t.Fatalf("second Sign failed: %v", err)
}
if second.CycleNo != 1 || second.DayIndex != 2 || second.ResourceGroupID != 7002 {
t.Fatalf("second claim mismatch: %+v", second)
}
now = now.AddDate(0, 0, 2)
reset, err := svc.Sign(ctx, 10001, "cmd-checkin-reset")
if err != nil {
t.Fatalf("reset Sign failed: %v", err)
}
if reset.CycleNo != 2 || reset.DayIndex != 1 || reset.ResourceGroupID != 7001 {
t.Fatalf("reset claim mismatch: %+v", reset)
}
}
func TestSevenDayCheckInKeepsCycleConfigVersion(t *testing.T) {
repository := mysqltest.NewRepository(t)
wallet := &fakeCheckInWallet{}
svc := service.New(repository, wallet)
ctx := appcode.WithContext(context.Background(), "lalu")
now := time.Date(2026, 5, 16, 0, 0, 0, 0, time.UTC)
svc.SetClock(func() time.Time { return now })
if _, err := svc.UpdateConfig(ctx, domain.Config{Enabled: true, Rewards: checkInRewards(1000), UpdatedByAdminID: 90001}); err != nil {
t.Fatalf("seed config v1 failed: %v", err)
}
first, err := svc.Sign(ctx, 20001, "cmd-version-1")
if err != nil {
t.Fatalf("sign v1 failed: %v", err)
}
if _, err := svc.UpdateConfig(ctx, domain.Config{Enabled: true, Rewards: checkInRewards(2000), UpdatedByAdminID: 90001}); err != nil {
t.Fatalf("update config v2 failed: %v", err)
}
now = now.AddDate(0, 0, 1)
second, err := svc.Sign(ctx, 20001, "cmd-version-2")
if err != nil {
t.Fatalf("sign v1 day 2 failed: %v", err)
}
if first.ConfigVersion != second.ConfigVersion || second.ResourceGroupID != 1002 {
t.Fatalf("cycle should keep original config: first=%+v second=%+v", first, second)
}
}
func checkInRewards(base int64) []domain.Reward {
rewards := make([]domain.Reward, 0, domain.DayCount)
for day := int32(1); day <= domain.DayCount; day++ {
rewards = append(rewards, domain.Reward{DayIndex: day, ResourceGroupID: base + int64(day)})
}
return rewards
}
type fakeCheckInWallet struct {
calls int
}
func (f *fakeCheckInWallet) GrantResourceGroup(context.Context, *walletv1.GrantResourceGroupRequest, ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
f.calls++
return &walletv1.ResourceGrantResponse{
Grant: &walletv1.ResourceGrant{GrantId: "grant-1"},
}, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
package mysql
import (
"testing"
domain "hyapp/services/activity-service/internal/domain/luckygift"
)
func TestLuckyRuntimeConfigScalesTierRewardsByActualSpend(t *testing.T) {
config := domain.Config{
GiftID: domain.GlobalConfigGiftID,
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 != 500_000 || runtimeConfig.UserHourlyPayoutCap != 1_000_000 {
t.Fatalf("expected risk caps to scale by spend, got single=%d hourly=%d", runtimeConfig.MaxSinglePayout, runtimeConfig.UserHourlyPayoutCap)
}
if config.Tiers[0].RewardCoins != 50_000 {
t.Fatalf("runtime scaling mutated source config")
}
}
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)
}
}

View File

@ -0,0 +1,742 @@
package mysql
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"hyapp/pkg/appcode"
"hyapp/pkg/idgen"
"hyapp/pkg/xerr"
domain "hyapp/services/activity-service/internal/domain/sevendaycheckin"
)
type checkinRewardSnapshot struct {
Rewards []checkinRewardSnapshotItem `json:"rewards"`
}
type checkinRewardSnapshotItem struct {
DayIndex int32 `json:"day_index"`
ResourceGroupID int64 `json:"resource_group_id"`
}
// GetSevenDayCheckInConfig 读取七日签到当前配置;未配置是后台首屏的正常状态。
func (r *Repository) GetSevenDayCheckInConfig(ctx context.Context) (domain.Config, bool, error) {
if r == nil || r.db == nil {
return domain.Config{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
config, exists, err := r.getSevenDayCheckInConfig(ctx, r.db)
if err != nil || !exists {
return config, exists, err
}
rewards, err := r.listSevenDayCheckInCurrentRewards(ctx, r.db)
if err != nil {
return domain.Config{}, false, err
}
config.Rewards = rewards
return config, true, nil
}
// GetSevenDayCheckInRewardsForVersion 从版本快照读取旧轮次奖励,保证用户一轮内配置稳定。
func (r *Repository) GetSevenDayCheckInRewardsForVersion(ctx context.Context, version int64) ([]domain.Reward, error) {
if r == nil || r.db == nil {
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
var snapshot string
err := r.db.QueryRowContext(ctx, `
SELECT COALESCE(CAST(snapshot_json AS CHAR), '')
FROM seven_day_checkin_config_versions
WHERE app_code = ? AND config_version = ?`,
appcode.FromContext(ctx), version,
).Scan(&snapshot)
if errors.Is(err, sql.ErrNoRows) {
return nil, xerr.New(xerr.NotFound, "seven day checkin config version not found")
}
if err != nil {
return nil, err
}
return rewardsFromSnapshot(snapshot)
}
// UpdateSevenDayCheckInConfig 原子替换当前 7 天奖励,并写入不可变版本快照。
func (r *Repository) UpdateSevenDayCheckInConfig(ctx context.Context, config domain.Config, nowMS int64) (domain.Config, error) {
if r == nil || r.db == nil {
return domain.Config{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return domain.Config{}, err
}
defer func() { _ = tx.Rollback() }()
current, exists, err := r.getSevenDayCheckInConfigForUpdate(ctx, tx)
if err != nil {
return domain.Config{}, err
}
nextVersion := int64(1)
createdAt := nowMS
if exists {
nextVersion = current.ConfigVersion + 1
createdAt = current.CreatedAtMS
}
snapshot, err := rewardsSnapshot(config.Rewards)
if err != nil {
return domain.Config{}, err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO seven_day_checkin_configs (
app_code, enabled, config_version, updated_by_admin_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
enabled = VALUES(enabled),
config_version = VALUES(config_version),
updated_by_admin_id = VALUES(updated_by_admin_id),
updated_at_ms = VALUES(updated_at_ms)`,
appcode.FromContext(ctx), config.Enabled, nextVersion, config.UpdatedByAdminID, createdAt, nowMS,
); err != nil {
return domain.Config{}, err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM seven_day_checkin_rewards WHERE app_code = ?`, appcode.FromContext(ctx)); err != nil {
return domain.Config{}, err
}
for _, reward := range config.Rewards {
if _, err := tx.ExecContext(ctx, `
INSERT INTO seven_day_checkin_rewards (
app_code, day_index, config_version, resource_group_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?)`,
appcode.FromContext(ctx), reward.DayIndex, nextVersion, reward.ResourceGroupID, nowMS, nowMS,
); err != nil {
return domain.Config{}, err
}
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO seven_day_checkin_config_versions (
app_code, config_version, enabled, snapshot_json, created_by_admin_id, created_at_ms
) VALUES (?, ?, ?, ?, ?, ?)`,
appcode.FromContext(ctx), nextVersion, config.Enabled, snapshot, config.UpdatedByAdminID, nowMS,
); err != nil {
return domain.Config{}, err
}
if err := tx.Commit(); err != nil {
return domain.Config{}, err
}
updated, exists, err := r.GetSevenDayCheckInConfig(ctx)
if err != nil {
return domain.Config{}, err
}
if !exists {
return domain.Config{}, xerr.New(xerr.NotFound, "seven day checkin config not found")
}
return updated, nil
}
func (r *Repository) GetSevenDayCheckInAccount(ctx context.Context, userID int64) (domain.Account, bool, error) {
if r == nil || r.db == nil {
return domain.Account{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
row := r.db.QueryRowContext(ctx, checkinAccountSelectSQL()+`
WHERE app_code = ? AND user_id = ?`,
appcode.FromContext(ctx), userID,
)
account, err := scanCheckinAccount(row)
if errors.Is(err, sql.ErrNoRows) {
return domain.Account{}, false, nil
}
return account, err == nil, err
}
func (r *Repository) GetSevenDayCheckInClaimByDay(ctx context.Context, userID int64, checkinDay string) (domain.Claim, bool, error) {
if r == nil || r.db == nil {
return domain.Claim{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
row := r.db.QueryRowContext(ctx, checkinClaimSelectSQL()+`
WHERE app_code = ? AND user_id = ? AND checkin_day = ?`,
appcode.FromContext(ctx), userID, checkinDay,
)
claim, err := scanCheckinClaim(row)
if errors.Is(err, sql.ErrNoRows) {
return domain.Claim{}, false, nil
}
return claim, err == nil, err
}
func (r *Repository) ListSevenDayCheckInClaimsForUserCycle(ctx context.Context, userID int64, cycleNo int64) ([]domain.Claim, error) {
if r == nil || r.db == nil {
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
rows, err := r.db.QueryContext(ctx, checkinClaimSelectSQL()+`
WHERE app_code = ? AND user_id = ? AND cycle_no = ?
ORDER BY day_index ASC`,
appcode.FromContext(ctx), userID, cycleNo,
)
if err != nil {
return nil, err
}
defer rows.Close()
claims, _, err := scanCheckinClaims(rows, 0)
return claims, err
}
// PrepareSevenDayCheckInClaim 锁定用户账户并创建/复用 pending claim账户只在 grant 成功后推进。
func (r *Repository) PrepareSevenDayCheckInClaim(ctx context.Context, userID int64, commandID string, checkinDay string, yesterday string, nowMS int64) (domain.PrepareResult, error) {
if r == nil || r.db == nil {
return domain.PrepareResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return domain.PrepareResult{}, err
}
defer func() { _ = tx.Rollback() }()
if existing, exists, err := r.getCheckinClaimByCommandForUpdate(ctx, tx, commandID); err != nil || exists {
if err != nil {
return domain.PrepareResult{}, err
}
if existing.UserID != userID {
return domain.PrepareResult{}, xerr.New(xerr.IdempotencyConflict, "checkin command payload conflicts")
}
if existing.Status == domain.StatusFailed {
existing.Status = domain.StatusPending
existing.FailureReason = ""
existing.UpdatedAtMS = nowMS
if err := r.updateCheckinClaimPending(ctx, tx, existing); err != nil {
return domain.PrepareResult{}, err
}
}
if err := tx.Commit(); err != nil {
return domain.PrepareResult{}, err
}
return domain.PrepareResult{Claim: existing, Prepared: existing.Status != domain.StatusGranted}, nil
}
if existing, exists, err := r.getCheckinClaimByUserDayForUpdate(ctx, tx, userID, checkinDay); err != nil || exists {
if err != nil {
return domain.PrepareResult{}, err
}
if err := tx.Commit(); err != nil {
return domain.PrepareResult{}, err
}
return domain.PrepareResult{Claim: existing, Reason: domain.ReasonAlreadySigned}, nil
}
config, exists, err := r.getSevenDayCheckInConfigForUpdate(ctx, tx)
if err != nil {
return domain.PrepareResult{}, err
}
if !exists {
return domain.PrepareResult{Reason: domain.ReasonNotConfigured}, tx.Commit()
}
config.Rewards, err = r.listSevenDayCheckInCurrentRewards(ctx, tx)
if err != nil {
return domain.PrepareResult{}, err
}
if !config.Enabled {
return domain.PrepareResult{Reason: domain.ReasonDisabled}, tx.Commit()
}
if !validCheckinRewards(config.Rewards) {
return domain.PrepareResult{Reason: domain.ReasonInvalidConfig}, tx.Commit()
}
account, accountExists, err := r.getCheckinAccountForUpdate(ctx, tx, userID)
if err != nil {
return domain.PrepareResult{}, err
}
if accountExists && account.LastCheckinDay == checkinDay {
return domain.PrepareResult{Reason: domain.ReasonAlreadySigned}, tx.Commit()
}
slot := nextCheckinSlot(account, accountExists, config.ConfigVersion, checkinDay, yesterday)
rewards := config.Rewards
if slot.ConfigVersion != config.ConfigVersion {
rewards, err = r.getSevenDayCheckInRewardsForVersionTx(ctx, tx, slot.ConfigVersion)
if err != nil {
return domain.PrepareResult{}, err
}
}
resourceGroupID := rewardGroupID(rewards, slot.DayIndex)
if resourceGroupID <= 0 {
return domain.PrepareResult{Reason: domain.ReasonInvalidConfig}, tx.Commit()
}
claim := domain.Claim{
ClaimID: idgen.New("checkin"),
CommandID: commandID,
UserID: userID,
CycleNo: slot.CycleNo,
CheckinDay: checkinDay,
DayIndex: slot.DayIndex,
ConfigVersion: slot.ConfigVersion,
ResourceGroupID: resourceGroupID,
Status: domain.StatusPending,
SignedAtMS: nowMS,
CreatedAtMS: nowMS,
UpdatedAtMS: nowMS,
}
claim.WalletCommandID = walletCheckinCommandID(claim.ClaimID)
if err := r.insertCheckinClaim(ctx, tx, claim); err != nil {
if isMySQLDuplicate(err) {
return domain.PrepareResult{}, xerr.New(xerr.Conflict, "checkin claim already exists")
}
return domain.PrepareResult{}, err
}
if err := tx.Commit(); err != nil {
return domain.PrepareResult{}, err
}
return domain.PrepareResult{Claim: claim, Prepared: true}, nil
}
func (r *Repository) MarkSevenDayCheckInClaimGranted(ctx context.Context, claimID string, walletGrantID string, grantedAtMS int64) (domain.Claim, error) {
if r == nil || r.db == nil {
return domain.Claim{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return domain.Claim{}, err
}
defer func() { _ = tx.Rollback() }()
claim, err := r.getCheckinClaimByIDForUpdate(ctx, tx, claimID)
if err != nil {
return domain.Claim{}, err
}
if claim.Status == domain.StatusGranted {
if err := tx.Commit(); err != nil {
return domain.Claim{}, err
}
return claim, nil
}
if _, err := tx.ExecContext(ctx, `
UPDATE seven_day_checkin_claims
SET status = 'granted',
wallet_grant_id = ?,
failure_reason = '',
granted_at_ms = ?,
updated_at_ms = ?
WHERE app_code = ? AND claim_id = ?`,
walletGrantID, grantedAtMS, grantedAtMS, appcode.FromContext(ctx), claimID,
); err != nil {
return domain.Claim{}, err
}
if err := r.upsertCheckinAccountFromClaim(ctx, tx, claim, grantedAtMS); err != nil {
return domain.Claim{}, err
}
if err := tx.Commit(); err != nil {
return domain.Claim{}, err
}
claim.Status = domain.StatusGranted
claim.WalletGrantID = walletGrantID
claim.FailureReason = ""
claim.GrantedAtMS = grantedAtMS
claim.UpdatedAtMS = grantedAtMS
return claim, nil
}
func (r *Repository) MarkSevenDayCheckInClaimFailed(ctx context.Context, claimID string, failureReason string, nowMS int64) error {
if r == nil || r.db == nil {
return xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
claim, err := r.getCheckinClaimByIDForUpdate(ctx, tx, claimID)
if err != nil {
return err
}
if claim.Status == domain.StatusGranted {
return tx.Commit()
}
if _, err := tx.ExecContext(ctx, `
UPDATE seven_day_checkin_claims
SET status = 'failed', failure_reason = ?, updated_at_ms = ?
WHERE app_code = ? AND claim_id = ?`,
truncateTaskFailure(failureReason), nowMS, appcode.FromContext(ctx), claimID,
); err != nil {
return err
}
return tx.Commit()
}
func (r *Repository) ListSevenDayCheckInClaims(ctx context.Context, query domain.ClaimQuery) ([]domain.Claim, int64, error) {
if r == nil || r.db == nil {
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
where, args := checkinClaimWhere(ctx, query)
var total int64
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM seven_day_checkin_claims `+where, args...).Scan(&total); err != nil {
return nil, 0, err
}
pageSize := normalizePageSize(query.PageSize)
page := query.Page
if page <= 0 {
page = 1
}
args = append(args, pageSize, (page-1)*pageSize)
rows, err := r.db.QueryContext(ctx, checkinClaimSelectSQL()+`
`+where+`
ORDER BY created_at_ms DESC, claim_id DESC
LIMIT ? OFFSET ?`, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
return scanCheckinClaims(rows, total)
}
func (r *Repository) getSevenDayCheckInConfig(ctx context.Context, q queryer) (domain.Config, bool, error) {
row := q.QueryRowContext(ctx, checkinConfigSelectSQL()+` WHERE app_code = ?`, appcode.FromContext(ctx))
config, err := scanCheckinConfig(row)
if errors.Is(err, sql.ErrNoRows) {
return domain.Config{}, false, nil
}
return config, err == nil, err
}
func (r *Repository) getSevenDayCheckInConfigForUpdate(ctx context.Context, tx *sql.Tx) (domain.Config, bool, error) {
row := tx.QueryRowContext(ctx, checkinConfigSelectSQL()+` WHERE app_code = ? FOR UPDATE`, appcode.FromContext(ctx))
config, err := scanCheckinConfig(row)
if errors.Is(err, sql.ErrNoRows) {
return domain.Config{}, false, nil
}
return config, err == nil, err
}
func (r *Repository) listSevenDayCheckInCurrentRewards(ctx context.Context, q queryer) ([]domain.Reward, error) {
rows, err := q.QueryContext(ctx, `
SELECT day_index, resource_group_id
FROM seven_day_checkin_rewards
WHERE app_code = ?
ORDER BY day_index ASC`,
appcode.FromContext(ctx),
)
if err != nil {
return nil, err
}
defer rows.Close()
return scanCheckinRewards(rows)
}
func (r *Repository) getSevenDayCheckInRewardsForVersionTx(ctx context.Context, tx *sql.Tx, version int64) ([]domain.Reward, error) {
var snapshot string
err := tx.QueryRowContext(ctx, `
SELECT COALESCE(CAST(snapshot_json AS CHAR), '')
FROM seven_day_checkin_config_versions
WHERE app_code = ? AND config_version = ?`,
appcode.FromContext(ctx), version,
).Scan(&snapshot)
if errors.Is(err, sql.ErrNoRows) {
return nil, xerr.New(xerr.NotFound, "seven day checkin config version not found")
}
if err != nil {
return nil, err
}
return rewardsFromSnapshot(snapshot)
}
func (r *Repository) getCheckinAccountForUpdate(ctx context.Context, tx *sql.Tx, userID int64) (domain.Account, bool, error) {
row := tx.QueryRowContext(ctx, checkinAccountSelectSQL()+`
WHERE app_code = ? AND user_id = ?
FOR UPDATE`,
appcode.FromContext(ctx), userID,
)
account, err := scanCheckinAccount(row)
if errors.Is(err, sql.ErrNoRows) {
return domain.Account{}, false, nil
}
return account, err == nil, err
}
func (r *Repository) getCheckinClaimByCommandForUpdate(ctx context.Context, tx *sql.Tx, commandID string) (domain.Claim, bool, error) {
row := tx.QueryRowContext(ctx, checkinClaimSelectSQL()+`
WHERE app_code = ? AND command_id = ?
FOR UPDATE`,
appcode.FromContext(ctx), commandID,
)
claim, err := scanCheckinClaim(row)
if errors.Is(err, sql.ErrNoRows) {
return domain.Claim{}, false, nil
}
return claim, err == nil, err
}
func (r *Repository) getCheckinClaimByUserDayForUpdate(ctx context.Context, tx *sql.Tx, userID int64, checkinDay string) (domain.Claim, bool, error) {
row := tx.QueryRowContext(ctx, checkinClaimSelectSQL()+`
WHERE app_code = ? AND user_id = ? AND checkin_day = ?
FOR UPDATE`,
appcode.FromContext(ctx), userID, checkinDay,
)
claim, err := scanCheckinClaim(row)
if errors.Is(err, sql.ErrNoRows) {
return domain.Claim{}, false, nil
}
return claim, err == nil, err
}
func (r *Repository) getCheckinClaimByIDForUpdate(ctx context.Context, tx *sql.Tx, claimID string) (domain.Claim, error) {
row := tx.QueryRowContext(ctx, checkinClaimSelectSQL()+`
WHERE app_code = ? AND claim_id = ?
FOR UPDATE`,
appcode.FromContext(ctx), claimID,
)
claim, err := scanCheckinClaim(row)
if errors.Is(err, sql.ErrNoRows) {
return domain.Claim{}, xerr.New(xerr.NotFound, "seven day checkin claim not found")
}
return claim, err
}
func (r *Repository) insertCheckinClaim(ctx context.Context, tx *sql.Tx, claim domain.Claim) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO seven_day_checkin_claims (
app_code, claim_id, command_id, user_id, cycle_no, checkin_day, day_index,
config_version, resource_group_id, wallet_command_id, wallet_grant_id,
status, failure_reason, signed_at_ms, granted_at_ms, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, '', ?, 0, ?, ?)`,
appcode.FromContext(ctx), claim.ClaimID, claim.CommandID, claim.UserID, claim.CycleNo,
claim.CheckinDay, claim.DayIndex, claim.ConfigVersion, claim.ResourceGroupID,
claim.WalletCommandID, claim.Status, claim.SignedAtMS, claim.CreatedAtMS, claim.UpdatedAtMS,
)
return err
}
func (r *Repository) updateCheckinClaimPending(ctx context.Context, tx *sql.Tx, claim domain.Claim) error {
_, err := tx.ExecContext(ctx, `
UPDATE seven_day_checkin_claims
SET status = 'pending',
wallet_grant_id = '',
failure_reason = '',
granted_at_ms = 0,
updated_at_ms = ?
WHERE app_code = ? AND claim_id = ?`,
claim.UpdatedAtMS, appcode.FromContext(ctx), claim.ClaimID,
)
return err
}
func (r *Repository) upsertCheckinAccountFromClaim(ctx context.Context, tx *sql.Tx, claim domain.Claim, nowMS int64) error {
account, exists, err := r.getCheckinAccountForUpdate(ctx, tx, claim.UserID)
if err != nil {
return err
}
cycleStartDay := claim.CheckinDay
if exists && account.CycleNo == claim.CycleNo && account.CycleStartDay != "" && claim.DayIndex > 1 {
cycleStartDay = account.CycleStartDay
}
_, err = tx.ExecContext(ctx, `
INSERT INTO user_seven_day_checkin_accounts (
app_code, user_id, cycle_no, cycle_start_day, last_checkin_day,
last_day_index, config_version, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
cycle_no = VALUES(cycle_no),
cycle_start_day = VALUES(cycle_start_day),
last_checkin_day = VALUES(last_checkin_day),
last_day_index = VALUES(last_day_index),
config_version = VALUES(config_version),
updated_at_ms = VALUES(updated_at_ms)`,
appcode.FromContext(ctx), claim.UserID, claim.CycleNo, cycleStartDay, claim.CheckinDay,
claim.DayIndex, claim.ConfigVersion, nowMS,
)
return err
}
func scanCheckinConfig(row rowScanner) (domain.Config, error) {
var config domain.Config
err := row.Scan(
&config.AppCode,
&config.Enabled,
&config.ConfigVersion,
&config.UpdatedByAdminID,
&config.CreatedAtMS,
&config.UpdatedAtMS,
)
return config, err
}
func scanCheckinRewards(rows *sql.Rows) ([]domain.Reward, error) {
rewards := make([]domain.Reward, 0)
for rows.Next() {
var reward domain.Reward
if err := rows.Scan(&reward.DayIndex, &reward.ResourceGroupID); err != nil {
return nil, err
}
rewards = append(rewards, reward)
}
if err := rows.Err(); err != nil {
return nil, err
}
return rewards, nil
}
func scanCheckinAccount(row rowScanner) (domain.Account, error) {
var account domain.Account
err := row.Scan(
&account.AppCode,
&account.UserID,
&account.CycleNo,
&account.CycleStartDay,
&account.LastCheckinDay,
&account.LastDayIndex,
&account.ConfigVersion,
&account.UpdatedAtMS,
)
return account, err
}
func scanCheckinClaims(rows *sql.Rows, total int64) ([]domain.Claim, int64, error) {
claims := make([]domain.Claim, 0)
for rows.Next() {
claim, err := scanCheckinClaim(rows)
if err != nil {
return nil, 0, err
}
claims = append(claims, claim)
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
return claims, total, nil
}
func scanCheckinClaim(row rowScanner) (domain.Claim, error) {
var claim domain.Claim
err := row.Scan(
&claim.ClaimID,
&claim.CommandID,
&claim.UserID,
&claim.CycleNo,
&claim.CheckinDay,
&claim.DayIndex,
&claim.ConfigVersion,
&claim.ResourceGroupID,
&claim.WalletCommandID,
&claim.WalletGrantID,
&claim.Status,
&claim.FailureReason,
&claim.SignedAtMS,
&claim.GrantedAtMS,
&claim.CreatedAtMS,
&claim.UpdatedAtMS,
)
return claim, err
}
func checkinConfigSelectSQL() string {
return `SELECT app_code, enabled, config_version, updated_by_admin_id, created_at_ms, updated_at_ms
FROM seven_day_checkin_configs`
}
func checkinAccountSelectSQL() string {
return `SELECT app_code, user_id, cycle_no, cycle_start_day, last_checkin_day,
last_day_index, config_version, updated_at_ms
FROM user_seven_day_checkin_accounts`
}
func checkinClaimSelectSQL() string {
return `SELECT claim_id, command_id, user_id, cycle_no, checkin_day, day_index,
config_version, resource_group_id, wallet_command_id, wallet_grant_id,
status, failure_reason, signed_at_ms, granted_at_ms, created_at_ms, updated_at_ms
FROM seven_day_checkin_claims`
}
func checkinClaimWhere(ctx context.Context, query domain.ClaimQuery) (string, []any) {
conditions := []string{"app_code = ?"}
args := []any{appcode.FromContext(ctx)}
if query.Status != "" {
conditions = append(conditions, "status = ?")
args = append(args, query.Status)
}
if query.UserID > 0 {
conditions = append(conditions, "user_id = ?")
args = append(args, query.UserID)
}
if query.CheckinDay != "" {
conditions = append(conditions, "checkin_day = ?")
args = append(args, query.CheckinDay)
}
if query.DayIndex > 0 {
conditions = append(conditions, "day_index = ?")
args = append(args, query.DayIndex)
}
return "WHERE " + strings.Join(conditions, " AND "), args
}
func rewardsSnapshot(rewards []domain.Reward) (string, error) {
snapshot := checkinRewardSnapshot{Rewards: make([]checkinRewardSnapshotItem, 0, len(rewards))}
for _, reward := range rewards {
snapshot.Rewards = append(snapshot.Rewards, checkinRewardSnapshotItem{
DayIndex: reward.DayIndex,
ResourceGroupID: reward.ResourceGroupID,
})
}
payload, err := json.Marshal(snapshot)
if err != nil {
return "", err
}
return string(payload), nil
}
func rewardsFromSnapshot(snapshot string) ([]domain.Reward, error) {
var decoded checkinRewardSnapshot
if err := json.Unmarshal([]byte(snapshot), &decoded); err != nil {
return nil, err
}
rewards := make([]domain.Reward, 0, len(decoded.Rewards))
for _, reward := range decoded.Rewards {
rewards = append(rewards, domain.Reward{DayIndex: reward.DayIndex, ResourceGroupID: reward.ResourceGroupID})
}
return rewards, nil
}
type checkinSlot struct {
CycleNo int64
DayIndex int32
ConfigVersion int64
}
func nextCheckinSlot(account domain.Account, exists bool, currentVersion int64, today string, yesterday string) checkinSlot {
if !exists || account.UserID <= 0 {
return checkinSlot{CycleNo: 1, DayIndex: 1, ConfigVersion: currentVersion}
}
if account.LastCheckinDay == yesterday && account.LastDayIndex > 0 && account.LastDayIndex < domain.DayCount {
return checkinSlot{CycleNo: account.CycleNo, DayIndex: account.LastDayIndex + 1, ConfigVersion: account.ConfigVersion}
}
nextCycle := account.CycleNo + 1
if nextCycle <= 0 {
nextCycle = 1
}
return checkinSlot{CycleNo: nextCycle, DayIndex: 1, ConfigVersion: currentVersion}
}
func rewardGroupID(rewards []domain.Reward, dayIndex int32) int64 {
for _, reward := range rewards {
if reward.DayIndex == dayIndex {
return reward.ResourceGroupID
}
}
return 0
}
func validCheckinRewards(rewards []domain.Reward) bool {
if len(rewards) != domain.DayCount {
return false
}
seen := make(map[int32]struct{}, domain.DayCount)
for _, reward := range rewards {
if reward.DayIndex < 1 || reward.DayIndex > domain.DayCount || reward.ResourceGroupID <= 0 {
return false
}
if _, ok := seen[reward.DayIndex]; ok {
return false
}
seen[reward.DayIndex] = struct{}{}
}
return len(seen) == domain.DayCount
}
func walletCheckinCommandID(claimID string) string {
return fmt.Sprintf("wcheckin_%s", claimID)
}

View File

@ -0,0 +1,248 @@
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{
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,
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, domain.DrawCommand{
CommandID: meta.GetCommandId(),
UserID: meta.GetUserId(),
DeviceID: meta.GetDeviceId(),
RoomID: meta.GetRoomId(),
AnchorID: meta.GetAnchorId(),
GiftID: meta.GetGiftId(),
CoinSpent: meta.GetCoinSpent(),
PaidAtMS: meta.GetPaidAtMs(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &activityv1.ExecuteLuckyGiftDrawResponse{Result: luckyDrawResultToProto(result)}, nil
}
// 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)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &activityv1.GetLuckyGiftConfigResponse{Config: luckyConfigToProto(config)}, nil
}
func (s *AdminLuckyGiftServer) UpsertLuckyGiftConfig(ctx context.Context, req *activityv1.UpsertLuckyGiftConfigRequest) (*activityv1.UpsertLuckyGiftConfigResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
config := luckyConfigFromProto(req.GetConfig())
config.UpdatedByAdminID = req.GetOperatorAdminId()
updated, err := s.svc.UpsertConfig(ctx, config)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &activityv1.UpsertLuckyGiftConfigResponse{Config: luckyConfigToProto(updated)}, 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(),
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 luckyConfigFromProto(config *activityv1.LuckyGiftConfig) domain.Config {
if config == nil {
return domain.Config{}
}
tiers := make([]domain.Tier, 0, len(config.GetTiers()))
for _, tier := range config.GetTiers() {
tiers = append(tiers, domain.Tier{
Pool: tier.GetPool(),
TierID: tier.GetTierId(),
RewardCoins: tier.GetRewardCoins(),
Weight: tier.GetWeight(),
HighWaterOnly: tier.GetHighWaterOnly(),
Enabled: tier.GetEnabled(),
})
}
return domain.Config{
AppCode: config.GetAppCode(),
GiftID: config.GetGiftId(),
Enabled: config.GetEnabled(),
RuleVersion: config.GetRuleVersion(),
GiftPrice: config.GetGiftPrice(),
TargetRTPPPM: config.GetTargetRtpPpm(),
PoolRatePPM: config.GetPoolRatePpm(),
GlobalWindowDraws: config.GetGlobalWindowDraws(),
GiftWindowDraws: config.GetGiftWindowDraws(),
NoviceDrawLimit: config.GetNoviceDrawLimit(),
IntermediateDrawLimit: config.GetIntermediateDrawLimit(),
HighMultiplier: config.GetHighMultiplier(),
HighWaterPoolMultiple: config.GetHighWaterPoolMultiple(),
PlatformPoolWeightPPM: config.GetPlatformPoolWeightPpm(),
RoomPoolWeightPPM: config.GetRoomPoolWeightPpm(),
GiftPoolWeightPPM: config.GetGiftPoolWeightPpm(),
InitialPlatformPool: config.GetInitialPlatformPool(),
InitialGiftPool: config.GetInitialGiftPool(),
InitialRoomPool: config.GetInitialRoomPool(),
PlatformReserve: config.GetPlatformReserve(),
GiftReserve: config.GetGiftReserve(),
RoomReserve: config.GetRoomReserve(),
MaxSinglePayout: config.GetMaxSinglePayout(),
UserHourlyPayoutCap: config.GetUserHourlyPayoutCap(),
UserDailyPayoutCap: config.GetUserDailyPayoutCap(),
DeviceDailyPayoutCap: config.GetDeviceDailyPayoutCap(),
RoomHourlyPayoutCap: config.GetRoomHourlyPayoutCap(),
AnchorDailyPayoutCap: config.GetAnchorDailyPayoutCap(),
RoomAtmosphereRatePPM: config.GetRoomAtmosphereRatePpm(),
RoomAtmosphereInitial: config.GetRoomAtmosphereInitial(),
RoomAtmosphereReserve: config.GetRoomAtmosphereReserve(),
ActivityBudget: config.GetActivityBudget(),
ActivityDailyLimit: config.GetActivityDailyLimit(),
LargeTierEnabled: config.GetLargeTierEnabled(),
Tiers: tiers,
UpdatedByAdminID: config.GetUpdatedByAdminId(),
CreatedAtMS: config.GetCreatedAtMs(),
UpdatedAtMS: config.GetUpdatedAtMs(),
}
}
func luckyConfigToProto(config domain.Config) *activityv1.LuckyGiftConfig {
tiers := make([]*activityv1.LuckyGiftTier, 0, len(config.Tiers))
for _, tier := range config.Tiers {
tiers = append(tiers, &activityv1.LuckyGiftTier{
Pool: tier.Pool,
TierId: tier.TierID,
RewardCoins: tier.RewardCoins,
Weight: tier.Weight,
HighWaterOnly: tier.HighWaterOnly,
Enabled: tier.Enabled,
})
}
return &activityv1.LuckyGiftConfig{
AppCode: config.AppCode,
GiftId: config.GiftID,
Enabled: config.Enabled,
RuleVersion: config.RuleVersion,
GiftPrice: config.GiftPrice,
TargetRtpPpm: config.TargetRTPPPM,
PoolRatePpm: config.PoolRatePPM,
GlobalWindowDraws: config.GlobalWindowDraws,
GiftWindowDraws: config.GiftWindowDraws,
NoviceDrawLimit: config.NoviceDrawLimit,
IntermediateDrawLimit: config.IntermediateDrawLimit,
HighMultiplier: config.HighMultiplier,
HighWaterPoolMultiple: config.HighWaterPoolMultiple,
PlatformPoolWeightPpm: config.PlatformPoolWeightPPM,
RoomPoolWeightPpm: config.RoomPoolWeightPPM,
GiftPoolWeightPpm: config.GiftPoolWeightPPM,
InitialPlatformPool: config.InitialPlatformPool,
InitialGiftPool: config.InitialGiftPool,
InitialRoomPool: config.InitialRoomPool,
PlatformReserve: config.PlatformReserve,
GiftReserve: config.GiftReserve,
RoomReserve: config.RoomReserve,
MaxSinglePayout: config.MaxSinglePayout,
UserHourlyPayoutCap: config.UserHourlyPayoutCap,
UserDailyPayoutCap: config.UserDailyPayoutCap,
DeviceDailyPayoutCap: config.DeviceDailyPayoutCap,
RoomHourlyPayoutCap: config.RoomHourlyPayoutCap,
AnchorDailyPayoutCap: config.AnchorDailyPayoutCap,
RoomAtmosphereRatePpm: config.RoomAtmosphereRatePPM,
RoomAtmosphereInitial: config.RoomAtmosphereInitial,
RoomAtmosphereReserve: config.RoomAtmosphereReserve,
ActivityBudget: config.ActivityBudget,
ActivityDailyLimit: config.ActivityDailyLimit,
LargeTierEnabled: config.LargeTierEnabled,
Tiers: tiers,
UpdatedByAdminId: config.UpdatedByAdminID,
CreatedAtMs: config.CreatedAtMS,
UpdatedAtMs: config.UpdatedAtMS,
}
}
func luckyDrawResultToProto(result domain.DrawResult) *activityv1.LuckyGiftDrawResult {
return &activityv1.LuckyGiftDrawResult{
DrawId: result.DrawID,
CommandId: result.CommandID,
GiftId: result.GiftID,
RuleVersion: result.RuleVersion,
ExperiencePool: result.ExperiencePool,
SelectedTierId: result.SelectedTierID,
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,
}
}

View File

@ -0,0 +1,179 @@
package grpc
import (
"context"
activityv1 "hyapp.local/api/proto/activity/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
domain "hyapp/services/activity-service/internal/domain/sevendaycheckin"
service "hyapp/services/activity-service/internal/service/sevendaycheckin"
)
// SevenDayCheckInServer 暴露 App 七日签到面板查询和签到命令入口。
type SevenDayCheckInServer struct {
activityv1.UnimplementedSevenDayCheckInServiceServer
svc *service.Service
}
func NewSevenDayCheckInServer(svc *service.Service) *SevenDayCheckInServer {
return &SevenDayCheckInServer{svc: svc}
}
func (s *SevenDayCheckInServer) GetSevenDayCheckInStatus(ctx context.Context, req *activityv1.GetSevenDayCheckInStatusRequest) (*activityv1.GetSevenDayCheckInStatusResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
status, err := s.svc.GetStatus(ctx, req.GetUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return checkinStatusToProto(status), nil
}
func (s *SevenDayCheckInServer) SignSevenDayCheckIn(ctx context.Context, req *activityv1.SignSevenDayCheckInRequest) (*activityv1.SignSevenDayCheckInResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
claim, err := s.svc.Sign(ctx, req.GetUserId(), req.GetCommandId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return checkinSignToProto(claim), nil
}
// AdminSevenDayCheckInServer 暴露后台配置和领取记录查询入口。
type AdminSevenDayCheckInServer struct {
activityv1.UnimplementedAdminSevenDayCheckInServiceServer
svc *service.Service
}
func NewAdminSevenDayCheckInServer(svc *service.Service) *AdminSevenDayCheckInServer {
return &AdminSevenDayCheckInServer{svc: svc}
}
func (s *AdminSevenDayCheckInServer) GetSevenDayCheckInConfig(ctx context.Context, req *activityv1.GetSevenDayCheckInConfigRequest) (*activityv1.GetSevenDayCheckInConfigResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
config, err := s.svc.GetConfig(ctx)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &activityv1.GetSevenDayCheckInConfigResponse{Config: checkinConfigToProto(config)}, nil
}
func (s *AdminSevenDayCheckInServer) UpdateSevenDayCheckInConfig(ctx context.Context, req *activityv1.UpdateSevenDayCheckInConfigRequest) (*activityv1.UpdateSevenDayCheckInConfigResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
rewards := make([]domain.Reward, 0, len(req.GetRewards()))
for _, reward := range req.GetRewards() {
rewards = append(rewards, domain.Reward{DayIndex: reward.GetDayIndex(), ResourceGroupID: reward.GetResourceGroupId()})
}
config, err := s.svc.UpdateConfig(ctx, domain.Config{
Enabled: req.GetEnabled(),
Rewards: rewards,
UpdatedByAdminID: req.GetOperatorAdminId(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &activityv1.UpdateSevenDayCheckInConfigResponse{Config: checkinConfigToProto(config)}, nil
}
func (s *AdminSevenDayCheckInServer) ListSevenDayCheckInClaims(ctx context.Context, req *activityv1.ListSevenDayCheckInClaimsRequest) (*activityv1.ListSevenDayCheckInClaimsResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
claims, total, err := s.svc.ListClaims(ctx, domain.ClaimQuery{
Status: req.GetStatus(),
UserID: req.GetUserId(),
CheckinDay: req.GetCheckinDay(),
DayIndex: req.GetDayIndex(),
Page: req.GetPage(),
PageSize: req.GetPageSize(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &activityv1.ListSevenDayCheckInClaimsResponse{Claims: make([]*activityv1.SevenDayCheckInClaim, 0, len(claims)), Total: total}
for _, claim := range claims {
resp.Claims = append(resp.Claims, checkinClaimToProto(claim))
}
return resp, nil
}
func checkinStatusToProto(status domain.StatusResult) *activityv1.GetSevenDayCheckInStatusResponse {
resp := &activityv1.GetSevenDayCheckInStatusResponse{
Enabled: status.Enabled,
CanSign: status.CanSign,
Reason: status.Reason,
CycleNo: status.CycleNo,
CurrentDayIndex: status.CurrentDayIndex,
NextDayIndex: status.NextDayIndex,
CheckinDay: status.CheckinDay,
LastCheckinDay: status.LastCheckinDay,
ConfigVersion: status.ConfigVersion,
Rewards: make([]*activityv1.SevenDayCheckInRewardStatus, 0, len(status.Rewards)),
ServerTimeMs: status.ServerTimeMS,
NextRefreshAtMs: status.NextRefreshMS,
}
for _, reward := range status.Rewards {
resp.Rewards = append(resp.Rewards, &activityv1.SevenDayCheckInRewardStatus{
DayIndex: reward.DayIndex,
ResourceGroupId: reward.ResourceGroupID,
Status: reward.Status,
ClaimId: reward.ClaimID,
WalletGrantId: reward.WalletGrantID,
GrantedAtMs: reward.GrantedAtMS,
})
}
return resp
}
func checkinSignToProto(claim domain.Claim) *activityv1.SignSevenDayCheckInResponse {
return &activityv1.SignSevenDayCheckInResponse{
ClaimId: claim.ClaimID,
CycleNo: claim.CycleNo,
CheckinDay: claim.CheckinDay,
DayIndex: claim.DayIndex,
ResourceGroupId: claim.ResourceGroupID,
Status: claim.Status,
WalletGrantId: claim.WalletGrantID,
GrantedAtMs: claim.GrantedAtMS,
ServerTimeMs: claim.UpdatedAtMS,
}
}
func checkinConfigToProto(config domain.Config) *activityv1.SevenDayCheckInConfig {
resp := &activityv1.SevenDayCheckInConfig{
AppCode: config.AppCode,
Enabled: config.Enabled,
ConfigVersion: config.ConfigVersion,
Rewards: make([]*activityv1.SevenDayCheckInReward, 0, len(config.Rewards)),
UpdatedByAdminId: config.UpdatedByAdminID,
CreatedAtMs: config.CreatedAtMS,
UpdatedAtMs: config.UpdatedAtMS,
}
for _, reward := range config.Rewards {
resp.Rewards = append(resp.Rewards, &activityv1.SevenDayCheckInReward{
DayIndex: reward.DayIndex,
ResourceGroupId: reward.ResourceGroupID,
})
}
return resp
}
func checkinClaimToProto(claim domain.Claim) *activityv1.SevenDayCheckInClaim {
return &activityv1.SevenDayCheckInClaim{
ClaimId: claim.ClaimID,
CommandId: claim.CommandID,
UserId: claim.UserID,
CycleNo: claim.CycleNo,
CheckinDay: claim.CheckinDay,
DayIndex: claim.DayIndex,
ConfigVersion: claim.ConfigVersion,
ResourceGroupId: claim.ResourceGroupID,
WalletCommandId: claim.WalletCommandID,
WalletGrantId: claim.WalletGrantID,
Status: claim.Status,
FailureReason: claim.FailureReason,
SignedAtMs: claim.SignedAtMS,
GrantedAtMs: claim.GrantedAtMS,
CreatedAtMs: claim.CreatedAtMS,
UpdatedAtMs: claim.UpdatedAtMS,
}
}

View File

@ -1,35 +1,37 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
CREATE DATABASE IF NOT EXISTS hyapp_cron DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE hyapp_cron;
CREATE TABLE IF NOT EXISTS cron_task_leases (
task_name VARCHAR(96) NOT NULL,
app_code VARCHAR(32) NOT NULL,
owner_node_id VARCHAR(128) NOT NULL,
lease_until_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
task_name VARCHAR(96) NOT NULL COMMENT '任务名称',
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
owner_node_id VARCHAR(128) NOT NULL COMMENT '房主节点 ID',
lease_until_ms BIGINT NOT NULL COMMENT '租约截止时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (task_name, app_code),
KEY idx_cron_task_leases_expire (lease_until_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定时任务租约表';
CREATE TABLE IF NOT EXISTS cron_task_runs (
run_id VARCHAR(96) NOT NULL,
task_name VARCHAR(96) NOT NULL,
app_code VARCHAR(32) NOT NULL,
owner_node_id VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
claimed_count INT NOT NULL DEFAULT 0,
processed_count INT NOT NULL DEFAULT 0,
success_count INT NOT NULL DEFAULT 0,
failure_count INT NOT NULL DEFAULT 0,
started_at_ms BIGINT NOT NULL,
finished_at_ms BIGINT NULL,
duration_ms BIGINT NOT NULL DEFAULT 0,
error_message VARCHAR(512) NOT NULL DEFAULT '',
run_id VARCHAR(96) NOT NULL COMMENT '运行 ID',
task_name VARCHAR(96) NOT NULL COMMENT '任务名称',
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
owner_node_id VARCHAR(128) NOT NULL COMMENT '房主节点 ID',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
claimed_count INT NOT NULL DEFAULT 0 COMMENT '认领数量',
processed_count INT NOT NULL DEFAULT 0 COMMENT '处理数量',
success_count INT NOT NULL DEFAULT 0 COMMENT '成功数量',
failure_count INT NOT NULL DEFAULT 0 COMMENT '失败数量',
started_at_ms BIGINT NOT NULL COMMENT '开始时间UTC epoch ms',
finished_at_ms BIGINT NULL COMMENT '结束时间UTC epoch ms',
duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '持续时长,毫秒',
error_message VARCHAR(512) NOT NULL DEFAULT '' COMMENT '错误消息',
PRIMARY KEY (run_id),
KEY idx_cron_task_runs_task_time (task_name, app_code, started_at_ms),
KEY idx_cron_task_runs_status (status, started_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定时任务运行记录表';

View File

@ -1,169 +1,171 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
CREATE DATABASE IF NOT EXISTS hyapp_game DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE hyapp_game;
CREATE TABLE IF NOT EXISTS game_platforms (
app_code VARCHAR(32) NOT NULL,
platform_code VARCHAR(64) NOT NULL,
platform_name VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
api_base_url VARCHAR(512) NOT NULL DEFAULT '',
callback_secret_ciphertext VARCHAR(1024) NOT NULL DEFAULT '',
callback_ip_whitelist JSON NULL,
adapter_config JSON NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
platform_code VARCHAR(64) NOT NULL COMMENT '平台编码',
platform_name VARCHAR(128) NOT NULL COMMENT '平台名称',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
api_base_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'API 基准 URL',
callback_secret_ciphertext VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '回调密钥密文',
callback_ip_whitelist JSON NULL COMMENT '回调IP白名单',
adapter_config JSON NULL COMMENT '适配器配置',
sort_order INT 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, platform_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='游戏平台表';
CREATE TABLE IF NOT EXISTS game_catalog (
app_code VARCHAR(32) NOT NULL,
game_id VARCHAR(96) NOT NULL,
platform_code VARCHAR(64) NOT NULL,
provider_game_id VARCHAR(128) NOT NULL,
game_name VARCHAR(128) NOT NULL,
category VARCHAR(64) NOT NULL DEFAULT '',
icon_url VARCHAR(512) NOT NULL DEFAULT '',
cover_url VARCHAR(512) NOT NULL DEFAULT '',
launch_mode VARCHAR(32) NOT NULL DEFAULT 'h5_popup',
orientation VARCHAR(32) NOT NULL DEFAULT 'portrait',
min_coin BIGINT NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
tags JSON NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
game_id VARCHAR(96) NOT NULL COMMENT '游戏 ID',
platform_code VARCHAR(64) NOT NULL COMMENT '平台编码',
provider_game_id VARCHAR(128) NOT NULL COMMENT '提供方游戏 ID',
game_name VARCHAR(128) NOT NULL COMMENT '游戏名称',
category VARCHAR(64) NOT NULL DEFAULT '' COMMENT '分类',
icon_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '图标 URL',
cover_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '封面 URL',
launch_mode VARCHAR(32) NOT NULL DEFAULT 'h5_popup' COMMENT '启动模式',
orientation VARCHAR(32) NOT NULL DEFAULT 'portrait' COMMENT '屏幕方向',
min_coin BIGINT NOT NULL DEFAULT 0 COMMENT '最小金币',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
tags JSON NULL COMMENT '标签',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY(app_code, game_id),
UNIQUE KEY uk_game_provider(app_code, platform_code, provider_game_id),
KEY idx_game_platform_status(app_code, platform_code, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='游戏目录表';
CREATE TABLE IF NOT EXISTS game_display_rules (
app_code VARCHAR(32) NOT NULL,
rule_id VARCHAR(96) NOT NULL,
game_id VARCHAR(96) NOT NULL,
scene VARCHAR(64) NOT NULL,
region_id BIGINT NOT NULL DEFAULT 0,
language VARCHAR(32) NOT NULL DEFAULT '',
platform VARCHAR(16) NOT NULL DEFAULT '',
visible TINYINT(1) NOT NULL DEFAULT 1,
enabled TINYINT(1) NOT NULL DEFAULT 1,
sort_order INT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
rule_id VARCHAR(96) NOT NULL COMMENT '规则 ID',
game_id VARCHAR(96) NOT NULL COMMENT '游戏 ID',
scene VARCHAR(64) NOT NULL COMMENT '场景',
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '区域 ID',
language VARCHAR(32) NOT NULL DEFAULT '' COMMENT '语言',
platform VARCHAR(16) NOT NULL DEFAULT '' COMMENT '平台',
visible TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否可见',
enabled TINYINT(1) NOT NULL DEFAULT 1 COMMENT '启用',
sort_order INT 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, rule_id),
KEY idx_game_display(app_code, scene, region_id, visible, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='游戏展示规则表';
CREATE TABLE IF NOT EXISTS game_launch_sessions (
app_code VARCHAR(32) NOT NULL,
session_id VARCHAR(96) NOT NULL,
user_id BIGINT NOT NULL,
display_user_id VARCHAR(32) NOT NULL,
room_id VARCHAR(96) NOT NULL DEFAULT '',
scene VARCHAR(64) NOT NULL,
platform_code VARCHAR(64) NOT NULL,
game_id VARCHAR(96) NOT NULL,
provider_game_id VARCHAR(128) NOT NULL,
launch_token_hash VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
expires_at_ms BIGINT NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
session_id VARCHAR(96) NOT NULL COMMENT '会话 ID',
user_id BIGINT NOT NULL COMMENT '用户 ID',
display_user_id VARCHAR(32) NOT NULL COMMENT '展示用户 ID',
room_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '房间 ID',
scene VARCHAR(64) NOT NULL COMMENT '场景',
platform_code VARCHAR(64) NOT NULL COMMENT '平台编码',
game_id VARCHAR(96) NOT NULL COMMENT '游戏 ID',
provider_game_id VARCHAR(128) NOT NULL COMMENT '提供方游戏 ID',
launch_token_hash VARCHAR(128) NOT NULL COMMENT '启动令牌哈希值',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
expires_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, session_id),
KEY idx_game_launch_user(app_code, user_id, created_at_ms),
KEY idx_game_launch_token(app_code, launch_token_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='游戏启动会话表';
CREATE TABLE IF NOT EXISTS game_orders (
app_code VARCHAR(32) NOT NULL,
order_id VARCHAR(96) NOT NULL,
platform_code VARCHAR(64) NOT NULL,
provider_order_id VARCHAR(160) NOT NULL,
provider_round_id VARCHAR(160) NOT NULL DEFAULT '',
game_id VARCHAR(96) NOT NULL,
provider_game_id VARCHAR(128) NOT NULL,
user_id BIGINT NOT NULL,
room_id VARCHAR(96) NOT NULL DEFAULT '',
op_type VARCHAR(32) NOT NULL,
coin_amount BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '',
wallet_balance_after BIGINT NOT NULL DEFAULT 0,
request_hash VARCHAR(128) NOT NULL,
raw_payload_ref VARCHAR(256) NOT NULL DEFAULT '',
failure_code VARCHAR(64) NOT NULL DEFAULT '',
failure_message VARCHAR(256) NOT NULL DEFAULT '',
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
order_id VARCHAR(96) NOT NULL COMMENT '订单 ID',
platform_code VARCHAR(64) NOT NULL COMMENT '平台编码',
provider_order_id VARCHAR(160) NOT NULL COMMENT '提供方订单 ID',
provider_round_id VARCHAR(160) NOT NULL DEFAULT '' COMMENT '提供方轮次 ID',
game_id VARCHAR(96) NOT NULL COMMENT '游戏 ID',
provider_game_id VARCHAR(128) NOT NULL COMMENT '提供方游戏 ID',
user_id BIGINT NOT NULL COMMENT '用户 ID',
room_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '房间 ID',
op_type VARCHAR(32) NOT NULL COMMENT 'op类型',
coin_amount BIGINT NOT NULL COMMENT '金币数量',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
wallet_balance_after BIGINT NOT NULL DEFAULT 0 COMMENT '钱包余额后',
request_hash VARCHAR(128) NOT NULL COMMENT '请求哈希值',
raw_payload_ref VARCHAR(256) NOT NULL DEFAULT '' COMMENT '原始负载外部引用',
failure_code VARCHAR(64) NOT NULL DEFAULT '' COMMENT '失败编码',
failure_message VARCHAR(256) NOT NULL DEFAULT '' 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, order_id),
UNIQUE KEY uk_game_provider_order(app_code, platform_code, provider_order_id),
KEY idx_game_order_user_time(app_code, user_id, created_at_ms),
KEY idx_game_order_round(app_code, platform_code, provider_round_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='游戏钱包订单表';
CREATE TABLE IF NOT EXISTS game_callback_logs (
app_code VARCHAR(32) NOT NULL,
callback_id VARCHAR(96) NOT NULL,
platform_code VARCHAR(64) NOT NULL,
operation VARCHAR(64) NOT NULL,
request_id VARCHAR(96) NOT NULL DEFAULT '',
provider_request_id VARCHAR(160) NOT NULL DEFAULT '',
signature_valid TINYINT(1) NOT NULL DEFAULT 0,
request_hash VARCHAR(128) NOT NULL,
response_code VARCHAR(64) NOT NULL DEFAULT '',
response_body_hash VARCHAR(128) NOT NULL DEFAULT '',
status VARCHAR(32) NOT NULL,
created_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
callback_id VARCHAR(96) NOT NULL COMMENT '回调 ID',
platform_code VARCHAR(64) NOT NULL COMMENT '平台编码',
operation VARCHAR(64) NOT NULL COMMENT '操作',
request_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '链路请求 ID用于排查问题',
provider_request_id VARCHAR(160) NOT NULL DEFAULT '' COMMENT '提供方请求 ID',
signature_valid TINYINT(1) NOT NULL DEFAULT 0 COMMENT '签名有效',
request_hash VARCHAR(128) NOT NULL COMMENT '请求哈希值',
response_code VARCHAR(64) NOT NULL DEFAULT '' COMMENT '响应编码',
response_body_hash VARCHAR(128) NOT NULL DEFAULT '' COMMENT '响应正文哈希值',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
PRIMARY KEY(app_code, callback_id),
KEY idx_game_callback_platform_time(app_code, platform_code, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='游戏回调日志表';
CREATE TABLE IF NOT EXISTS game_repair_orders (
app_code VARCHAR(32) NOT NULL,
repair_id VARCHAR(96) NOT NULL,
platform_code VARCHAR(64) NOT NULL,
provider_order_id VARCHAR(160) NOT NULL,
reason VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
operator_admin_id BIGINT NOT NULL DEFAULT 0,
attempts INT NOT NULL DEFAULT 0,
next_run_at_ms BIGINT NOT NULL DEFAULT 0,
last_error_code VARCHAR(64) NOT NULL DEFAULT '',
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
repair_id VARCHAR(96) NOT NULL COMMENT '修复 ID',
platform_code VARCHAR(64) NOT NULL COMMENT '平台编码',
provider_order_id VARCHAR(160) NOT NULL COMMENT '提供方订单 ID',
reason VARCHAR(128) NOT NULL COMMENT '原因',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
operator_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '操作管理员 ID',
attempts INT NOT NULL DEFAULT 0 COMMENT '尝试次数',
next_run_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '下一个运行时间UTC epoch ms',
last_error_code VARCHAR(64) NOT NULL DEFAULT '' 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, repair_id),
UNIQUE KEY uk_game_repair_provider_order(app_code, platform_code, provider_order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='游戏修复订单表';
CREATE TABLE IF NOT EXISTS game_level_event_outbox (
app_code VARCHAR(32) NOT NULL,
event_id VARCHAR(160) NOT NULL,
order_id VARCHAR(96) NOT NULL,
user_id BIGINT NOT NULL,
game_id VARCHAR(96) NOT NULL,
provider_order_id VARCHAR(160) NOT NULL,
provider_round_id VARCHAR(160) NOT NULL DEFAULT '',
coin_amount BIGINT NOT NULL,
wallet_transaction_id VARCHAR(96) NOT NULL,
payload_json JSON NULL,
status VARCHAR(32) NOT NULL,
attempt_count INT NOT NULL DEFAULT 0,
next_retry_at_ms BIGINT NOT NULL DEFAULT 0,
locked_by VARCHAR(128) NOT NULL DEFAULT '',
locked_until_ms BIGINT NOT NULL DEFAULT 0,
failure_reason VARCHAR(255) NOT NULL DEFAULT '',
occurred_at_ms BIGINT NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
event_id VARCHAR(160) NOT NULL COMMENT '事件幂等 ID',
order_id VARCHAR(96) NOT NULL COMMENT '订单 ID',
user_id BIGINT NOT NULL COMMENT '用户 ID',
game_id VARCHAR(96) NOT NULL COMMENT '游戏 ID',
provider_order_id VARCHAR(160) NOT NULL COMMENT '提供方订单 ID',
provider_round_id VARCHAR(160) NOT NULL DEFAULT '' COMMENT '提供方轮次 ID',
coin_amount BIGINT NOT NULL COMMENT '金币数量',
wallet_transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
payload_json JSON NULL COMMENT '业务负载 JSON 快照',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
attempt_count INT NOT NULL DEFAULT 0 COMMENT '尝试次数',
next_retry_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '下一次重试时间UTC epoch ms',
locked_by VARCHAR(128) NOT NULL DEFAULT '' COMMENT '当前锁定 worker',
locked_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT '锁定截止时间UTC epoch ms',
failure_reason VARCHAR(255) NOT NULL DEFAULT '' COMMENT '失败原因',
occurred_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, event_id),
UNIQUE KEY uk_game_level_event_order(app_code, order_id),
KEY idx_game_level_event_pending(app_code, status, next_retry_at_ms, created_at_ms),
KEY idx_game_level_event_user(app_code, user_id, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='游戏等级事件 outbox 表';
INSERT INTO game_platforms (
app_code, platform_code, platform_name, status, api_base_url, sort_order, created_at_ms, updated_at_ms

View File

@ -94,6 +94,7 @@ func New(cfg config.Config) (*App, error) {
var growthLevelClient client.GrowthLevelClient = client.NewGRPCGrowthLevelClient(activityConn)
var achievementClient client.AchievementClient = client.NewGRPCAchievementClient(activityConn)
var registrationRewardClient client.RegistrationRewardClient = client.NewGRPCRegistrationRewardClient(activityConn)
var sevenDayCheckInClient client.SevenDayCheckInClient = client.NewGRPCSevenDayCheckInClient(activityConn)
var broadcastClient client.BroadcastClient = client.NewGRPCBroadcastClient(activityConn)
var gameClient client.GameClient = client.NewGRPCGameClient(gameConn)
appConfigReader, err := openAppConfigReader(cfg.AppConfig)
@ -124,6 +125,7 @@ func New(cfg config.Config) (*App, error) {
handler.SetGrowthLevelClient(growthLevelClient)
handler.SetAchievementClient(achievementClient)
handler.SetRegistrationRewardClient(registrationRewardClient)
handler.SetSevenDayCheckInClient(sevenDayCheckInClient)
handler.SetBroadcastClient(broadcastClient)
handler.SetGameClient(gameClient)
if appConfigReader != nil {

View File

@ -6,6 +6,7 @@ import (
"database/sql"
"errors"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
)
@ -14,12 +15,14 @@ const h5LinkGroup = "h5-links"
const listH5LinksSQL = "SELECT `key`, COALESCE(value, ''), updated_at_ms FROM admin_app_configs WHERE `group` = ?"
const listAppBannersSQL = `
SELECT id, app_code, cover_url, banner_type, param, platform, sort_order, region_id, country_code, description, updated_at_ms
SELECT id, app_code, cover_url, room_small_image_url, banner_type, display_scope, param, platform, sort_order, region_id, country_code, description, starts_at_ms, ends_at_ms, updated_at_ms
FROM admin_app_banners
WHERE app_code = ? AND status = 'active'
WHERE app_code = ? AND status = 'active' AND display_scope = ?
AND (? = '' OR platform = ?)
AND (region_id = 0 OR region_id = ?)
AND (country_code = '' OR country_code = ?)
AND (starts_at_ms = 0 OR starts_at_ms <= ?)
AND (ends_at_ms = 0 OR ends_at_ms > ?)
ORDER BY sort_order ASC, id DESC`
const latestAppVersionSQL = `
SELECT id, app_code, platform, version, build_number, force_update, download_url, COALESCE(description, ''), updated_at_ms
@ -36,26 +39,31 @@ type H5Link struct {
UpdatedAtMs int64 `json:"updated_at_ms"`
}
// BannerQuery 是 App 首页 banner 的公开筛选条件。
// BannerQuery 是 App banner 的公开筛选条件。
type BannerQuery struct {
AppCode string
Platform string
RegionID int64
Country string
AppCode string
DisplayScope string
Platform string
RegionID int64
Country string
}
// Banner 是 gateway 下发给 App 首页的 banner 配置。
// Banner 是 gateway 下发给 App 指定显示范围的 banner 配置。
type Banner struct {
ID uint `json:"id"`
CoverURL string `json:"cover_url"`
BannerType string `json:"type"`
Param string `json:"param"`
Platform string `json:"platform"`
SortOrder int `json:"sort_order"`
RegionID int64 `json:"region_id,omitempty"`
CountryCode string `json:"country_code,omitempty"`
Description string `json:"description,omitempty"`
UpdatedAtMs int64 `json:"updated_at_ms"`
ID uint `json:"id"`
CoverURL string `json:"cover_url"`
RoomSmallImageURL string `json:"room_small_image_url,omitempty"`
BannerType string `json:"type"`
DisplayScope string `json:"display_scope"`
Param string `json:"param"`
Platform string `json:"platform"`
SortOrder int `json:"sort_order"`
RegionID int64 `json:"region_id,omitempty"`
CountryCode string `json:"country_code,omitempty"`
Description string `json:"description,omitempty"`
StartsAtMs int64 `json:"starts_at_ms,omitempty"`
EndsAtMs int64 `json:"ends_at_ms,omitempty"`
UpdatedAtMs int64 `json:"updated_at_ms"`
}
// VersionQuery 是 App 检查更新的公开筛选条件。
@ -171,16 +179,21 @@ func (r *MySQLReader) ListH5Links(ctx context.Context) ([]H5Link, error) {
return items, nil
}
// ListBanners 返回当前 App、平台和地域可见的首页 banner。
// ListBanners 返回当前 App、显示范围、平台和地域可见的 banner。
func (r *MySQLReader) ListBanners(ctx context.Context, query BannerQuery) ([]Banner, error) {
if r == nil || r.db == nil {
return nil, errors.New("app config reader is not configured")
}
appCode := normalizeAppCode(query.AppCode)
displayScope := NormalizeBannerDisplayScope(query.DisplayScope)
if displayScope == "" {
return []Banner{}, nil
}
platform := normalizePlatform(query.Platform)
country := normalizeCountry(query.Country)
rows, err := r.db.QueryContext(ctx, listAppBannersSQL, appCode, platform, platform, query.RegionID, country)
nowMs := time.Now().UTC().UnixMilli()
rows, err := r.db.QueryContext(ctx, listAppBannersSQL, appCode, displayScope, platform, platform, query.RegionID, country, nowMs, nowMs)
if err != nil {
return nil, err
}
@ -195,13 +208,17 @@ func (r *MySQLReader) ListBanners(ctx context.Context, query BannerQuery) ([]Ban
&item.ID,
&appCode,
&item.CoverURL,
&item.RoomSmallImageURL,
&item.BannerType,
&item.DisplayScope,
&item.Param,
&item.Platform,
&item.SortOrder,
&item.RegionID,
&item.CountryCode,
&item.Description,
&item.StartsAtMs,
&item.EndsAtMs,
&updatedAtMS,
); err != nil {
return nil, err
@ -302,3 +319,20 @@ func normalizePlatform(value string) string {
func normalizeCountry(value string) string {
return strings.ToUpper(strings.TrimSpace(value))
}
// NormalizeBannerDisplayScope 统一 App 和后台约定的显示范围枚举。
func NormalizeBannerDisplayScope(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
switch value {
case "":
return "home"
case "home", "首页":
return "home"
case "room", "房间内":
return "room"
case "recharge", "充值页":
return "recharge"
default:
return ""
}
}

View File

@ -0,0 +1,19 @@
package appconfig
import "testing"
func TestNormalizeBannerDisplayScopeAcceptsChineseLabels(t *testing.T) {
cases := map[string]string{
"": "home",
"首页": "home",
"房间内": "room",
"充值页": "recharge",
"ROOM": "room",
"bad": "",
}
for input, want := range cases {
if got := NormalizeBannerDisplayScope(input); got != want {
t.Fatalf("scope %q mismatch: got %q want %q", input, got, want)
}
}
}

View File

@ -0,0 +1,30 @@
package client
import (
"context"
"google.golang.org/grpc"
activityv1 "hyapp.local/api/proto/activity/v1"
)
// SevenDayCheckInClient abstracts gateway access to activity-service seven-day checkin APIs.
type SevenDayCheckInClient interface {
GetSevenDayCheckInStatus(ctx context.Context, req *activityv1.GetSevenDayCheckInStatusRequest) (*activityv1.GetSevenDayCheckInStatusResponse, error)
SignSevenDayCheckIn(ctx context.Context, req *activityv1.SignSevenDayCheckInRequest) (*activityv1.SignSevenDayCheckInResponse, error)
}
type grpcSevenDayCheckInClient struct {
client activityv1.SevenDayCheckInServiceClient
}
func NewGRPCSevenDayCheckInClient(conn *grpc.ClientConn) SevenDayCheckInClient {
return &grpcSevenDayCheckInClient{client: activityv1.NewSevenDayCheckInServiceClient(conn)}
}
func (c *grpcSevenDayCheckInClient) GetSevenDayCheckInStatus(ctx context.Context, req *activityv1.GetSevenDayCheckInStatusRequest) (*activityv1.GetSevenDayCheckInStatusResponse, error) {
return c.client.GetSevenDayCheckInStatus(ctx, req)
}
func (c *grpcSevenDayCheckInClient) SignSevenDayCheckIn(ctx context.Context, req *activityv1.SignSevenDayCheckInRequest) (*activityv1.SignSevenDayCheckInResponse, error) {
return c.client.SignSevenDayCheckIn(ctx, req)
}

View File

@ -26,6 +26,7 @@ type WalletClient interface {
GetResource(ctx context.Context, req *walletv1.GetResourceRequest) (*walletv1.GetResourceResponse, error)
GetResourceGroup(ctx context.Context, req *walletv1.GetResourceGroupRequest) (*walletv1.GetResourceGroupResponse, error)
ListGiftConfigs(ctx context.Context, req *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error)
ListGiftTypeConfigs(ctx context.Context, req *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error)
GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error)
ListUserResources(ctx context.Context, req *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error)
EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error)
@ -104,6 +105,10 @@ func (c *grpcWalletClient) ListGiftConfigs(ctx context.Context, req *walletv1.Li
return c.client.ListGiftConfigs(ctx, req)
}
func (c *grpcWalletClient) ListGiftTypeConfigs(ctx context.Context, req *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error) {
return c.client.ListGiftTypeConfigs(ctx, req)
}
func (c *grpcWalletClient) GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error) {
return c.client.GrantResource(ctx, req)
}

View File

@ -13,6 +13,7 @@ type Handler struct {
achievementClient client.AchievementClient
walletClient client.WalletClient
registrationReward client.RegistrationRewardClient
sevenDayCheckIn client.SevenDayCheckInClient
}
type Config struct {
@ -21,6 +22,7 @@ type Config struct {
AchievementClient client.AchievementClient
WalletClient client.WalletClient
RegistrationReward client.RegistrationRewardClient
SevenDayCheckIn client.SevenDayCheckInClient
}
func New(config Config) *Handler {
@ -30,6 +32,7 @@ func New(config Config) *Handler {
achievementClient: config.AchievementClient,
walletClient: config.WalletClient,
registrationReward: config.RegistrationReward,
sevenDayCheckIn: config.SevenDayCheckIn,
}
}
@ -38,6 +41,8 @@ func (h *Handler) TaskHandlers() httproutes.TaskHandlers {
ListTaskTabs: h.listTaskTabs,
ClaimTaskReward: h.claimTaskReward,
GetRegistrationRewardEligibility: h.getRegistrationRewardEligibility,
GetSevenDayCheckInStatus: h.getSevenDayCheckInStatus,
SignSevenDayCheckIn: h.signSevenDayCheckIn,
}
}

View File

@ -0,0 +1,297 @@
package activityapi
import (
"net/http"
"strings"
activityv1 "hyapp.local/api/proto/activity/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
)
type checkinStatusData struct {
Enabled bool `json:"enabled"`
CanSign bool `json:"can_sign"`
Reason string `json:"reason"`
CycleNo int64 `json:"cycle_no"`
CurrentDayIndex int32 `json:"current_day_index"`
NextDayIndex int32 `json:"next_day_index"`
CheckinDay string `json:"checkin_day"`
LastCheckinDay string `json:"last_checkin_day"`
ConfigVersion int64 `json:"config_version"`
Rewards []checkinRewardData `json:"rewards"`
ServerTimeMS int64 `json:"server_time_ms"`
NextRefreshAtMS int64 `json:"next_refresh_at_ms"`
}
type checkinRewardData struct {
DayIndex int32 `json:"day_index"`
ResourceGroupID int64 `json:"resource_group_id"`
Status string `json:"status"`
ClaimID string `json:"claim_id,omitempty"`
WalletGrantID string `json:"wallet_grant_id,omitempty"`
GrantedAtMS int64 `json:"granted_at_ms,omitempty"`
ResourceGroup *checkinResourceGroupData `json:"resource_group,omitempty"`
}
type checkinSignData struct {
ClaimID string `json:"claim_id"`
CycleNo int64 `json:"cycle_no"`
CheckinDay string `json:"checkin_day"`
DayIndex int32 `json:"day_index"`
ResourceGroupID int64 `json:"resource_group_id"`
Status string `json:"status"`
WalletGrantID string `json:"wallet_grant_id"`
GrantedAtMS int64 `json:"granted_at_ms"`
ServerTimeMS int64 `json:"server_time_ms"`
}
type checkinSignRequestBody struct {
CommandID string `json:"command_id"`
CommandIDCamel string `json:"commandId"`
}
func (b checkinSignRequestBody) normalizedCommandID() string {
if value := strings.TrimSpace(b.CommandID); value != "" {
return value
}
return strings.TrimSpace(b.CommandIDCamel)
}
type checkinResourceData struct {
ResourceID int64 `json:"resource_id"`
ResourceCode string `json:"resource_code"`
ResourceType string `json:"resource_type"`
Name string `json:"name"`
Status string `json:"status"`
Grantable bool `json:"grantable"`
GrantStrategy string `json:"grant_strategy"`
WalletAssetType string `json:"wallet_asset_type,omitempty"`
WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"`
UsageScopes []string `json:"usage_scopes"`
AssetURL string `json:"asset_url"`
PreviewURL string `json:"preview_url"`
AnimationURL string `json:"animation_url"`
MetadataJSON string `json:"metadata_json"`
SortOrder int32 `json:"sort_order"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type checkinResourceGroupItemData struct {
GroupItemID int64 `json:"group_item_id"`
ItemType string `json:"item_type"`
ResourceID int64 `json:"resource_id"`
Resource *checkinResourceData `json:"resource,omitempty"`
WalletAssetType string `json:"wallet_asset_type,omitempty"`
WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"`
Quantity int64 `json:"quantity"`
DurationMS int64 `json:"duration_ms"`
SortOrder int32 `json:"sort_order"`
}
type checkinResourceGroupData struct {
GroupID int64 `json:"group_id"`
GroupCode string `json:"group_code"`
Name string `json:"name"`
Status string `json:"status"`
Description string `json:"description"`
SortOrder int32 `json:"sort_order"`
Items []checkinResourceGroupItemData `json:"items"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
// getSevenDayCheckInStatus 返回当前用户七日签到面板;资源组展示材料由 gateway 从 wallet-service 补齐。
func (h *Handler) getSevenDayCheckInStatus(writer http.ResponseWriter, request *http.Request) {
if h.sevenDayCheckIn == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
resp, err := h.sevenDayCheckIn.GetSevenDayCheckInStatus(request.Context(), &activityv1.GetSevenDayCheckInStatusRequest{
Meta: httpkit.ActivityMeta(request),
UserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
groups, ok := h.resolveCheckinResourceGroups(writer, request, checkinRewardGroupIDs(resp.GetRewards()))
if !ok {
return
}
httpkit.WriteOK(writer, request, checkinStatusFromProto(resp, groups))
}
// signSevenDayCheckIn 只透传当前用户和客户端幂等命令,连续天数和发奖由 activity-service 决定。
func (h *Handler) signSevenDayCheckIn(writer http.ResponseWriter, request *http.Request) {
if h.sevenDayCheckIn == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
var body checkinSignRequestBody
if !httpkit.Decode(writer, request, &body) {
return
}
commandID := body.normalizedCommandID()
if commandID == "" {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.sevenDayCheckIn.SignSevenDayCheckIn(request.Context(), &activityv1.SignSevenDayCheckInRequest{
Meta: httpkit.ActivityMeta(request),
UserId: auth.UserIDFromContext(request.Context()),
CommandId: commandID,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, checkinSignData{
ClaimID: resp.GetClaimId(),
CycleNo: resp.GetCycleNo(),
CheckinDay: resp.GetCheckinDay(),
DayIndex: resp.GetDayIndex(),
ResourceGroupID: resp.GetResourceGroupId(),
Status: resp.GetStatus(),
WalletGrantID: resp.GetWalletGrantId(),
GrantedAtMS: resp.GetGrantedAtMs(),
ServerTimeMS: resp.GetServerTimeMs(),
})
}
func (h *Handler) resolveCheckinResourceGroups(writer http.ResponseWriter, request *http.Request, ids []int64) (map[int64]checkinResourceGroupData, bool) {
ids = uniquePositiveIDs(ids)
groups := make(map[int64]checkinResourceGroupData, len(ids))
if len(ids) == 0 {
return groups, true
}
if h.walletClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return nil, false
}
for _, id := range ids {
resp, err := h.walletClient.GetResourceGroup(request.Context(), &walletv1.GetResourceGroupRequest{
RequestId: httpkit.RequestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
GroupId: id,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return nil, false
}
group := checkinResourceGroupFromProto(resp.GetGroup())
if group.GroupID > 0 {
groups[group.GroupID] = group
}
}
return groups, true
}
func checkinStatusFromProto(resp *activityv1.GetSevenDayCheckInStatusResponse, groups map[int64]checkinResourceGroupData) checkinStatusData {
items := make([]checkinRewardData, 0, len(resp.GetRewards()))
for _, reward := range resp.GetRewards() {
item := checkinRewardData{
DayIndex: reward.GetDayIndex(),
ResourceGroupID: reward.GetResourceGroupId(),
Status: reward.GetStatus(),
ClaimID: reward.GetClaimId(),
WalletGrantID: reward.GetWalletGrantId(),
GrantedAtMS: reward.GetGrantedAtMs(),
}
if group, ok := groups[reward.GetResourceGroupId()]; ok {
item.ResourceGroup = &group
}
items = append(items, item)
}
return checkinStatusData{
Enabled: resp.GetEnabled(),
CanSign: resp.GetCanSign(),
Reason: resp.GetReason(),
CycleNo: resp.GetCycleNo(),
CurrentDayIndex: resp.GetCurrentDayIndex(),
NextDayIndex: resp.GetNextDayIndex(),
CheckinDay: resp.GetCheckinDay(),
LastCheckinDay: resp.GetLastCheckinDay(),
ConfigVersion: resp.GetConfigVersion(),
Rewards: items,
ServerTimeMS: resp.GetServerTimeMs(),
NextRefreshAtMS: resp.GetNextRefreshAtMs(),
}
}
func checkinRewardGroupIDs(items []*activityv1.SevenDayCheckInRewardStatus) []int64 {
ids := make([]int64, 0, len(items))
for _, item := range items {
if item.GetResourceGroupId() > 0 {
ids = append(ids, item.GetResourceGroupId())
}
}
return ids
}
func checkinResourceGroupFromProto(group *walletv1.ResourceGroup) checkinResourceGroupData {
if group == nil {
return checkinResourceGroupData{}
}
items := make([]checkinResourceGroupItemData, 0, len(group.GetItems()))
for _, item := range group.GetItems() {
items = append(items, checkinResourceGroupItemData{
GroupItemID: item.GetGroupItemId(),
ItemType: item.GetItemType(),
ResourceID: item.GetResourceId(),
Resource: checkinResourcePointerFromProto(item.GetResource()),
WalletAssetType: item.GetWalletAssetType(),
WalletAssetAmount: item.GetWalletAssetAmount(),
Quantity: item.GetQuantity(),
DurationMS: item.GetDurationMs(),
SortOrder: item.GetSortOrder(),
})
}
return checkinResourceGroupData{
GroupID: group.GetGroupId(),
GroupCode: group.GetGroupCode(),
Name: group.GetName(),
Status: group.GetStatus(),
Description: group.GetDescription(),
SortOrder: group.GetSortOrder(),
Items: items,
CreatedAtMS: group.GetCreatedAtMs(),
UpdatedAtMS: group.GetUpdatedAtMs(),
}
}
func checkinResourcePointerFromProto(item *walletv1.Resource) *checkinResourceData {
if item == nil || item.GetResourceId() == 0 {
return nil
}
resource := checkinResourceFromProto(item)
return &resource
}
func checkinResourceFromProto(item *walletv1.Resource) checkinResourceData {
if item == nil {
return checkinResourceData{}
}
return checkinResourceData{
ResourceID: item.GetResourceId(),
ResourceCode: item.GetResourceCode(),
ResourceType: item.GetResourceType(),
Name: item.GetName(),
Status: item.GetStatus(),
Grantable: item.GetGrantable(),
GrantStrategy: item.GetGrantStrategy(),
WalletAssetType: item.GetWalletAssetType(),
WalletAssetAmount: item.GetWalletAssetAmount(),
UsageScopes: item.GetUsageScopes(),
AssetURL: item.GetAssetUrl(),
PreviewURL: item.GetPreviewUrl(),
AnimationURL: item.GetAnimationUrl(),
MetadataJSON: item.GetMetadataJson(),
SortOrder: item.GetSortOrder(),
CreatedAtMS: item.GetCreatedAtMs(),
UpdatedAtMS: item.GetUpdatedAtMs(),
}
}

View File

@ -94,18 +94,24 @@ func (h *Handler) listH5Links(writer http.ResponseWriter, request *http.Request)
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
}
// listAppBanners 返回后台 APP配置/BANNER配置 中维护的首页 banner。
// listAppBanners 返回后台 APP配置/BANNER配置 中维护的指定显示范围 banner。
func (h *Handler) listAppBanners(writer http.ResponseWriter, request *http.Request) {
if h.appConfigReader == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
displayScope := appconfig.NormalizeBannerDisplayScope(firstQuery(request, "display_scope", "displayScope"))
if displayScope == "" {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "display_scope is invalid")
return
}
items, err := h.appConfigReader.ListBanners(request.Context(), appconfig.BannerQuery{
AppCode: appcode.FromContext(request.Context()),
Platform: httpkit.FirstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"),
RegionID: optionalInt64Query(request, "region_id"),
Country: httpkit.FirstQueryOrHeader(request, "country", "X-Country-Code", "X-App-Country"),
AppCode: appcode.FromContext(request.Context()),
DisplayScope: displayScope,
Platform: httpkit.FirstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"),
RegionID: optionalInt64Query(request, "region_id"),
Country: httpkit.FirstQueryOrHeader(request, "country", "X-Country-Code", "X-App-Country"),
})
if err != nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
@ -176,6 +182,16 @@ func optionalInt64Query(request *http.Request, key string) int64 {
return parsed
}
func firstQuery(request *http.Request, keys ...string) string {
values := request.URL.Query()
for _, key := range keys {
if value := strings.TrimSpace(values.Get(key)); value != "" {
return value
}
}
return ""
}
func normalizeAppPlatform(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}

View File

@ -38,6 +38,7 @@ type Handler struct {
growthLevelClient client.GrowthLevelClient
achievementClient client.AchievementClient
registrationReward client.RegistrationRewardClient
sevenDayCheckIn client.SevenDayCheckInClient
broadcastClient client.BroadcastClient
gameClient client.GameClient
appConfigReader appapi.ConfigReader
@ -190,6 +191,11 @@ func (h *Handler) SetRegistrationRewardClient(registrationReward client.Registra
h.registrationReward = registrationReward
}
// SetSevenDayCheckInClient 注入 activity-service 七日签到 client。
func (h *Handler) SetSevenDayCheckInClient(sevenDayCheckIn client.SevenDayCheckInClient) {
h.sevenDayCheckIn = sevenDayCheckIn
}
// SetBroadcastClient 注入 activity-service 播报群成员关系 client。
func (h *Handler) SetBroadcastClient(broadcastClient client.BroadcastClient) {
h.broadcastClient = broadcastClient

View File

@ -128,6 +128,8 @@ type TaskHandlers struct {
ListTaskTabs http.HandlerFunc
ClaimTaskReward http.HandlerFunc
GetRegistrationRewardEligibility http.HandlerFunc
GetSevenDayCheckInStatus http.HandlerFunc
SignSevenDayCheckIn http.HandlerFunc
}
type LevelHandlers struct {
@ -317,6 +319,8 @@ func (r routes) registerMessageRoutes() {
func (r routes) registerTaskRoutes() {
h := r.config.Task
r.profile("/activities/registration-reward/eligibility", http.MethodGet, h.GetRegistrationRewardEligibility)
r.profile("/activities/seven-day-checkin", http.MethodGet, h.GetSevenDayCheckInStatus)
r.profile("/activities/seven-day-checkin/sign", http.MethodPost, h.SignSevenDayCheckIn)
r.profile("/tasks/tabs", http.MethodGet, h.ListTaskTabs)
r.profile("/tasks/claim", http.MethodPost, h.ClaimTaskReward)
}

View File

@ -1122,6 +1122,10 @@ func (f *fakeWalletClient) ListGiftConfigs(context.Context, *walletv1.ListGiftCo
return &walletv1.ListGiftConfigsResponse{}, nil
}
func (f *fakeWalletClient) ListGiftTypeConfigs(context.Context, *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error) {
return &walletv1.ListGiftTypeConfigsResponse{}, nil
}
func (f *fakeWalletClient) GrantResource(_ context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error) {
f.lastGrantResource = req
if f.grantResourceErr != nil {
@ -2191,24 +2195,39 @@ func TestAppBootstrapIsPublicAndLightweight(t *testing.T) {
}
}
type captureAppConfigReader struct {
appconfig.StaticReader
bannerQuery appconfig.BannerQuery
}
func (r *captureAppConfigReader) ListBanners(ctx context.Context, query appconfig.BannerQuery) ([]appconfig.Banner, error) {
r.bannerQuery = query
return r.StaticReader.ListBanners(ctx, query)
}
func TestListAppBannersReturnsAdminAppConfig(t *testing.T) {
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
handler.SetAppConfigReader(appconfig.StaticReader{Banners: []appconfig.Banner{
reader := &captureAppConfigReader{StaticReader: appconfig.StaticReader{Banners: []appconfig.Banner{
{
ID: 8,
CoverURL: "https://cdn.example.com/banner.png",
BannerType: "h5",
Param: "https://h5.example.com/activity",
Platform: "android",
SortOrder: 10,
RegionID: 1,
CountryCode: "CN",
Description: "homepage",
UpdatedAtMs: 1700000002000,
ID: 8,
CoverURL: "https://cdn.example.com/banner.png",
RoomSmallImageURL: "https://cdn.example.com/banner-small.png",
BannerType: "h5",
DisplayScope: "room",
Param: "https://h5.example.com/activity",
Platform: "android",
SortOrder: 10,
RegionID: 1,
CountryCode: "CN",
Description: "room banner",
StartsAtMs: 1700000000000,
EndsAtMs: 1800000000000,
UpdatedAtMs: 1700000002000,
},
}})
}}}
handler.SetAppConfigReader(reader)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/app/banners?platform=android&country=CN&region_id=1", nil)
request := httptest.NewRequest(http.MethodGet, "/api/v1/app/banners?display_scope=room&platform=android&country=CN&region_id=1", nil)
request.Header.Set("X-Request-ID", "req-app-banners")
request.Header.Set("X-App-Package", "com.org.laluparty")
recorder := httptest.NewRecorder()
@ -2226,6 +2245,9 @@ func TestListAppBannersReturnsAdminAppConfig(t *testing.T) {
if response.Code != httpkit.CodeOK || !ok {
t.Fatalf("unexpected envelope: %+v", response)
}
if reader.bannerQuery.DisplayScope != "room" || reader.bannerQuery.Platform != "android" || reader.bannerQuery.RegionID != 1 || reader.bannerQuery.Country != "CN" {
t.Fatalf("banner query mismatch: %+v", reader.bannerQuery)
}
if data["total"].(float64) != 1 {
t.Fatalf("banner total mismatch: %+v", data)
}
@ -2237,7 +2259,10 @@ func TestListAppBannersReturnsAdminAppConfig(t *testing.T) {
if !ok || first["cover_url"] != "https://cdn.example.com/banner.png" || first["type"] != "h5" || first["param"] != "https://h5.example.com/activity" {
t.Fatalf("banner item mismatch: %+v", first)
}
if first["platform"] != "android" || first["sort_order"].(float64) != 10 || first["updated_at_ms"].(float64) != 1700000002000 {
if first["display_scope"] != "room" || first["room_small_image_url"] != "https://cdn.example.com/banner-small.png" {
t.Fatalf("banner display fields mismatch: %+v", first)
}
if first["platform"] != "android" || first["sort_order"].(float64) != 10 || first["starts_at_ms"].(float64) != 1700000000000 || first["ends_at_ms"].(float64) != 1800000000000 || first["updated_at_ms"].(float64) != 1700000002000 {
t.Fatalf("banner metadata mismatch: %+v", first)
}
}

View File

@ -44,6 +44,14 @@ type giftConfigData struct {
RegionIDs []int64 `json:"region_ids"`
}
type giftTypeConfigData struct {
TypeCode string `json:"type_code"`
Name string `json:"name"`
TabKey string `json:"tab_key"`
Status string `json:"status"`
SortOrder int32 `json:"sort_order"`
}
func giftFromProto(gift *walletv1.GiftConfig) giftConfigData {
if gift == nil {
return giftConfigData{}
@ -71,6 +79,19 @@ func giftFromProto(gift *walletv1.GiftConfig) giftConfigData {
}
}
func giftTypeFromProto(item *walletv1.GiftTypeConfig) giftTypeConfigData {
if item == nil {
return giftTypeConfigData{}
}
return giftTypeConfigData{
TypeCode: item.GetTypeCode(),
Name: item.GetName(),
TabKey: item.GetTabKey(),
Status: item.GetStatus(),
SortOrder: item.GetSortOrder(),
}
}
func resourceFromProto(item *walletv1.Resource) resourceData {
if item == nil {
return resourceData{}

View File

@ -473,6 +473,19 @@ func (h *Handler) getRoomGiftPanel(writer http.ResponseWriter, request *http.Req
httpkit.WriteRPCError(writer, request, err)
return
}
giftTypeResp, err := h.walletClient.ListGiftTypeConfigs(request.Context(), &walletv1.ListGiftTypeConfigsRequest{
RequestId: httpkit.RequestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
ActiveOnly: true,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
giftTypes := make([]giftTypeConfigData, 0, len(giftTypeResp.GetGiftTypes()))
for _, item := range giftTypeResp.GetGiftTypes() {
giftTypes = append(giftTypes, giftTypeFromProto(item))
}
giftResp, err := h.walletClient.ListGiftConfigs(request.Context(), &walletv1.ListGiftConfigsRequest{
RequestId: httpkit.RequestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
@ -490,11 +503,12 @@ func (h *Handler) getRoomGiftPanel(writer http.ResponseWriter, request *http.Req
for _, gift := range giftResp.GetGifts() {
gifts = append(gifts, giftFromProto(gift))
}
gifts = filterGiftPanelGifts(gifts, giftTypes)
recipientProfiles := h.roomDisplayProfileMap(request, roomGiftRecipientUserIDs(snapshot))
httpkit.WriteOK(writer, request, roomGiftPanelData{
CoinBalance: coinBalanceFromProto(balances),
Recipients: roomGiftRecipients(snapshot, recipientProfiles),
Tabs: roomGiftTabs(gifts),
Tabs: roomGiftTabs(gifts, giftTypes),
Gifts: gifts,
QuantityPresets: []int32{1, 9, 99, 999},
})
@ -1198,8 +1212,34 @@ func roomGiftRecipients(snapshot *roomv1.RoomSnapshot, profiles map[int64]roomDi
return recipients
}
func roomGiftTabs(gifts []giftConfigData) []roomGiftTabData {
func roomGiftTabs(gifts []giftConfigData, giftTypes []giftTypeConfigData) []roomGiftTabData {
tabs := []roomGiftTabData{{Key: "all", Order: 0}}
if len(giftTypes) > 0 {
hasGift := make(map[string]bool, len(gifts))
for _, gift := range gifts {
key := strings.TrimSpace(gift.GiftTypeCode)
if key != "" {
hasGift[key] = true
}
}
for _, giftType := range giftTypes {
typeCode := strings.TrimSpace(giftType.TypeCode)
if typeCode == "" || !hasGift[typeCode] {
continue
}
tabKey := strings.TrimSpace(giftType.TabKey)
if tabKey == "" {
tabKey = typeCode
}
tabs = append(tabs, roomGiftTabData{
Key: typeCode,
GiftTypeCode: typeCode,
Label: tabKey,
Order: giftType.SortOrder,
})
}
return tabs
}
seen := map[string]bool{"all": true}
for _, gift := range gifts {
key := strings.TrimSpace(gift.GiftTypeCode)
@ -1212,6 +1252,26 @@ func roomGiftTabs(gifts []giftConfigData) []roomGiftTabData {
return tabs
}
func filterGiftPanelGifts(gifts []giftConfigData, giftTypes []giftTypeConfigData) []giftConfigData {
if len(giftTypes) == 0 {
return gifts
}
activeTypes := make(map[string]bool, len(giftTypes))
for _, giftType := range giftTypes {
typeCode := strings.TrimSpace(giftType.TypeCode)
if typeCode != "" {
activeTypes[typeCode] = true
}
}
out := make([]giftConfigData, 0, len(gifts))
for _, gift := range gifts {
if activeTypes[strings.TrimSpace(gift.GiftTypeCode)] {
out = append(out, gift)
}
}
return out
}
func coinBalanceFromProto(resp *walletv1.GetBalancesResponse) int64 {
for _, balance := range resp.GetBalances() {
if balance.GetAssetType() == "COIN" {

View File

@ -0,0 +1,28 @@
package roomapi
import "testing"
func TestRoomGiftTabsUsesTypeCodeAsKeyAndTabKeyAsLabel(t *testing.T) {
tabs := roomGiftTabs(
[]giftConfigData{
{GiftTypeCode: "normal"},
{GiftTypeCode: "lucky"},
{GiftTypeCode: "disabled"},
},
[]giftTypeConfigData{
{TypeCode: "normal", Name: "普通礼物", TabKey: "Gift", SortOrder: 10},
{TypeCode: "lucky", Name: "幸运礼物", TabKey: "Lucky", SortOrder: 20},
{TypeCode: "empty", Name: "空礼物", TabKey: "Empty", SortOrder: 30},
},
)
if len(tabs) != 3 {
t.Fatalf("tab count mismatch: got %d tabs=%+v", len(tabs), tabs)
}
if tabs[1].Key != "normal" || tabs[1].GiftTypeCode != "normal" || tabs[1].Label != "Gift" || tabs[1].Order != 10 {
t.Fatalf("normal tab mismatch: %+v", tabs[1])
}
if tabs[2].Key != "lucky" || tabs[2].GiftTypeCode != "lucky" || tabs[2].Label != "Lucky" || tabs[2].Order != 20 {
t.Fatalf("lucky tab mismatch: %+v", tabs[2])
}
}

View File

@ -124,8 +124,10 @@ type roomGiftRecipientData struct {
}
type roomGiftTabData struct {
Key string `json:"key"`
Order int32 `json:"order"`
Key string `json:"key"`
GiftTypeCode string `json:"gift_type_code,omitempty"`
Label string `json:"label,omitempty"`
Order int32 `json:"order"`
}
type updateRoomProfileData struct {
@ -191,6 +193,7 @@ type roomSeatData struct {
MicSessionID string `json:"mic_session_id,omitempty"`
PublishDeadlineMS int64 `json:"publish_deadline_ms,omitempty"`
MicMuted bool `json:"mic_muted"`
SeatStatus string `json:"seat_status,omitempty"`
}
type roomRankItemData struct {
@ -363,6 +366,7 @@ func roomSeatDataFromSnapshot(snapshot *roomv1.RoomSnapshot) []roomSeatData {
MicSessionID: seat.GetMicSessionId(),
PublishDeadlineMS: seat.GetPublishDeadlineMs(),
MicMuted: seat.GetMicMuted(),
SeatStatus: seat.GetSeatStatus(),
})
}
return items

View File

@ -62,6 +62,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
AchievementClient: h.achievementClient,
WalletClient: h.walletClient,
RegistrationReward: h.registrationReward,
SevenDayCheckIn: h.sevenDayCheckIn,
})
gameAPI := gameapi.New(gameapi.Config{
GameClient: h.gameClient,

View File

@ -1,3 +1,7 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
CREATE DATABASE IF NOT EXISTS hyapp_notice
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
@ -5,24 +9,24 @@ CREATE DATABASE IF NOT EXISTS hyapp_notice
USE hyapp_notice;
CREATE TABLE IF NOT EXISTS notice_delivery_events (
source_name VARCHAR(64) NOT NULL COMMENT '事件源表或服务名,例如 wallet_outbox',
source_name VARCHAR(64) NOT NULL COMMENT '事件源服务或表名,例如钱包 outbox',
app_code VARCHAR(32) NOT NULL COMMENT '租户编码,必须和源 outbox 保持一致',
source_event_id VARCHAR(128) NOT NULL COMMENT '源事件幂等键',
channel VARCHAR(64) NOT NULL COMMENT '投递通道,例如 tencent_im_c2c',
notice_type VARCHAR(64) NOT NULL COMMENT '业务通知类型,例如 WalletBalanceChanged',
channel VARCHAR(64) NOT NULL COMMENT '投递通道,例如腾讯云 IM 单聊',
notice_type VARCHAR(64) NOT NULL COMMENT '业务通知类型,例如钱包余额变更',
target_user_id BIGINT NOT NULL COMMENT '私有通知目标用户',
status VARCHAR(32) NOT NULL COMMENT 'delivering/delivered/retryable/failed',
retry_count INT NOT NULL DEFAULT 0 COMMENT '已尝试次数,达到上限后进入 failed',
locked_by VARCHAR(128) NOT NULL DEFAULT '' COMMENT '当前抢占 worker',
status VARCHAR(32) NOT NULL COMMENT '投递状态:投递中、已投递、可重试、失败',
retry_count INT NOT NULL DEFAULT 0 COMMENT '已尝试次数,达到上限后进入失败',
locked_by VARCHAR(128) NOT NULL DEFAULT '' COMMENT '当前抢占工作节点',
lock_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT '抢占锁过期时间UTC epoch ms',
next_retry_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '下一次允许重试时间UTC epoch ms',
payload_json JSON NOT NULL COMMENT '发送给客户端的最终负载快照',
last_error TEXT NULL COMMENT '最后一次投递失败原因,限制在应用层截断',
delivered_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '成功投递时间UTC epoch ms',
created_at_ms BIGINT NOT NULL COMMENT 'notice 位点创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT 'notice 位点更新时间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 (source_name, app_code, source_event_id, channel),
KEY idx_notice_delivery_pending (status, next_retry_at_ms, lock_until_ms, updated_at_ms),
KEY idx_notice_delivery_target (app_code, target_user_id, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='notice-service 外部通知投递位点和死信表';
COMMENT='通知服务外部通知投递位点和死信表';

View File

@ -1,5 +1,7 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用 utf8mb4 连接字符集,保证中文注释、中文名称和 emoji 在导入时不被降级。
CREATE DATABASE IF NOT EXISTS hyapp_room
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
@ -7,170 +9,170 @@ CREATE DATABASE IF NOT EXISTS hyapp_room
USE hyapp_room;
CREATE TABLE IF NOT EXISTS rooms (
app_code VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
room_short_id VARCHAR(32) NOT NULL DEFAULT '',
owner_user_id BIGINT NOT NULL,
seat_count INT NOT NULL,
mode VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
room_password_hash VARCHAR(128) NOT NULL DEFAULT '',
close_reason VARCHAR(512) NOT NULL DEFAULT '',
closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '',
closed_at_ms BIGINT NOT NULL DEFAULT 0,
visible_region_id BIGINT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
room_short_id VARCHAR(32) NOT NULL DEFAULT '' COMMENT '房间短号 ID',
owner_user_id BIGINT NOT NULL COMMENT '房主用户 ID',
seat_count INT NOT NULL COMMENT '麦位数量',
mode VARCHAR(64) NOT NULL COMMENT '业务模式',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
room_password_hash VARCHAR(128) NOT NULL DEFAULT '' COMMENT '房间密码哈希值',
close_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '关闭原因',
closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关闭操作管理员 ID',
closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '关闭操作管理员名称',
closed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '关闭时间UTC epoch ms',
visible_region_id BIGINT NOT NULL DEFAULT 0 COMMENT '可见区域 ID',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, room_id),
UNIQUE KEY uk_rooms_short_id (app_code, room_short_id),
UNIQUE KEY uk_rooms_owner_user (app_code, owner_user_id),
KEY idx_rooms_region_status (app_code, visible_region_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间核心状态持久表';
CREATE TABLE IF NOT EXISTS room_list_entries (
app_code VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
room_short_id VARCHAR(32) NOT NULL DEFAULT '',
visible_region_id BIGINT NOT NULL DEFAULT 0,
owner_user_id BIGINT NOT NULL,
title VARCHAR(128) NOT NULL DEFAULT '',
cover_url VARCHAR(512) NOT NULL DEFAULT '',
mode VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
locked BOOLEAN NOT NULL DEFAULT FALSE,
close_reason VARCHAR(512) NOT NULL DEFAULT '',
closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '',
closed_at_ms BIGINT NOT NULL DEFAULT 0,
heat BIGINT NOT NULL DEFAULT 0,
online_count INT NOT NULL DEFAULT 0,
seat_count INT NOT NULL DEFAULT 0,
occupied_seat_count INT NOT NULL DEFAULT 0,
sort_score BIGINT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
room_short_id VARCHAR(32) NOT NULL DEFAULT '' COMMENT '房间短号 ID',
visible_region_id BIGINT NOT NULL DEFAULT 0 COMMENT '可见区域 ID',
owner_user_id BIGINT NOT NULL COMMENT '房主用户 ID',
title VARCHAR(128) NOT NULL DEFAULT '' COMMENT '标题',
cover_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '封面 URL',
mode VARCHAR(64) NOT NULL COMMENT '业务模式',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
locked BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否锁定',
close_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '关闭原因',
closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关闭操作管理员 ID',
closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '关闭操作管理员名称',
closed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '关闭时间UTC epoch ms',
heat BIGINT NOT NULL DEFAULT 0 COMMENT '热度值',
online_count INT NOT NULL DEFAULT 0 COMMENT '在线数量',
seat_count INT NOT NULL DEFAULT 0 COMMENT '麦位数量',
occupied_seat_count INT NOT NULL DEFAULT 0 COMMENT '占用麦位数量',
sort_score 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),
KEY idx_room_list_short_id (app_code, room_short_id),
KEY idx_room_list_region_hot (app_code, visible_region_id, status, sort_score DESC, room_id),
KEY idx_room_list_region_new (app_code, visible_region_id, status, created_at_ms DESC, room_id),
KEY idx_room_list_owner (app_code, owner_user_id, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间列表项表';
CREATE TABLE IF NOT EXISTS room_user_presence (
app_code VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
room_id VARCHAR(64) NOT NULL,
role VARCHAR(32) NOT NULL,
publish_state VARCHAR(32) NOT NULL DEFAULT '',
mic_session_id VARCHAR(128) NOT NULL DEFAULT '',
room_version BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
last_seen_at_ms BIGINT NOT NULL DEFAULT 0,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
user_id BIGINT NOT NULL COMMENT '用户 ID',
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
role VARCHAR(32) NOT NULL COMMENT '角色',
publish_state VARCHAR(32) NOT NULL DEFAULT '' COMMENT '推流状态',
mic_session_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '麦位会话 ID',
room_version BIGINT NOT NULL COMMENT 'Room Cell 状态版本',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
last_seen_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '最后活跃时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, user_id),
KEY idx_room_user_presence_room (app_code, room_id, status),
KEY idx_room_user_presence_updated (app_code, status, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间用户在线状态表';
CREATE TABLE IF NOT EXISTS room_snapshots (
app_code VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
room_version BIGINT NOT NULL,
payload LONGBLOB NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
room_version BIGINT NOT NULL COMMENT 'Room Cell 状态版本',
payload LONGBLOB NOT NULL COMMENT '业务负载快照',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, room_id),
INDEX idx_room_snapshots_version (app_code, room_id, room_version)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Room Cell 快照恢复表';
CREATE TABLE IF NOT EXISTS room_command_log (
app_code VARCHAR(32) NOT NULL,
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
room_id VARCHAR(64) NOT NULL,
room_version BIGINT NOT NULL,
command_id VARCHAR(128) NOT NULL,
command_type VARCHAR(64) NOT NULL,
owner_node_id VARCHAR(128) NOT NULL DEFAULT '',
lease_token VARCHAR(128) NOT NULL DEFAULT '',
payload LONGBLOB NOT NULL,
replayable BOOLEAN NOT NULL,
created_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键 ID',
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
room_version BIGINT NOT NULL COMMENT 'Room Cell 状态版本',
command_id VARCHAR(128) NOT NULL COMMENT '业务命令幂等 ID',
command_type VARCHAR(64) NOT NULL COMMENT '命令类型',
owner_node_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '房主节点 ID',
lease_token VARCHAR(128) NOT NULL DEFAULT '' COMMENT '租约令牌',
payload LONGBLOB NOT NULL COMMENT '业务负载快照',
replayable BOOLEAN NOT NULL COMMENT '是否可重放',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
UNIQUE KEY uk_room_command (app_code, room_id, command_id),
KEY idx_room_command_replay (app_code, room_id, room_version)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Room Cell 命令日志表';
CREATE TABLE IF NOT EXISTS room_outbox (
app_code VARCHAR(32) NOT NULL,
event_id VARCHAR(128) NOT NULL,
event_type VARCHAR(64) NOT NULL,
room_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
worker_id VARCHAR(128) NOT NULL DEFAULT '',
lock_until_ms BIGINT NULL,
envelope LONGBLOB NOT NULL,
created_at_ms BIGINT NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
next_retry_at_ms BIGINT NOT NULL DEFAULT 0,
last_error TEXT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
event_id VARCHAR(128) NOT NULL COMMENT '事件幂等 ID',
event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
worker_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'worker ID',
lock_until_ms BIGINT NULL COMMENT '锁定过期时间UTC epoch ms',
envelope LONGBLOB NOT NULL COMMENT '事件信封',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
next_retry_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '下一次重试时间UTC epoch ms',
last_error TEXT NULL COMMENT '最后一次失败原因',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, event_id),
KEY idx_room_outbox_pending (app_code, status, next_retry_at_ms, created_at_ms),
KEY idx_room_outbox_retry (app_code, status, next_retry_at_ms, created_at_ms),
KEY idx_room_outbox_claim (app_code, status, lock_until_ms, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间事件 outbox 表';
CREATE TABLE IF NOT EXISTS room_user_feed_entries (
app_code VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
feed_type VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
user_id BIGINT NOT NULL COMMENT '用户 ID',
feed_type VARCHAR(32) NOT NULL COMMENT '动态类型',
room_id VARCHAR(64) NOT NULL COMMENT '房间 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, user_id, feed_type, room_id),
KEY idx_room_user_feed_list (app_code, user_id, feed_type, updated_at_ms DESC, room_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间用户动态项表';
CREATE TABLE IF NOT EXISTS room_follows (
app_code VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
room_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
followed_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
user_id BIGINT NOT NULL COMMENT '用户 ID',
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
followed_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, room_id),
KEY idx_room_follows_user_list (app_code, user_id, status, followed_at_ms DESC, room_id),
KEY idx_room_follows_room (app_code, room_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间关注表';
CREATE TABLE IF NOT EXISTS room_region_pins (
id BIGINT NOT NULL AUTO_INCREMENT,
app_code VARCHAR(32) NOT NULL,
visible_region_id BIGINT NOT NULL,
room_id VARCHAR(64) NOT NULL,
weight BIGINT NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL,
pinned_at_ms BIGINT NOT NULL,
expires_at_ms BIGINT NOT NULL,
cancelled_at_ms BIGINT NOT NULL DEFAULT 0,
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
cancelled_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键 ID',
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
visible_region_id BIGINT NOT NULL COMMENT '可见区域 ID',
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
weight BIGINT NOT NULL DEFAULT 0 COMMENT '权重',
status VARCHAR(32) NOT NULL COMMENT '业务状态',
pinned_at_ms BIGINT NOT NULL COMMENT '置顶时间UTC epoch ms',
expires_at_ms BIGINT NOT NULL COMMENT '过期时间UTC epoch ms',
cancelled_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '取消时间UTC epoch ms',
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建管理员 ID',
cancelled_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '取消操作管理员 ID',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (id),
UNIQUE KEY uk_room_region_pins_room (app_code, visible_region_id, room_id),
KEY idx_room_region_pins_active (app_code, visible_region_id, status, expires_at_ms, weight DESC, room_id),
KEY idx_room_region_pins_room (app_code, room_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间区域置顶表';
CREATE TABLE IF NOT EXISTS room_seat_configs (
app_code VARCHAR(32) NOT NULL,
allowed_seat_counts VARCHAR(128) NOT NULL,
default_seat_count INT NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
allowed_seat_counts VARCHAR(128) NOT NULL COMMENT '允许麦位数量',
default_seat_count INT NOT NULL COMMENT '默认麦位数量',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间麦位配置表';
INSERT IGNORE INTO room_seat_configs (
app_code,

Some files were not shown because too many files have changed in this diff Show More