Fix databi statistics dimensions and backfill

This commit is contained in:
zhx 2026-06-08 12:01:55 +08:00
parent 1b6c77c6dc
commit b49a2109c7
25 changed files with 9164 additions and 5154 deletions

File diff suppressed because it is too large Load Diff

View File

@ -63,6 +63,8 @@ message RoomUserJoined {
// visible_region_id / room-service
int64 visible_region_id = 3;
RoomEntryVehicleSnapshot entry_vehicle = 4;
int64 country_id = 5;
int64 region_id = 6;
}
// RoomUserLeft
@ -154,6 +156,8 @@ message RoomGiftSent {
string command_id = 8;
string pool_id = 9;
int64 coin_spent = 10;
int64 country_id = 11;
int64 region_id = 12;
}
// RoomHeatChanged

File diff suppressed because it is too large Load Diff

View File

@ -468,6 +468,8 @@ message JoinRoomRequest {
// password room-service command log
string password = 3;
RoomEntryVehicleSnapshot entry_vehicle = 4;
int64 actor_country_id = 5;
int64 actor_region_id = 6;
}
// JoinRoomResponse
@ -841,6 +843,8 @@ message SendGiftRequest {
int64 target_agency_owner_user_id = 10;
// sender_region_id gateway user-service wallet
int64 sender_region_id = 11;
// sender_country_id gateway user-service
int64 sender_country_id = 12;
}
// SendGiftResponse

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc-gen-go-grpc v1.5.1
// - protoc v7.35.0
// source: proto/room/v1/room.proto
@ -470,100 +470,100 @@ type RoomCommandServiceServer interface {
type UnimplementedRoomCommandServiceServer struct{}
func (UnimplementedRoomCommandServiceServer) CreateRoom(context.Context, *CreateRoomRequest) (*CreateRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateRoom not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateRoomProfile not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateRoomProfile not implemented")
}
func (UnimplementedRoomCommandServiceServer) SaveRoomBackground(context.Context, *SaveRoomBackgroundRequest) (*SaveRoomBackgroundResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SaveRoomBackground not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SaveRoomBackground not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetRoomBackground(context.Context, *SetRoomBackgroundRequest) (*SetRoomBackgroundResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetRoomBackground not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetRoomBackground not implemented")
}
func (UnimplementedRoomCommandServiceServer) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method JoinRoom not implemented")
return nil, status.Errorf(codes.Unimplemented, "method JoinRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) RoomHeartbeat(context.Context, *RoomHeartbeatRequest) (*RoomHeartbeatResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RoomHeartbeat not implemented")
return nil, status.Errorf(codes.Unimplemented, "method RoomHeartbeat not implemented")
}
func (UnimplementedRoomCommandServiceServer) LeaveRoom(context.Context, *LeaveRoomRequest) (*LeaveRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method LeaveRoom not implemented")
return nil, status.Errorf(codes.Unimplemented, "method LeaveRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) CloseRoom(context.Context, *CloseRoomRequest) (*CloseRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CloseRoom not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CloseRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoom(context.Context, *AdminUpdateRoomRequest) (*AdminUpdateRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoom not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminUpdateRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) AdminDeleteRoom(context.Context, *AdminDeleteRoomRequest) (*AdminDeleteRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminDeleteRoom not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminDeleteRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoomRocketConfig(context.Context, *AdminUpdateRoomRocketConfigRequest) (*AdminUpdateRoomRocketConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoomRocketConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminUpdateRoomRocketConfig not implemented")
}
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoomSeatConfig(context.Context, *AdminUpdateRoomSeatConfigRequest) (*AdminUpdateRoomSeatConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoomSeatConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminUpdateRoomSeatConfig not implemented")
}
func (UnimplementedRoomCommandServiceServer) AdminCreateRoomPin(context.Context, *AdminCreateRoomPinRequest) (*AdminCreateRoomPinResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminCreateRoomPin not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminCreateRoomPin not implemented")
}
func (UnimplementedRoomCommandServiceServer) AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminCancelRoomPin not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminCancelRoomPin not implemented")
}
func (UnimplementedRoomCommandServiceServer) MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MicUp not implemented")
return nil, status.Errorf(codes.Unimplemented, "method MicUp not implemented")
}
func (UnimplementedRoomCommandServiceServer) MicDown(context.Context, *MicDownRequest) (*MicDownResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MicDown not implemented")
return nil, status.Errorf(codes.Unimplemented, "method MicDown not implemented")
}
func (UnimplementedRoomCommandServiceServer) ChangeMicSeat(context.Context, *ChangeMicSeatRequest) (*ChangeMicSeatResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ChangeMicSeat not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ChangeMicSeat not implemented")
}
func (UnimplementedRoomCommandServiceServer) ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ConfirmMicPublishing not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ConfirmMicPublishing not implemented")
}
func (UnimplementedRoomCommandServiceServer) MicHeartbeat(context.Context, *MicHeartbeatRequest) (*MicHeartbeatResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MicHeartbeat not implemented")
return nil, status.Errorf(codes.Unimplemented, "method MicHeartbeat not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetMicMute not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetMicMute not implemented")
}
func (UnimplementedRoomCommandServiceServer) ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ApplyRTCEvent not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ApplyRTCEvent not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetMicSeatLock not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetMicSeatLock not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetChatEnabled(context.Context, *SetChatEnabledRequest) (*SetChatEnabledResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetChatEnabled not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetChatEnabled not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetRoomPassword(context.Context, *SetRoomPasswordRequest) (*SetRoomPasswordResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetRoomPassword not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetRoomPassword not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetRoomAdmin(context.Context, *SetRoomAdminRequest) (*SetRoomAdminResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetRoomAdmin not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetRoomAdmin not implemented")
}
func (UnimplementedRoomCommandServiceServer) MuteUser(context.Context, *MuteUserRequest) (*MuteUserResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MuteUser not implemented")
return nil, status.Errorf(codes.Unimplemented, "method MuteUser not implemented")
}
func (UnimplementedRoomCommandServiceServer) KickUser(context.Context, *KickUserRequest) (*KickUserResponse, error) {
return nil, status.Error(codes.Unimplemented, "method KickUser not implemented")
return nil, status.Errorf(codes.Unimplemented, "method KickUser not implemented")
}
func (UnimplementedRoomCommandServiceServer) UnbanUser(context.Context, *UnbanUserRequest) (*UnbanUserResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UnbanUser not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UnbanUser not implemented")
}
func (UnimplementedRoomCommandServiceServer) SystemEvictUser(context.Context, *SystemEvictUserRequest) (*SystemEvictUserResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SystemEvictUser not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SystemEvictUser not implemented")
}
func (UnimplementedRoomCommandServiceServer) SendGift(context.Context, *SendGiftRequest) (*SendGiftResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SendGift not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SendGift not implemented")
}
func (UnimplementedRoomCommandServiceServer) FollowRoom(context.Context, *FollowRoomRequest) (*FollowRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method FollowRoom not implemented")
return nil, status.Errorf(codes.Unimplemented, "method FollowRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) UnfollowRoom(context.Context, *UnfollowRoomRequest) (*UnfollowRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UnfollowRoom not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UnfollowRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) mustEmbedUnimplementedRoomCommandServiceServer() {}
func (UnimplementedRoomCommandServiceServer) testEmbeddedByValue() {}
@ -576,7 +576,7 @@ type UnsafeRoomCommandServiceServer interface {
}
func RegisterRoomCommandServiceServer(s grpc.ServiceRegistrar, srv RoomCommandServiceServer) {
// If the following call panics, it indicates UnimplementedRoomCommandServiceServer was
// If the following call pancis, 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.
@ -1364,10 +1364,10 @@ type RoomGuardServiceServer interface {
type UnimplementedRoomGuardServiceServer struct{}
func (UnimplementedRoomGuardServiceServer) CheckSpeakPermission(context.Context, *CheckSpeakPermissionRequest) (*CheckSpeakPermissionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CheckSpeakPermission not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CheckSpeakPermission not implemented")
}
func (UnimplementedRoomGuardServiceServer) VerifyRoomPresence(context.Context, *VerifyRoomPresenceRequest) (*VerifyRoomPresenceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method VerifyRoomPresence not implemented")
return nil, status.Errorf(codes.Unimplemented, "method VerifyRoomPresence not implemented")
}
func (UnimplementedRoomGuardServiceServer) mustEmbedUnimplementedRoomGuardServiceServer() {}
func (UnimplementedRoomGuardServiceServer) testEmbeddedByValue() {}
@ -1380,7 +1380,7 @@ type UnsafeRoomGuardServiceServer interface {
}
func RegisterRoomGuardServiceServer(s grpc.ServiceRegistrar, srv RoomGuardServiceServer) {
// If the following call panics, it indicates UnimplementedRoomGuardServiceServer was
// If the following call pancis, 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.
@ -1677,49 +1677,49 @@ type RoomQueryServiceServer interface {
type UnimplementedRoomQueryServiceServer struct{}
func (UnimplementedRoomQueryServiceServer) AdminListRooms(context.Context, *AdminListRoomsRequest) (*AdminListRoomsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminListRooms not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminListRooms not implemented")
}
func (UnimplementedRoomQueryServiceServer) AdminGetRoom(context.Context, *AdminGetRoomRequest) (*AdminGetRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminGetRoom not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminGetRoom not implemented")
}
func (UnimplementedRoomQueryServiceServer) AdminGetRoomRocketConfig(context.Context, *AdminGetRoomRocketConfigRequest) (*AdminGetRoomRocketConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminGetRoomRocketConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminGetRoomRocketConfig not implemented")
}
func (UnimplementedRoomQueryServiceServer) AdminGetRoomSeatConfig(context.Context, *AdminGetRoomSeatConfigRequest) (*AdminGetRoomSeatConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminGetRoomSeatConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminGetRoomSeatConfig not implemented")
}
func (UnimplementedRoomQueryServiceServer) AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminListRoomPins not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminListRoomPins not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRooms not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRooms not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomFeeds not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRoomFeeds not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRoomGiftLeaderboard(context.Context, *ListRoomGiftLeaderboardRequest) (*ListRoomGiftLeaderboardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomGiftLeaderboard not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRoomGiftLeaderboard not implemented")
}
func (UnimplementedRoomQueryServiceServer) GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetMyRoom not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetMyRoom not implemented")
}
func (UnimplementedRoomQueryServiceServer) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetCurrentRoom not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetCurrentRoom not implemented")
}
func (UnimplementedRoomQueryServiceServer) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRoomSnapshot not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetRoomSnapshot not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomBackgrounds not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRoomBackgrounds not implemented")
}
func (UnimplementedRoomQueryServiceServer) GetRoomRocket(context.Context, *GetRoomRocketRequest) (*GetRoomRocketResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRoomRocket not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetRoomRocket not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomOnlineUsers not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRoomOnlineUsers not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRoomBannedUsers(context.Context, *ListRoomBannedUsersRequest) (*ListRoomBannedUsersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomBannedUsers not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRoomBannedUsers not implemented")
}
func (UnimplementedRoomQueryServiceServer) mustEmbedUnimplementedRoomQueryServiceServer() {}
func (UnimplementedRoomQueryServiceServer) testEmbeddedByValue() {}
@ -1732,7 +1732,7 @@ type UnsafeRoomQueryServiceServer interface {
}
func RegisterRoomQueryServiceServer(s grpc.ServiceRegistrar, srv RoomQueryServiceServer) {
// If the following call panics, it indicates UnimplementedRoomQueryServiceServer was
// If the following call pancis, 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.

File diff suppressed because it is too large Load Diff

View File

@ -196,6 +196,8 @@ message AdminCreditCoinSellerStockRequest {
int64 operator_user_id = 9;
string reason = 10;
string app_code = 11;
int64 seller_country_id = 12;
int64 seller_region_id = 13;
}
message AdminCreditCoinSellerStockResponse {
@ -218,9 +220,10 @@ message TransferCoinFromSellerRequest {
int64 amount = 4;
string reason = 5;
string app_code = 6;
// seller_region_id target_region_id gateway user-service
// seller_region_idtarget_region_id target_country_id gateway user-service
int64 seller_region_id = 7;
int64 target_region_id = 8;
int64 target_country_id = 9;
}
// TransferCoinFromSellerResponse

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc-gen-go-grpc v1.5.1
// - protoc v7.35.0
// source: proto/wallet/v1/wallet.proto
@ -93,13 +93,13 @@ type WalletCronServiceServer interface {
type UnimplementedWalletCronServiceServer struct{}
func (UnimplementedWalletCronServiceServer) ProcessHostSalaryDailySettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessHostSalaryDailySettlementBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessHostSalaryDailySettlementBatch not implemented")
}
func (UnimplementedWalletCronServiceServer) ProcessHostSalaryHalfMonthSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessHostSalaryHalfMonthSettlementBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessHostSalaryHalfMonthSettlementBatch not implemented")
}
func (UnimplementedWalletCronServiceServer) ProcessHostSalaryMonthEndBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ProcessHostSalaryMonthEndBatch not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ProcessHostSalaryMonthEndBatch not implemented")
}
func (UnimplementedWalletCronServiceServer) mustEmbedUnimplementedWalletCronServiceServer() {}
func (UnimplementedWalletCronServiceServer) testEmbeddedByValue() {}
@ -112,7 +112,7 @@ type UnsafeWalletCronServiceServer interface {
}
func RegisterWalletCronServiceServer(s grpc.ServiceRegistrar, srv WalletCronServiceServer) {
// If the following call panics, it indicates UnimplementedWalletCronServiceServer was
// If the following call pancis, it indicates UnimplementedWalletCronServiceServer 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.
@ -1120,208 +1120,208 @@ type WalletServiceServer interface {
type UnimplementedWalletServiceServer struct{}
func (UnimplementedWalletServiceServer) DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DebitGift not implemented")
return nil, status.Errorf(codes.Unimplemented, "method DebitGift not implemented")
}
func (UnimplementedWalletServiceServer) BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error) {
return nil, status.Error(codes.Unimplemented, "method BatchDebitGift not implemented")
return nil, status.Errorf(codes.Unimplemented, "method BatchDebitGift not implemented")
}
func (UnimplementedWalletServiceServer) GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetBalances not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetBalances not implemented")
}
func (UnimplementedWalletServiceServer) GetActiveHostSalaryPolicy(context.Context, *GetActiveHostSalaryPolicyRequest) (*GetActiveHostSalaryPolicyResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetActiveHostSalaryPolicy not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetActiveHostSalaryPolicy not implemented")
}
func (UnimplementedWalletServiceServer) GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetHostSalaryProgress not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetHostSalaryProgress not implemented")
}
func (UnimplementedWalletServiceServer) AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminCreditAsset not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminCreditAsset not implemented")
}
func (UnimplementedWalletServiceServer) AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminCreditCoinSellerStock not implemented")
return nil, status.Errorf(codes.Unimplemented, "method AdminCreditCoinSellerStock not implemented")
}
func (UnimplementedWalletServiceServer) TransferCoinFromSeller(context.Context, *TransferCoinFromSellerRequest) (*TransferCoinFromSellerResponse, error) {
return nil, status.Error(codes.Unimplemented, "method TransferCoinFromSeller not implemented")
return nil, status.Errorf(codes.Unimplemented, "method TransferCoinFromSeller not implemented")
}
func (UnimplementedWalletServiceServer) ListCoinSellerSalaryExchangeRateTiers(context.Context, *ListCoinSellerSalaryExchangeRateTiersRequest) (*ListCoinSellerSalaryExchangeRateTiersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListCoinSellerSalaryExchangeRateTiers not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListCoinSellerSalaryExchangeRateTiers not implemented")
}
func (UnimplementedWalletServiceServer) ExchangeSalaryToCoin(context.Context, *ExchangeSalaryToCoinRequest) (*ExchangeSalaryToCoinResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ExchangeSalaryToCoin not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ExchangeSalaryToCoin not implemented")
}
func (UnimplementedWalletServiceServer) TransferSalaryToCoinSeller(context.Context, *TransferSalaryToCoinSellerRequest) (*TransferSalaryToCoinSellerResponse, error) {
return nil, status.Error(codes.Unimplemented, "method TransferSalaryToCoinSeller not implemented")
return nil, status.Errorf(codes.Unimplemented, "method TransferSalaryToCoinSeller not implemented")
}
func (UnimplementedWalletServiceServer) ListResources(context.Context, *ListResourcesRequest) (*ListResourcesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListResources not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListResources not implemented")
}
func (UnimplementedWalletServiceServer) GetResource(context.Context, *GetResourceRequest) (*GetResourceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetResource not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetResource not implemented")
}
func (UnimplementedWalletServiceServer) CreateResource(context.Context, *CreateResourceRequest) (*ResourceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateResource not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateResource not implemented")
}
func (UnimplementedWalletServiceServer) UpdateResource(context.Context, *UpdateResourceRequest) (*ResourceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateResource not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateResource not implemented")
}
func (UnimplementedWalletServiceServer) SetResourceStatus(context.Context, *SetResourceStatusRequest) (*ResourceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetResourceStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetResourceStatus not implemented")
}
func (UnimplementedWalletServiceServer) ListResourceGroups(context.Context, *ListResourceGroupsRequest) (*ListResourceGroupsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListResourceGroups not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListResourceGroups not implemented")
}
func (UnimplementedWalletServiceServer) GetResourceGroup(context.Context, *GetResourceGroupRequest) (*GetResourceGroupResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetResourceGroup not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetResourceGroup not implemented")
}
func (UnimplementedWalletServiceServer) CreateResourceGroup(context.Context, *CreateResourceGroupRequest) (*ResourceGroupResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateResourceGroup not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateResourceGroup not implemented")
}
func (UnimplementedWalletServiceServer) UpdateResourceGroup(context.Context, *UpdateResourceGroupRequest) (*ResourceGroupResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateResourceGroup not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateResourceGroup not implemented")
}
func (UnimplementedWalletServiceServer) SetResourceGroupStatus(context.Context, *SetResourceGroupStatusRequest) (*ResourceGroupResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetResourceGroupStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetResourceGroupStatus not implemented")
}
func (UnimplementedWalletServiceServer) ListGiftConfigs(context.Context, *ListGiftConfigsRequest) (*ListGiftConfigsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListGiftConfigs not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListGiftConfigs not implemented")
}
func (UnimplementedWalletServiceServer) ListGiftTypeConfigs(context.Context, *ListGiftTypeConfigsRequest) (*ListGiftTypeConfigsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListGiftTypeConfigs not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListGiftTypeConfigs not implemented")
}
func (UnimplementedWalletServiceServer) CreateGiftConfig(context.Context, *CreateGiftConfigRequest) (*GiftConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateGiftConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateGiftConfig not implemented")
}
func (UnimplementedWalletServiceServer) UpdateGiftConfig(context.Context, *UpdateGiftConfigRequest) (*GiftConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateGiftConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateGiftConfig not implemented")
}
func (UnimplementedWalletServiceServer) SetGiftConfigStatus(context.Context, *SetGiftConfigStatusRequest) (*GiftConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetGiftConfigStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetGiftConfigStatus not implemented")
}
func (UnimplementedWalletServiceServer) UpsertGiftTypeConfig(context.Context, *UpsertGiftTypeConfigRequest) (*GiftTypeConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertGiftTypeConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpsertGiftTypeConfig not implemented")
}
func (UnimplementedWalletServiceServer) GrantResource(context.Context, *GrantResourceRequest) (*ResourceGrantResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GrantResource not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GrantResource not implemented")
}
func (UnimplementedWalletServiceServer) GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GrantResourceGroup not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GrantResourceGroup not implemented")
}
func (UnimplementedWalletServiceServer) ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListUserResources not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListUserResources not implemented")
}
func (UnimplementedWalletServiceServer) EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method EquipUserResource not implemented")
return nil, status.Errorf(codes.Unimplemented, "method EquipUserResource not implemented")
}
func (UnimplementedWalletServiceServer) UnequipUserResource(context.Context, *UnequipUserResourceRequest) (*UnequipUserResourceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UnequipUserResource not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UnequipUserResource not implemented")
}
func (UnimplementedWalletServiceServer) BatchGetUserEquippedResources(context.Context, *BatchGetUserEquippedResourcesRequest) (*BatchGetUserEquippedResourcesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method BatchGetUserEquippedResources not implemented")
return nil, status.Errorf(codes.Unimplemented, "method BatchGetUserEquippedResources not implemented")
}
func (UnimplementedWalletServiceServer) ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListResourceGrants not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListResourceGrants not implemented")
}
func (UnimplementedWalletServiceServer) ListResourceShopItems(context.Context, *ListResourceShopItemsRequest) (*ListResourceShopItemsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListResourceShopItems not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListResourceShopItems not implemented")
}
func (UnimplementedWalletServiceServer) UpsertResourceShopItems(context.Context, *UpsertResourceShopItemsRequest) (*UpsertResourceShopItemsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertResourceShopItems not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpsertResourceShopItems not implemented")
}
func (UnimplementedWalletServiceServer) SetResourceShopItemStatus(context.Context, *SetResourceShopItemStatusRequest) (*ResourceShopItemResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetResourceShopItemStatus not implemented")
return nil, status.Errorf(codes.Unimplemented, "method SetResourceShopItemStatus not implemented")
}
func (UnimplementedWalletServiceServer) PurchaseResourceShopItem(context.Context, *PurchaseResourceShopItemRequest) (*PurchaseResourceShopItemResponse, error) {
return nil, status.Error(codes.Unimplemented, "method PurchaseResourceShopItem not implemented")
return nil, status.Errorf(codes.Unimplemented, "method PurchaseResourceShopItem not implemented")
}
func (UnimplementedWalletServiceServer) ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRechargeBills not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRechargeBills not implemented")
}
func (UnimplementedWalletServiceServer) GetWalletOverview(context.Context, *GetWalletOverviewRequest) (*GetWalletOverviewResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetWalletOverview not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetWalletOverview not implemented")
}
func (UnimplementedWalletServiceServer) GetWalletValueSummary(context.Context, *GetWalletValueSummaryRequest) (*GetWalletValueSummaryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetWalletValueSummary not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetWalletValueSummary not implemented")
}
func (UnimplementedWalletServiceServer) GetUserGiftWall(context.Context, *GetUserGiftWallRequest) (*GetUserGiftWallResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetUserGiftWall not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetUserGiftWall not implemented")
}
func (UnimplementedWalletServiceServer) ListRechargeProducts(context.Context, *ListRechargeProductsRequest) (*ListRechargeProductsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRechargeProducts not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRechargeProducts not implemented")
}
func (UnimplementedWalletServiceServer) ConfirmGooglePayment(context.Context, *ConfirmGooglePaymentRequest) (*ConfirmGooglePaymentResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ConfirmGooglePayment not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ConfirmGooglePayment not implemented")
}
func (UnimplementedWalletServiceServer) ListAdminRechargeProducts(context.Context, *ListAdminRechargeProductsRequest) (*ListAdminRechargeProductsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListAdminRechargeProducts not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListAdminRechargeProducts not implemented")
}
func (UnimplementedWalletServiceServer) CreateRechargeProduct(context.Context, *CreateRechargeProductRequest) (*RechargeProductResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateRechargeProduct not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateRechargeProduct not implemented")
}
func (UnimplementedWalletServiceServer) UpdateRechargeProduct(context.Context, *UpdateRechargeProductRequest) (*RechargeProductResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateRechargeProduct not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateRechargeProduct not implemented")
}
func (UnimplementedWalletServiceServer) DeleteRechargeProduct(context.Context, *DeleteRechargeProductRequest) (*DeleteRechargeProductResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteRechargeProduct not implemented")
return nil, status.Errorf(codes.Unimplemented, "method DeleteRechargeProduct not implemented")
}
func (UnimplementedWalletServiceServer) GetDiamondExchangeConfig(context.Context, *GetDiamondExchangeConfigRequest) (*GetDiamondExchangeConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetDiamondExchangeConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetDiamondExchangeConfig not implemented")
}
func (UnimplementedWalletServiceServer) ListWalletTransactions(context.Context, *ListWalletTransactionsRequest) (*ListWalletTransactionsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListWalletTransactions not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListWalletTransactions not implemented")
}
func (UnimplementedWalletServiceServer) ListVipPackages(context.Context, *ListVipPackagesRequest) (*ListVipPackagesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListVipPackages not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListVipPackages not implemented")
}
func (UnimplementedWalletServiceServer) GetMyVip(context.Context, *GetMyVipRequest) (*GetMyVipResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetMyVip not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetMyVip not implemented")
}
func (UnimplementedWalletServiceServer) PurchaseVip(context.Context, *PurchaseVipRequest) (*PurchaseVipResponse, error) {
return nil, status.Error(codes.Unimplemented, "method PurchaseVip not implemented")
return nil, status.Errorf(codes.Unimplemented, "method PurchaseVip not implemented")
}
func (UnimplementedWalletServiceServer) GrantVip(context.Context, *GrantVipRequest) (*GrantVipResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GrantVip not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GrantVip not implemented")
}
func (UnimplementedWalletServiceServer) ListAdminVipLevels(context.Context, *ListAdminVipLevelsRequest) (*ListAdminVipLevelsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListAdminVipLevels not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListAdminVipLevels not implemented")
}
func (UnimplementedWalletServiceServer) UpdateAdminVipLevels(context.Context, *UpdateAdminVipLevelsRequest) (*UpdateAdminVipLevelsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateAdminVipLevels not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateAdminVipLevels not implemented")
}
func (UnimplementedWalletServiceServer) CreditTaskReward(context.Context, *CreditTaskRewardRequest) (*CreditTaskRewardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreditTaskReward not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreditTaskReward not implemented")
}
func (UnimplementedWalletServiceServer) CreditLuckyGiftReward(context.Context, *CreditLuckyGiftRewardRequest) (*CreditLuckyGiftRewardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreditLuckyGiftReward not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreditLuckyGiftReward not implemented")
}
func (UnimplementedWalletServiceServer) CreditRoomTurnoverReward(context.Context, *CreditRoomTurnoverRewardRequest) (*CreditRoomTurnoverRewardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreditRoomTurnoverReward not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreditRoomTurnoverReward not implemented")
}
func (UnimplementedWalletServiceServer) ApplyGameCoinChange(context.Context, *ApplyGameCoinChangeRequest) (*ApplyGameCoinChangeResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ApplyGameCoinChange not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ApplyGameCoinChange not implemented")
}
func (UnimplementedWalletServiceServer) GetRedPacketConfig(context.Context, *GetRedPacketConfigRequest) (*GetRedPacketConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRedPacketConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetRedPacketConfig not implemented")
}
func (UnimplementedWalletServiceServer) UpdateRedPacketConfig(context.Context, *UpdateRedPacketConfigRequest) (*UpdateRedPacketConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateRedPacketConfig not implemented")
return nil, status.Errorf(codes.Unimplemented, "method UpdateRedPacketConfig not implemented")
}
func (UnimplementedWalletServiceServer) CreateRedPacket(context.Context, *CreateRedPacketRequest) (*CreateRedPacketResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CreateRedPacket not implemented")
return nil, status.Errorf(codes.Unimplemented, "method CreateRedPacket not implemented")
}
func (UnimplementedWalletServiceServer) ClaimRedPacket(context.Context, *ClaimRedPacketRequest) (*ClaimRedPacketResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ClaimRedPacket not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ClaimRedPacket not implemented")
}
func (UnimplementedWalletServiceServer) ListRedPackets(context.Context, *ListRedPacketsRequest) (*ListRedPacketsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRedPackets not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ListRedPackets not implemented")
}
func (UnimplementedWalletServiceServer) GetRedPacket(context.Context, *GetRedPacketRequest) (*GetRedPacketResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRedPacket not implemented")
return nil, status.Errorf(codes.Unimplemented, "method GetRedPacket not implemented")
}
func (UnimplementedWalletServiceServer) ExpireRedPackets(context.Context, *ExpireRedPacketsRequest) (*ExpireRedPacketsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ExpireRedPackets not implemented")
return nil, status.Errorf(codes.Unimplemented, "method ExpireRedPackets not implemented")
}
func (UnimplementedWalletServiceServer) RetryRedPacketRefund(context.Context, *RetryRedPacketRefundRequest) (*RetryRedPacketRefundResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RetryRedPacketRefund not implemented")
return nil, status.Errorf(codes.Unimplemented, "method RetryRedPacketRefund not implemented")
}
func (UnimplementedWalletServiceServer) mustEmbedUnimplementedWalletServiceServer() {}
func (UnimplementedWalletServiceServer) testEmbeddedByValue() {}
@ -1334,7 +1334,7 @@ type UnsafeWalletServiceServer interface {
}
func RegisterWalletServiceServer(s grpc.ServiceRegistrar, srv WalletServiceServer) {
// If the following call panics, it indicates UnimplementedWalletServiceServer was
// If the following call pancis, 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.

View File

@ -303,10 +303,23 @@ func (s *Service) CreditCoinSellerStock(ctx context.Context, actorID int64, sell
if profile == nil || profile.Status != coinSellerStatusActive || profile.MerchantAssetType != coinSellerMerchantAssetType {
return nil, fmt.Errorf("target user is not an active coin seller")
}
seller, err := s.userClient.GetUser(ctx, userclient.GetUserRequest{
RequestID: requestID,
Caller: "hyapp-admin-server",
UserID: sellerUserID,
})
if err != nil {
return nil, err
}
if seller == nil || seller.CountryID <= 0 || seller.RegionID <= 0 {
return nil, fmt.Errorf("coin seller country and region are required")
}
return s.walletClient.AdminCreditCoinSellerStock(ctx, &walletv1.AdminCreditCoinSellerStockRequest{
CommandId: commandID,
SellerUserId: sellerUserID,
SellerCountryId: seller.CountryID,
SellerRegionId: seller.RegionID,
StockType: stockType,
CoinAmount: req.CoinAmount,
PaidCurrencyCode: paidCurrencyCode,

View File

@ -836,11 +836,18 @@ func (h *Handler) joinRoom(writer http.ResponseWriter, request *http.Request) {
if !httpkit.Decode(writer, request, &body) {
return
}
actorCountryID, actorRegionID, err := h.resolveRoomActorCountryRegion(request)
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
resp, err := h.roomClient.JoinRoom(request.Context(), &roomv1.JoinRoomRequest{
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
Role: body.Role,
Password: body.Password,
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
Role: body.Role,
Password: body.Password,
ActorCountryId: actorCountryID,
ActorRegionId: actorRegionID,
// 座驾快照在进房命令提交前生成room-service 只保存快照,不回查 wallet。
EntryVehicle: h.joinRoomEntryVehicleSnapshot(request),
})
@ -1307,7 +1314,7 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
senderRegionID, err := h.resolveGiftSenderRegionID(request)
senderCountryID, senderRegionID, err := h.resolveRoomActorCountryRegion(request)
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
@ -1326,6 +1333,7 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
GiftId: body.GiftID,
GiftCount: body.GiftCount,
PoolId: body.PoolID,
SenderCountryId: senderCountryID,
SenderRegionId: senderRegionID,
TargetIsHost: targetIsHost,
TargetHostRegionId: targetHostRegionID,
@ -1334,21 +1342,25 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
httpkit.Write(writer, request, resp, err)
}
func (h *Handler) resolveGiftSenderRegionID(request *http.Request) (int64, error) {
func (h *Handler) resolveRoomActorCountryRegion(request *http.Request) (int64, int64, error) {
if h.userProfileClient == nil {
return 0, xerr.New(xerr.Unavailable, "user service is not configured")
return 0, 0, xerr.New(xerr.Unavailable, "user service is not configured")
}
resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
return 0, err
return 0, 0, err
}
if resp.GetUser() == nil {
return 0, xerr.New(xerr.NotFound, "sender not found")
return 0, 0, xerr.New(xerr.NotFound, "user not found")
}
return resp.GetUser().GetRegionId(), nil
if resp.GetUser().GetCountryId() <= 0 || resp.GetUser().GetRegionId() <= 0 {
return 0, 0, xerr.New(xerr.InvalidArgument, "user country and region are required")
}
// room-service 事件必须携带用户真实国家和区域;房间 visible_region_id 只表达房间列表归属。
return resp.GetUser().GetCountryId(), resp.GetUser().GetRegionId(), nil
}
func (h *Handler) resolveGiftTargetHostScope(request *http.Request, targetUserID int64) (bool, int64, int64, error) {

View File

@ -133,20 +133,21 @@ func (h *Handler) transferCoinFromSeller(writer http.ResponseWriter, request *ht
if !ok {
return
}
sellerRegionID, targetRegionID, ok := h.coinSellerTransferRegions(writer, request, sellerUserID, targetUserID)
sellerRegionID, targetRegionID, targetCountryID, ok := h.coinSellerTransferRegions(writer, request, sellerUserID, targetUserID)
if !ok {
return
}
resp, err := h.walletClient.TransferCoinFromSeller(request.Context(), &walletv1.TransferCoinFromSellerRequest{
CommandId: commandID,
SellerUserId: sellerUserID,
TargetUserId: targetUserID,
SellerRegionId: sellerRegionID,
TargetRegionId: targetRegionID,
Amount: body.Amount,
Reason: reason,
AppCode: appcode.FromContext(request.Context()),
CommandId: commandID,
SellerUserId: sellerUserID,
TargetUserId: targetUserID,
SellerRegionId: sellerRegionID,
TargetRegionId: targetRegionID,
TargetCountryId: targetCountryID,
Amount: body.Amount,
Reason: reason,
AppCode: appcode.FromContext(request.Context()),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
@ -184,22 +185,24 @@ func (h *Handler) resolveCoinSellerTransferTargetUserID(writer http.ResponseWrit
return targetUserID, true
}
func (h *Handler) coinSellerTransferRegions(writer http.ResponseWriter, request *http.Request, sellerUserID int64, targetUserID int64) (int64, int64, bool) {
func (h *Handler) coinSellerTransferRegions(writer http.ResponseWriter, request *http.Request, sellerUserID int64, targetUserID int64) (int64, int64, int64, bool) {
sellerResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: sellerUserID,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return 0, 0, false
return 0, 0, 0, false
}
sellerRegionID := sellerResp.GetUser().GetRegionId()
seller := sellerResp.GetUser()
sellerCountryID := seller.GetCountryId()
sellerRegionID := seller.GetRegionId()
if sellerUserID == targetUserID {
if sellerRegionID <= 0 {
if sellerCountryID <= 0 || sellerRegionID <= 0 {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return 0, 0, false
return 0, 0, 0, false
}
return sellerRegionID, sellerRegionID, true
return sellerRegionID, sellerRegionID, sellerCountryID, true
}
targetResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: httpkit.UserMeta(request, ""),
@ -207,19 +210,21 @@ func (h *Handler) coinSellerTransferRegions(writer http.ResponseWriter, request
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return 0, 0, false
return 0, 0, 0, false
}
targetRegionID := targetResp.GetUser().GetRegionId()
if sellerRegionID <= 0 || targetRegionID <= 0 {
target := targetResp.GetUser()
targetCountryID := target.GetCountryId()
targetRegionID := target.GetRegionId()
if sellerRegionID <= 0 || targetCountryID <= 0 || targetRegionID <= 0 {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return 0, 0, false
return 0, 0, 0, false
}
if sellerRegionID != targetRegionID {
// 充值政策当前按单一区域生效;跨区转账必须先设计明确的结算区域,不能静默套用任意一方政策。
httpkit.WriteError(writer, request, http.StatusConflict, string(xerr.Conflict), "conflict")
return 0, 0, false
return 0, 0, 0, false
}
return sellerRegionID, targetRegionID, true
return sellerRegionID, targetRegionID, targetCountryID, true
}
func walletAssetTypes(request *http.Request) []string {

View File

@ -121,6 +121,10 @@ type JoinRoom struct {
Base
// Role 是用户在房间内的轻量角色,默认 audience。
Role string `json:"role"`
// ActorCountryID 是 gateway 从 user-service 注入的真实国家,用于统计进房活跃国家维度。
ActorCountryID int64 `json:"actor_country_id,omitempty"`
// ActorRegionID 是 gateway 从 user-service 注入的真实区域;房间 visible_region_id 不能代替用户国家。
ActorRegionID int64 `json:"actor_region_id,omitempty"`
// EntryVehicle 是 gateway 在进房时解析出的当前有效佩戴座驾快照,用于进房 IM。
EntryVehicle EntryVehicleSnapshot `json:"entry_vehicle,omitempty"`
}
@ -396,6 +400,8 @@ type SendGift struct {
GiftCount int32 `json:"gift_count"`
// SenderRegionID 是送礼用户所属区域,由 gateway 从 user-service 注入。
SenderRegionID int64 `json:"sender_region_id,omitempty"`
// SenderCountryID 是送礼用户所属国家,由 gateway 从 user-service 注入,统计服务不能用房间区域反推。
SenderCountryID int64 `json:"sender_country_id,omitempty"`
// TargetIsHost 是 gateway 从 user-service active host profile 注入的工资入账开关。
TargetIsHost bool `json:"target_is_host,omitempty"`
// TargetHostRegionID 是工资政策匹配区域room-service 不自行推导主播身份或区域。

View File

@ -26,14 +26,15 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
ctx = contextFromMeta(ctx, req.GetMeta())
// SendGift 的账务不在 room-service 内结算,但房间表现、热度和榜单由 Room Cell 同步更新。
cmd := command.SendGift{
Base: baseFromMeta(req.GetMeta()),
TargetType: normalizeGiftTargetType(req.GetTargetType()),
TargetUserIDs: normalizeGiftTargetUserIDs(req.GetTargetUserId(), req.GetTargetUserIds()),
GiftID: req.GetGiftId(),
PoolID: strings.TrimSpace(req.GetPoolId()),
GiftCount: req.GetGiftCount(),
SenderRegionID: req.GetSenderRegionId(),
TargetIsHost: req.GetTargetIsHost(),
Base: baseFromMeta(req.GetMeta()),
TargetType: normalizeGiftTargetType(req.GetTargetType()),
TargetUserIDs: normalizeGiftTargetUserIDs(req.GetTargetUserId(), req.GetTargetUserIds()),
GiftID: req.GetGiftId(),
PoolID: strings.TrimSpace(req.GetPoolId()),
GiftCount: req.GetGiftCount(),
SenderCountryID: req.GetSenderCountryId(),
SenderRegionID: req.GetSenderRegionId(),
TargetIsHost: req.GetTargetIsHost(),
// 工资区域由 gateway 从 user-service host profile 注入;房间 visible_region_id 只用于房间/礼物可见性。
TargetHostRegionID: req.GetTargetHostRegionId(),
TargetAgencyOwnerUserID: req.GetTargetAgencyOwnerUserId(),
@ -175,6 +176,8 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
CoinSpent: targetBilling.Billing.GetCoinSpent(),
BillingReceiptId: targetBilling.Billing.GetBillingReceiptId(),
VisibleRegionId: roomMeta.VisibleRegionID,
CountryId: cmd.SenderCountryID,
RegionId: firstNonZeroInt64(cmd.SenderRegionID, roomMeta.VisibleRegionID),
CommandId: targetBilling.CommandID,
})
if err != nil {

View File

@ -98,6 +98,15 @@ func runtimeRoomRef(key string) loadedRoomRef {
return loadedRoomRef{AppCode: "", RoomID: key}
}
func firstNonZeroInt64(values ...int64) int64 {
for _, value := range values {
if value != 0 {
return value
}
}
return 0
}
func commandResult(applied bool, roomVersion int64, now time.Time) *roomv1.CommandResult {
// 所有命令响应统一带 applied、room_version 和服务端时间,便于客户端处理幂等结果。
return &roomv1.CommandResult{

View File

@ -25,8 +25,10 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
password := strings.TrimSpace(req.GetPassword())
// JoinRoom 只维护 room-service presence不创建任何本地长连接订阅。
cmd := command.JoinRoom{
Base: baseFromMeta(req.GetMeta()),
Role: req.GetRole(),
Base: baseFromMeta(req.GetMeta()),
Role: req.GetRole(),
ActorCountryID: req.GetActorCountryId(),
ActorRegionID: req.GetActorRegionId(),
// entry_vehicle 来自 gateway 入房瞬间的 wallet 快照,写入 command log 后可稳定重放。
EntryVehicle: entryVehicleSnapshotFromProto(req.GetEntryVehicle()),
}
@ -78,6 +80,8 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
UserId: cmd.ActorUserID(),
Role: cmd.Role,
VisibleRegionId: current.VisibleRegionID,
CountryId: cmd.ActorCountryID,
RegionId: firstNonZeroInt64(cmd.ActorRegionID, current.VisibleRegionID),
// outbox 事件必须带座驾快照;否则异步补偿投递 IM 时会丢失入场座驾。
EntryVehicle: eventEntryVehicleFromCommand(cmd.EntryVehicle),
})

View File

@ -0,0 +1,647 @@
package main
import (
"context"
"database/sql"
"flag"
"fmt"
"log"
"os"
"sort"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
"hyapp/pkg/appcode"
)
type countryRegion struct {
countryID int64
regionID int64
}
type appDayKey struct {
day string
countryID int64
regionID int64
}
type appDayAgg struct {
newUsers int64
activeUsers int64
paidUsers int64
rechargeUSDMinor int64
newUserRechargeUSDMinor int64
coinSellerRechargeUSDMinor int64
googleRechargeUSDMinor int64
coinSellerTransferCoin int64
}
type userRegistrationRow struct {
userID int64
registeredDay string
dim countryRegion
createdAtMS int64
}
type payerRow struct {
day string
userID int64
dim countryRegion
firstMS int64
}
type activeRow struct {
day string
userID int64
dim countryRegion
firstMS int64
}
type rebuildState struct {
appDays map[appDayKey]*appDayAgg
registrations []userRegistrationRow
registeredByUserDay map[string]struct{}
payersByKey map[string]payerRow
activeByKey map[string]activeRow
userIDs map[int64]struct{}
userDims map[int64]countryRegion
}
func main() {
ctx := context.Background()
var (
appFlag = flag.String("app", envDefault("APP_CODE", "lalu"), "app_code to backfill")
tzFlag = flag.String("timezone", envDefault("BACKFILL_TIMEZONE", "UTC"), "day boundary timezone, e.g. UTC or Asia/Shanghai")
startFlag = flag.String("start-day", os.Getenv("START_DAY"), "inclusive yyyy-mm-dd in timezone; defaults to last 7 days")
endFlag = flag.String("end-day", os.Getenv("END_DAY"), "inclusive yyyy-mm-dd in timezone; defaults to today")
statsDSNFlag = flag.String("statistics-dsn", firstNonEmpty(os.Getenv("STATISTICS_MYSQL_DSN"), os.Getenv("MYSQL_DSN")), "statistics mysql dsn")
userDSNFlag = flag.String("user-dsn", os.Getenv("USER_MYSQL_DSN"), "user-service mysql dsn")
walletDSNFlag = flag.String("wallet-dsn", os.Getenv("WALLET_MYSQL_DSN"), "wallet-service mysql dsn")
applyFlag = flag.Bool("apply", envBool("APPLY"), "apply replacement; default dry-run")
)
flag.Parse()
if strings.TrimSpace(*statsDSNFlag) == "" || strings.TrimSpace(*userDSNFlag) == "" || strings.TrimSpace(*walletDSNFlag) == "" {
log.Fatal("statistics-dsn, user-dsn and wallet-dsn are required")
}
loc, err := time.LoadLocation(strings.TrimSpace(*tzFlag))
if err != nil {
log.Fatalf("load timezone: %v", err)
}
startDay, endDay, windowStartMS, windowEndMS, err := resolveWindow(*startFlag, *endFlag, loc)
if err != nil {
log.Fatal(err)
}
app := appcode.Normalize(*appFlag)
statsDB := mustOpen(ctx, *statsDSNFlag)
defer statsDB.Close()
userDB := mustOpen(ctx, *userDSNFlag)
defer userDB.Close()
walletDB := mustOpen(ctx, *walletDSNFlag)
defer walletDB.Close()
state := &rebuildState{
appDays: map[appDayKey]*appDayAgg{},
registeredByUserDay: map[string]struct{}{},
payersByKey: map[string]payerRow{},
activeByKey: map[string]activeRow{},
userIDs: map[int64]struct{}{},
userDims: map[int64]countryRegion{},
}
// 第一阶段只收集窗口内会参与重算的 user_id后续统一批量加载用户国家和区域避免按充值/活跃记录逐条访问 user-service 数据库。
if err := collectRegistrationUsers(ctx, userDB, state, app, windowStartMS, windowEndMS); err != nil {
log.Fatalf("collect registrations: %v", err)
}
if err := collectActiveUserIDs(ctx, statsDB, state, app, startDay, endDay); err != nil {
log.Fatalf("collect active ids: %v", err)
}
if err := collectRechargeUserIDs(ctx, walletDB, state, app, windowStartMS, windowEndMS); err != nil {
log.Fatalf("collect recharge ids: %v", err)
}
if err := loadUserDims(ctx, userDB, state, app); err != nil {
log.Fatalf("load user dims: %v", err)
}
if err := rebuildRegistrations(ctx, userDB, state, app, windowStartMS, windowEndMS, loc); err != nil {
log.Fatalf("rebuild registrations: %v", err)
}
if err := rebuildActive(ctx, statsDB, state, app, startDay, endDay); err != nil {
log.Fatalf("rebuild active: %v", err)
}
if err := rebuildWalletRecharge(ctx, walletDB, state, app, windowStartMS, windowEndMS, loc); err != nil {
log.Fatalf("rebuild wallet recharge: %v", err)
}
if err := rebuildCoinSellerStock(ctx, walletDB, state, app, windowStartMS, windowEndMS, loc); err != nil {
log.Fatalf("rebuild coin seller stock: %v", err)
}
if err := stageAndMaybeApply(ctx, statsDB, state, app, startDay, endDay, *applyFlag); err != nil {
log.Fatalf("stage/apply: %v", err)
}
mode := "dry-run"
if *applyFlag {
mode = "applied"
}
fmt.Printf("%s app=%s timezone=%s window=%s..%s app_day_rows=%d registrations=%d active=%d payers=%d\n",
mode, app, loc.String(), startDay, endDay, len(state.appDays), len(state.registrations), len(state.activeByKey), len(state.payersByKey))
}
func envDefault(key string, fallback string) string {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
return fallback
}
func envBool(key string) bool {
switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) {
case "1", "true", "yes", "y":
return true
default:
return false
}
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func mustOpen(ctx context.Context, dsn string) *sql.DB {
db, err := sql.Open("mysql", strings.TrimSpace(dsn))
if err != nil {
log.Fatal(err)
}
if err := db.PingContext(ctx); err != nil {
log.Fatal(err)
}
return db
}
func resolveWindow(rawStart, rawEnd string, loc *time.Location) (string, string, int64, int64, error) {
now := time.Now().In(loc)
endDayTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
startDayTime := endDayTime.AddDate(0, 0, -6)
var err error
if strings.TrimSpace(rawEnd) != "" {
endDayTime, err = parseDay(rawEnd, loc)
if err != nil {
return "", "", 0, 0, err
}
}
if strings.TrimSpace(rawStart) != "" {
startDayTime, err = parseDay(rawStart, loc)
if err != nil {
return "", "", 0, 0, err
}
}
if startDayTime.After(endDayTime) {
return "", "", 0, 0, fmt.Errorf("start-day must not be after end-day")
}
windowEnd := endDayTime.AddDate(0, 0, 1)
return formatDay(startDayTime), formatDay(endDayTime), startDayTime.UTC().UnixMilli(), windowEnd.UTC().UnixMilli(), nil
}
func parseDay(value string, loc *time.Location) (time.Time, error) {
parsed, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(value), loc)
if err != nil {
return time.Time{}, err
}
return time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 0, 0, 0, 0, loc), nil
}
func dayFromMS(ms int64, loc *time.Location) string {
return formatDay(time.UnixMilli(ms).In(loc))
}
func formatDay(value time.Time) string {
return value.Format("2006-01-02")
}
func collectRegistrationUsers(ctx context.Context, db *sql.DB, state *rebuildState, app string, startMS, endMS int64) error {
rows, err := db.QueryContext(ctx, `
SELECT u.user_id
FROM users u
WHERE u.app_code = ? AND u.created_at_ms >= ? AND u.created_at_ms < ?`,
app, startMS, endMS)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var userID int64
if err := rows.Scan(&userID); err != nil {
return err
}
state.userIDs[userID] = struct{}{}
}
return rows.Err()
}
func collectActiveUserIDs(ctx context.Context, db *sql.DB, state *rebuildState, app, startDay, endDay string) error {
rows, err := db.QueryContext(ctx, `
SELECT DISTINCT user_id
FROM stat_user_day_activity
WHERE app_code = ? AND stat_day BETWEEN ? AND ?`,
app, startDay, endDay)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var userID int64
if err := rows.Scan(&userID); err != nil {
return err
}
state.userIDs[userID] = struct{}{}
}
return rows.Err()
}
func collectRechargeUserIDs(ctx context.Context, db *sql.DB, state *rebuildState, app string, startMS, endMS int64) error {
for _, query := range []string{
`SELECT DISTINCT user_id FROM wallet_recharge_records WHERE app_code = ? AND created_at_ms >= ? AND created_at_ms < ?`,
`SELECT DISTINCT seller_user_id FROM coin_seller_stock_records WHERE app_code = ? AND counts_as_seller_recharge = 1 AND created_at_ms >= ? AND created_at_ms < ?`,
} {
rows, err := db.QueryContext(ctx, query, app, startMS, endMS)
if err != nil {
return err
}
for rows.Next() {
var userID int64
if err := rows.Scan(&userID); err != nil {
rows.Close()
return err
}
state.userIDs[userID] = struct{}{}
}
if err := rows.Close(); err != nil {
return err
}
}
return nil
}
func loadUserDims(ctx context.Context, db *sql.DB, state *rebuildState, app string) error {
userIDs := make([]int64, 0, len(state.userIDs))
for userID := range state.userIDs {
userIDs = append(userIDs, userID)
}
sort.Slice(userIDs, func(i, j int) bool { return userIDs[i] < userIDs[j] })
for start := 0; start < len(userIDs); start += 500 {
end := start + 500
if end > len(userIDs) {
end = len(userIDs)
}
placeholders := strings.TrimRight(strings.Repeat("?,", end-start), ",")
args := []any{app}
for _, userID := range userIDs[start:end] {
args = append(args, userID)
}
rows, err := db.QueryContext(ctx, `
SELECT u.user_id, COALESCE(c.country_id, 0), COALESCE(u.region_id, 0)
FROM users u
LEFT JOIN countries c ON c.app_code = u.app_code AND c.country_code = u.country
WHERE u.app_code = ? AND u.user_id IN (`+placeholders+`)`, args...)
if err != nil {
return err
}
for rows.Next() {
var userID int64
var dim countryRegion
if err := rows.Scan(&userID, &dim.countryID, &dim.regionID); err != nil {
rows.Close()
return err
}
state.userDims[userID] = dim
}
if err := rows.Close(); err != nil {
return err
}
}
return nil
}
func rebuildRegistrations(ctx context.Context, db *sql.DB, state *rebuildState, app string, startMS, endMS int64, loc *time.Location) error {
// 注册国家以 users.country 映射出的真实 country_id 为准region_id 只作为独立维度写入,不能再兜底成国家。
rows, err := db.QueryContext(ctx, `
SELECT u.user_id, COALESCE(c.country_id, 0), COALESCE(u.region_id, 0), u.created_at_ms
FROM users u
LEFT JOIN countries c ON c.app_code = u.app_code AND c.country_code = u.country
WHERE u.app_code = ? AND u.created_at_ms >= ? AND u.created_at_ms < ?`,
app, startMS, endMS)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var row userRegistrationRow
if err := rows.Scan(&row.userID, &row.dim.countryID, &row.dim.regionID, &row.createdAtMS); err != nil {
return err
}
row.registeredDay = dayFromMS(row.createdAtMS, loc)
state.registrations = append(state.registrations, row)
state.registeredByUserDay[userDayKey(row.registeredDay, row.userID)] = struct{}{}
agg := state.ensureAgg(row.registeredDay, row.dim)
agg.newUsers++
}
return rows.Err()
}
func rebuildActive(ctx context.Context, db *sql.DB, state *rebuildState, app, startDay, endDay string) error {
// 活跃的原始房间事件已经被实时消费进 stat_user_day_activity离线回填只重刷该表的国家/区域快照,避免在大窗口内扫描房间流水。
rows, err := db.QueryContext(ctx, `
SELECT stat_day, user_id, MIN(first_active_at_ms)
FROM stat_user_day_activity
WHERE app_code = ? AND stat_day BETWEEN ? AND ?
GROUP BY stat_day, user_id`,
app, startDay, endDay)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var day string
var userID, firstMS int64
if err := rows.Scan(&day, &userID, &firstMS); err != nil {
return err
}
dim := state.userDims[userID]
key := fmt.Sprintf("%s:%d", day, userID)
if _, exists := state.activeByKey[key]; exists {
continue
}
state.activeByKey[key] = activeRow{day: day, userID: userID, dim: dim, firstMS: firstMS}
state.ensureAgg(day, dim).activeUsers++
}
return rows.Err()
}
func rebuildWalletRecharge(ctx context.Context, db *sql.DB, state *rebuildState, app string, startMS, endMS int64, loc *time.Location) error {
// wallet_recharge_records 是到账事实表Google 也会写入该表payment_orders 只是 provider 审计,不能在回填里再扫一次导致双算。
// 币商向用户转账只代表金币充值用户,不产生用户 USDT 充值Google Play 则按记录里的 usd_minor_amount 计入用户充值。
rows, err := db.QueryContext(ctx, `
SELECT rr.user_id, rr.recharge_sequence, rr.coin_amount, rr.usd_minor_amount, rr.created_at_ms, wt.biz_type
FROM wallet_recharge_records rr
JOIN wallet_transactions wt ON wt.app_code = rr.app_code AND wt.transaction_id = rr.transaction_id
WHERE rr.app_code = ? AND rr.created_at_ms >= ? AND rr.created_at_ms < ?`,
app, startMS, endMS)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var userID, sequence, coinAmount, usdMinor, createdAtMS int64
var bizType string
if err := rows.Scan(&userID, &sequence, &coinAmount, &usdMinor, &createdAtMS, &bizType); err != nil {
return err
}
day := dayFromMS(createdAtMS, loc)
dim := state.userDims[userID]
state.addPayer(day, userID, dim, createdAtMS)
agg := state.ensureAgg(day, dim)
switch strings.ToLower(strings.TrimSpace(bizType)) {
case "coin_seller_transfer":
agg.coinSellerTransferCoin += coinAmount
case "google_play_recharge":
agg.rechargeUSDMinor += usdMinor
agg.googleRechargeUSDMinor += usdMinor
default:
if usdMinor > 0 {
agg.rechargeUSDMinor += usdMinor
}
}
if sequence == 1 && usdMinor > 0 {
agg.newUserRechargeUSDMinor += usdMinor
}
}
return rows.Err()
}
func rebuildCoinSellerStock(ctx context.Context, db *sql.DB, state *rebuildState, app string, startMS, endMS int64, loc *time.Location) error {
// 币商进货是平台收到的 USDT 充值,按 seller 的国家/区域进入总充值,但不写入用户金币转账金额。
rows, err := db.QueryContext(ctx, `
SELECT seller_user_id, paid_amount_micro, created_at_ms
FROM coin_seller_stock_records
WHERE app_code = ? AND counts_as_seller_recharge = 1 AND paid_amount_micro > 0 AND created_at_ms >= ? AND created_at_ms < ?`,
app, startMS, endMS)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var sellerUserID, amountMicro, createdAtMS int64
if err := rows.Scan(&sellerUserID, &amountMicro, &createdAtMS); err != nil {
return err
}
usdMinor := amountMicro / 10_000
day := dayFromMS(createdAtMS, loc)
dim := state.userDims[sellerUserID]
agg := state.ensureAgg(day, dim)
agg.rechargeUSDMinor += usdMinor
agg.coinSellerRechargeUSDMinor += usdMinor
}
return rows.Err()
}
func (s *rebuildState) ensureAgg(day string, dim countryRegion) *appDayAgg {
key := appDayKey{day: day, countryID: dim.countryID, regionID: dim.regionID}
if s.appDays[key] == nil {
s.appDays[key] = &appDayAgg{}
}
return s.appDays[key]
}
func (s *rebuildState) addPayer(day string, userID int64, dim countryRegion, firstMS int64) {
key := fmt.Sprintf("%s:%d", day, userID)
if existing, exists := s.payersByKey[key]; exists && existing.firstMS <= firstMS {
return
}
if _, exists := s.payersByKey[key]; !exists {
s.ensureAgg(day, dim).paidUsers++
}
s.payersByKey[key] = payerRow{day: day, userID: userID, dim: dim, firstMS: firstMS}
}
func (s *rebuildState) userRegisteredOn(userID int64, day string) bool {
_, ok := s.registeredByUserDay[userDayKey(day, userID)]
return ok
}
func userDayKey(day string, userID int64) string {
return fmt.Sprintf("%s:%d", day, userID)
}
func stageAndMaybeApply(ctx context.Context, db *sql.DB, state *rebuildState, app, startDay, endDay string, apply bool) error {
conn, err := db.Conn(ctx)
if err != nil {
return err
}
defer conn.Close()
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if err := createTempTables(ctx, tx); err != nil {
return err
}
if err := insertStageRows(ctx, tx, state, app); err != nil {
return err
}
if err := validateStage(ctx, tx); err != nil {
return err
}
if !apply {
return tx.Rollback()
}
nowMS := time.Now().UTC().UnixMilli()
// 本 job 只覆盖能从当前源表确定的近 7 天列礼物、游戏、MifaPay 等不在本次源表范围内的指标保留原值,避免修国家维度时误删其他业务数据。
if _, err := tx.ExecContext(ctx, `
UPDATE stat_app_day_country
SET new_users = 0, active_users = 0, paid_users = 0,
recharge_usd_minor = 0, new_user_recharge_usd_minor = 0,
coin_seller_recharge_usd_minor = 0,
google_recharge_usd_minor = 0, coin_seller_transfer_coin = 0,
updated_at_ms = ?
WHERE app_code = ? AND stat_day BETWEEN ? AND ?`,
nowMS, app, startDay, endDay); err != nil {
return err
}
for _, statement := range []string{
`DELETE FROM stat_user_registration WHERE app_code = ? AND registered_day BETWEEN ? AND ?`,
`DELETE FROM stat_user_day_activity WHERE app_code = ? AND stat_day BETWEEN ? AND ?`,
`DELETE FROM stat_recharge_day_payers WHERE app_code = ? AND stat_day BETWEEN ? AND ?`,
} {
if _, err := tx.ExecContext(ctx, statement, app, startDay, endDay); err != nil {
return err
}
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO stat_app_day_country (
app_code, stat_day, country_id, region_id, new_users, active_users, paid_users,
recharge_usd_minor, new_user_recharge_usd_minor, coin_seller_recharge_usd_minor,
google_recharge_usd_minor, coin_seller_transfer_coin, updated_at_ms
)
SELECT app_code, stat_day, country_id, region_id, new_users, active_users, paid_users,
recharge_usd_minor, new_user_recharge_usd_minor, coin_seller_recharge_usd_minor,
google_recharge_usd_minor, coin_seller_transfer_coin, ?
FROM tmp_stat_backfill_app_day
ON DUPLICATE KEY UPDATE
new_users = VALUES(new_users), active_users = VALUES(active_users), paid_users = VALUES(paid_users),
recharge_usd_minor = VALUES(recharge_usd_minor),
new_user_recharge_usd_minor = VALUES(new_user_recharge_usd_minor),
coin_seller_recharge_usd_minor = VALUES(coin_seller_recharge_usd_minor),
google_recharge_usd_minor = VALUES(google_recharge_usd_minor),
coin_seller_transfer_coin = VALUES(coin_seller_transfer_coin),
updated_at_ms = VALUES(updated_at_ms)`, nowMS); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO stat_user_registration (app_code, user_id, registered_day, country_id, region_id, registered_at_ms)
SELECT app_code, user_id, registered_day, country_id, region_id, registered_at_ms FROM tmp_stat_backfill_registration`); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO stat_user_day_activity (app_code, stat_day, country_id, region_id, user_id, first_active_at_ms)
SELECT app_code, stat_day, country_id, region_id, user_id, first_active_at_ms FROM tmp_stat_backfill_active`); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO stat_recharge_day_payers (app_code, stat_day, country_id, region_id, user_id, first_paid_at_ms)
SELECT app_code, stat_day, country_id, region_id, user_id, first_paid_at_ms FROM tmp_stat_backfill_payers`); err != nil {
return err
}
return tx.Commit()
}
func createTempTables(ctx context.Context, tx *sql.Tx) error {
statements := []string{
`CREATE TEMPORARY TABLE tmp_stat_backfill_app_day (
app_code VARCHAR(32) NOT NULL, stat_day DATE NOT NULL, country_id BIGINT NOT NULL, region_id BIGINT NOT NULL,
new_users BIGINT NOT NULL, active_users BIGINT NOT NULL, paid_users BIGINT NOT NULL,
recharge_usd_minor BIGINT NOT NULL, new_user_recharge_usd_minor BIGINT NOT NULL,
coin_seller_recharge_usd_minor BIGINT NOT NULL, google_recharge_usd_minor BIGINT NOT NULL,
coin_seller_transfer_coin BIGINT NOT NULL,
PRIMARY KEY(app_code, stat_day, country_id, region_id)
)`,
`CREATE TEMPORARY TABLE tmp_stat_backfill_registration (
app_code VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, registered_day DATE NOT NULL,
country_id BIGINT NOT NULL, region_id BIGINT NOT NULL, registered_at_ms BIGINT NOT NULL,
PRIMARY KEY(app_code, user_id)
)`,
`CREATE TEMPORARY TABLE tmp_stat_backfill_active (
app_code VARCHAR(32) NOT NULL, stat_day DATE NOT NULL, country_id BIGINT NOT NULL,
region_id BIGINT NOT NULL, user_id BIGINT NOT NULL, first_active_at_ms BIGINT NOT NULL,
PRIMARY KEY(app_code, stat_day, user_id)
)`,
`CREATE TEMPORARY TABLE tmp_stat_backfill_payers (
app_code VARCHAR(32) NOT NULL, stat_day DATE NOT NULL, country_id BIGINT NOT NULL,
region_id BIGINT NOT NULL, user_id BIGINT NOT NULL, first_paid_at_ms BIGINT NOT NULL,
PRIMARY KEY(app_code, stat_day, user_id)
)`,
}
for _, statement := range statements {
if _, err := tx.ExecContext(ctx, statement); err != nil {
return err
}
}
return nil
}
func insertStageRows(ctx context.Context, tx *sql.Tx, state *rebuildState, app string) error {
for key, agg := range state.appDays {
if _, err := tx.ExecContext(ctx, `
INSERT INTO tmp_stat_backfill_app_day (
app_code, stat_day, country_id, region_id, new_users, active_users, paid_users,
recharge_usd_minor, new_user_recharge_usd_minor, coin_seller_recharge_usd_minor,
google_recharge_usd_minor, coin_seller_transfer_coin
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
app, key.day, key.countryID, key.regionID, agg.newUsers, agg.activeUsers, agg.paidUsers,
agg.rechargeUSDMinor, agg.newUserRechargeUSDMinor, agg.coinSellerRechargeUSDMinor,
agg.googleRechargeUSDMinor, agg.coinSellerTransferCoin); err != nil {
return err
}
}
for _, row := range state.registrations {
if _, err := tx.ExecContext(ctx, `
INSERT INTO tmp_stat_backfill_registration (app_code, user_id, registered_day, country_id, region_id, registered_at_ms)
VALUES (?, ?, ?, ?, ?, ?)`, app, row.userID, row.registeredDay, row.dim.countryID, row.dim.regionID, row.createdAtMS); err != nil {
return err
}
}
for _, row := range state.activeByKey {
if _, err := tx.ExecContext(ctx, `
INSERT INTO tmp_stat_backfill_active (app_code, stat_day, country_id, region_id, user_id, first_active_at_ms)
VALUES (?, ?, ?, ?, ?, ?)`, app, row.day, row.dim.countryID, row.dim.regionID, row.userID, row.firstMS); err != nil {
return err
}
}
for _, row := range state.payersByKey {
if _, err := tx.ExecContext(ctx, `
INSERT INTO tmp_stat_backfill_payers (app_code, stat_day, country_id, region_id, user_id, first_paid_at_ms)
VALUES (?, ?, ?, ?, ?, ?)`, app, row.day, row.dim.countryID, row.dim.regionID, row.userID, row.firstMS); err != nil {
return err
}
}
return nil
}
func validateStage(ctx context.Context, tx *sql.Tx) error {
var negativeCount int64
if err := tx.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM tmp_stat_backfill_app_day
WHERE new_users < 0 OR active_users < 0 OR paid_users < 0 OR recharge_usd_minor < 0 OR coin_seller_transfer_coin < 0`).Scan(&negativeCount); err != nil {
return err
}
if negativeCount > 0 {
return fmt.Errorf("stage contains negative counters")
}
return nil
}

View File

@ -40,7 +40,7 @@ CREATE TABLE IF NOT EXISTS stat_app_day_country (
game_payout BIGINT NOT NULL DEFAULT 0 COMMENT '游戏返奖',
game_refund BIGINT NOT NULL DEFAULT 0 COMMENT '游戏退款',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, stat_day, country_id),
PRIMARY KEY (app_code, stat_day, country_id, region_id),
KEY idx_stat_app_day_region (app_code, stat_day, region_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='App 国家日统计聚合表';
@ -101,7 +101,7 @@ CREATE TABLE IF NOT EXISTS stat_lucky_gift_pool_day_country (
turnover_coin BIGINT NOT NULL DEFAULT 0 COMMENT '奖池幸运礼物流水金币',
payout_coin BIGINT NOT NULL DEFAULT 0 COMMENT '奖池幸运礼物返奖金币',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, stat_day, country_id, pool_id),
PRIMARY KEY (app_code, stat_day, country_id, region_id, pool_id),
KEY idx_stat_lucky_pool_overview (app_code, stat_day, pool_id),
KEY idx_stat_lucky_pool_region (app_code, stat_day, region_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物奖池国家日统计聚合表';
@ -118,7 +118,7 @@ CREATE TABLE IF NOT EXISTS stat_game_day_country (
refund_coin BIGINT NOT NULL DEFAULT 0,
player_count BIGINT NOT NULL DEFAULT 0,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, stat_day, country_id, platform_code, game_id),
PRIMARY KEY (app_code, stat_day, country_id, region_id, platform_code, game_id),
KEY idx_stat_game_day_region (app_code, stat_day, region_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='游戏国家日统计聚合表';

View File

@ -365,8 +365,8 @@ func roomGiftEvent(body []byte) (mysqlstorage.RoomGiftEvent, bool, error) {
AppCode: envelope.GetAppCode(),
EventID: envelope.GetEventId(),
SenderUserID: gift.GetSenderUserId(),
CountryID: 0,
VisibleRegionID: gift.GetVisibleRegionId(),
CountryID: gift.GetCountryId(),
VisibleRegionID: firstNonZeroInt64(gift.GetRegionId(), gift.GetVisibleRegionId()),
GiftValue: gift.GetGiftValue(),
CoinSpent: roomGiftCoinSpent(&gift),
PoolID: gift.GetPoolId(),
@ -402,8 +402,8 @@ func roomJoinActiveEvent(body []byte) (mysqlstorage.UserActiveEvent, bool, error
EventID: envelope.GetEventId(),
EventType: envelope.GetEventType(),
UserID: joined.GetUserId(),
CountryID: 0,
RegionID: joined.GetVisibleRegionId(),
CountryID: joined.GetCountryId(),
RegionID: firstNonZeroInt64(joined.GetRegionId(), joined.GetVisibleRegionId()),
OccurredAtMS: envelope.GetOccurredAtMs(),
}, true, nil
}

View File

@ -241,9 +241,8 @@ func (r *Repository) queryAppDayOverviewTotals(ctx context.Context, app, startDa
args = append(args, countryID)
}
if regionID > 0 {
// 旧统计行曾把 region_id 写入 country_id兼容分支只服务历史读数新事件写入后会走显式 region_id。
filter += " AND (region_id = ? OR (region_id = 0 AND country_id = ?))"
args = append(args, regionID, regionID)
filter += " AND region_id = ?"
args = append(args, regionID)
}
row := r.db.QueryRowContext(ctx, `
SELECT COALESCE(SUM(new_users),0), COALESCE(SUM(active_users),0), COALESCE(SUM(paid_users),0),
@ -272,8 +271,8 @@ func (r *Repository) queryNewPaidUsers(ctx context.Context, app, startDay, endDa
}
if regionID > 0 {
// p 表和 r 表都已经是统计小表,这里只按充值发生地过滤,保证新增付费用户口径和付费用户卡片一致。
filter += " AND (p.region_id = ? OR (p.region_id = 0 AND p.country_id = ?))"
args = append(args, regionID, regionID)
filter += " AND p.region_id = ?"
args = append(args, regionID)
}
row := r.db.QueryRowContext(ctx, `
SELECT COUNT(DISTINCT p.user_id)
@ -296,9 +295,8 @@ func (r *Repository) queryDailySeries(ctx context.Context, app, startDay, endDay
args = append(args, countryID)
}
if regionID > 0 {
// 和总览一致兼容历史 region_id 写到 country_id 的聚合行;新数据优先读显式 region_id。
filter += " AND (region_id = ? OR (region_id = 0 AND country_id = ?))"
args = append(args, regionID, regionID)
filter += " AND region_id = ?"
args = append(args, regionID)
}
rows, err := r.db.QueryContext(ctx, `
SELECT DATE_FORMAT(stat_day, '%Y-%m-%d'),
@ -355,8 +353,8 @@ func (r *Repository) queryDailyNewPaidUsers(ctx context.Context, app, startDay,
args = append(args, countryID)
}
if regionID > 0 {
filter += " AND (p.region_id = ? OR (p.region_id = 0 AND p.country_id = ?))"
args = append(args, regionID, regionID)
filter += " AND p.region_id = ?"
args = append(args, regionID)
}
rows, err := r.db.QueryContext(ctx, `
SELECT DATE_FORMAT(p.stat_day, '%Y-%m-%d'), COUNT(DISTINCT p.user_id)
@ -442,8 +440,8 @@ func (r *Repository) queryRetention(ctx context.Context, app, startDay, endDay s
args = append(args, countryID)
}
if regionID > 0 {
filter += " AND (r.region_id = ? OR (r.region_id = 0 AND r.country_id = ?))"
args = append(args, regionID, regionID)
filter += " AND r.region_id = ?"
args = append(args, regionID)
}
row := r.db.QueryRowContext(ctx, `
SELECT COUNT(*),
@ -470,8 +468,8 @@ func (r *Repository) queryGameRanking(ctx context.Context, app, startDay, endDay
args = append(args, countryID)
}
if regionID > 0 {
filter += " AND (region_id = ? OR (region_id = 0 AND country_id = ?))"
args = append(args, regionID, regionID)
filter += " AND region_id = ?"
args = append(args, regionID)
}
rows, err := r.db.QueryContext(ctx, `
SELECT platform_code, game_id, COALESCE(SUM(turnover_coin),0), COALESCE(SUM(payout_coin),0),
@ -502,8 +500,8 @@ func (r *Repository) queryCountryBreakdown(ctx context.Context, app, startDay, e
args := []any{app, startDay, endDay}
filter := ""
if regionID > 0 {
filter = " AND (region_id = ? OR (region_id = 0 AND country_id = ?))"
args = append(args, regionID, regionID)
filter = " AND region_id = ?"
args = append(args, regionID)
}
rows, err := r.db.QueryContext(ctx, `
SELECT country_id, COALESCE(MAX(region_id),0),
@ -561,8 +559,8 @@ func (r *Repository) queryLuckyGiftPools(ctx context.Context, app, startDay, end
args = append(args, countryID)
}
if regionID > 0 {
filter += " AND (region_id = ? OR (region_id = 0 AND country_id = ?))"
args = append(args, regionID, regionID)
filter += " AND region_id = ?"
args = append(args, regionID)
}
rows, err := r.db.QueryContext(ctx, `
SELECT pool_id, COALESCE(SUM(turnover_coin),0), COALESCE(SUM(payout_coin),0)

View File

@ -113,7 +113,7 @@ func (r *Repository) Migrate(ctx context.Context) error {
lucky_gift_turnover BIGINT NOT NULL DEFAULT 0, lucky_gift_payout BIGINT NOT NULL DEFAULT 0, lucky_gift_payers BIGINT NOT NULL DEFAULT 0,
game_turnover BIGINT NOT NULL DEFAULT 0, game_players BIGINT NOT NULL DEFAULT 0,
game_payout BIGINT NOT NULL DEFAULT 0, game_refund BIGINT NOT NULL DEFAULT 0, updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, stat_day, country_id),
PRIMARY KEY (app_code, stat_day, country_id, region_id),
KEY idx_stat_app_day_region (app_code, stat_day, region_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS stat_user_day_activity (
@ -150,7 +150,7 @@ func (r *Repository) Migrate(ctx context.Context) error {
region_id BIGINT NOT NULL DEFAULT 0,
pool_id VARCHAR(96) NOT NULL, turnover_coin BIGINT NOT NULL DEFAULT 0, payout_coin BIGINT NOT NULL DEFAULT 0,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, stat_day, country_id, pool_id),
PRIMARY KEY (app_code, stat_day, country_id, region_id, pool_id),
KEY idx_stat_lucky_pool_overview (app_code, stat_day, pool_id),
KEY idx_stat_lucky_pool_region (app_code, stat_day, region_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
@ -160,7 +160,7 @@ func (r *Repository) Migrate(ctx context.Context) error {
platform_code VARCHAR(64) NOT NULL DEFAULT '', game_id VARCHAR(96) NOT NULL,
turnover_coin BIGINT NOT NULL DEFAULT 0, payout_coin BIGINT NOT NULL DEFAULT 0, refund_coin BIGINT NOT NULL DEFAULT 0,
player_count BIGINT NOT NULL DEFAULT 0, updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, stat_day, country_id, platform_code, game_id),
PRIMARY KEY (app_code, stat_day, country_id, region_id, platform_code, game_id),
KEY idx_stat_game_day_region (app_code, stat_day, region_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS stat_game_day_players (
@ -205,9 +205,60 @@ func (r *Repository) Migrate(ctx context.Context) error {
return err
}
}
for table, columns := range map[string][]string{
"stat_app_day_country": {"app_code", "stat_day", "country_id", "region_id"},
"stat_lucky_gift_pool_day_country": {"app_code", "stat_day", "country_id", "region_id", "pool_id"},
"stat_game_day_country": {"app_code", "stat_day", "country_id", "region_id", "platform_code", "game_id"},
} {
if err := r.ensurePrimaryKeyColumns(ctx, table, columns); err != nil {
return err
}
}
return nil
}
func (r *Repository) ensurePrimaryKeyColumns(ctx context.Context, table string, expected []string) error {
rows, err := r.db.QueryContext(ctx, `
SELECT COLUMN_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'
ORDER BY ORDINAL_POSITION`, table)
if err != nil {
return err
}
defer rows.Close()
actual := make([]string, 0, len(expected))
for rows.Next() {
var column string
if err := rows.Scan(&column); err != nil {
return err
}
actual = append(actual, column)
}
if err := rows.Err(); err != nil {
return err
}
if sameStringSlice(actual, expected) {
return nil
}
// 统计聚合表必须同时以国家和区域做主键;否则区域筛选和国家筛选会写入同一行,导致阿富汗这类历史错位国家继续污染榜单。
_, err = r.db.ExecContext(ctx, "ALTER TABLE "+table+" DROP PRIMARY KEY, ADD PRIMARY KEY ("+strings.Join(expected, ", ")+")")
return err
}
func sameStringSlice(left, right []string) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}
type UserRegisteredEvent struct {
AppCode string
EventID string
@ -305,8 +356,8 @@ func (r *Repository) ConsumeUserRegistered(ctx context.Context, event UserRegist
_, err := tx.ExecContext(ctx, `
UPDATE stat_app_day_country
SET new_users = new_users + 1, updated_at_ms = ?
WHERE app_code = ? AND stat_day = ? AND country_id = ?
`, nowMS, appcode.Normalize(event.AppCode), day, countryID)
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ?
`, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID)
return err
})
}
@ -350,8 +401,8 @@ func (r *Repository) ConsumeRecharge(ctx context.Context, event RechargeEvent) e
coin_seller_transfer_coin = coin_seller_transfer_coin + ?,
paid_users = paid_users + ?,
updated_at_ms = ?
WHERE app_code = ? AND stat_day = ? AND country_id = ?
`, event.USDMinor, newUserUSD, coinSellerUSD, mifapayUSD, googleUSD, coinSellerTransferCoin, paidUsers, nowMS, appcode.Normalize(event.AppCode), day, countryID)
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ?
`, event.USDMinor, newUserUSD, coinSellerUSD, mifapayUSD, googleUSD, coinSellerTransferCoin, paidUsers, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID)
return err
})
}
@ -369,8 +420,8 @@ func (r *Repository) ConsumeCoinSellerStock(ctx context.Context, event CoinSelle
SET recharge_usd_minor = recharge_usd_minor + ?,
coin_seller_recharge_usd_minor = coin_seller_recharge_usd_minor + ?,
updated_at_ms = ?
WHERE app_code = ? AND stat_day = ? AND country_id = ?
`, event.USDMinor, event.USDMinor, nowMS, appcode.Normalize(event.AppCode), day, countryID)
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ?
`, event.USDMinor, event.USDMinor, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID)
return err
})
}
@ -401,8 +452,8 @@ func (r *Repository) ConsumeRoomGift(ctx context.Context, event RoomGiftEvent) e
if _, err := tx.ExecContext(ctx, `
UPDATE stat_lucky_gift_pool_day_country
SET turnover_coin = turnover_coin + ?, updated_at_ms = ?
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND pool_id = ?
`, luckyTurnover, nowMS, appcode.Normalize(event.AppCode), day, countryID, poolID); err != nil {
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ? AND pool_id = ?
`, luckyTurnover, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID, poolID); err != nil {
return err
}
}
@ -410,8 +461,8 @@ func (r *Repository) ConsumeRoomGift(ctx context.Context, event RoomGiftEvent) e
UPDATE stat_app_day_country
SET gift_coin_spent = gift_coin_spent + ?, lucky_gift_turnover = lucky_gift_turnover + ?,
lucky_gift_payers = lucky_gift_payers + ?, updated_at_ms = ?
WHERE app_code = ? AND stat_day = ? AND country_id = ?
`, event.CoinSpent, luckyTurnover, luckyPayers, nowMS, appcode.Normalize(event.AppCode), day, countryID)
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ?
`, event.CoinSpent, luckyTurnover, luckyPayers, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID)
return err
})
}
@ -432,16 +483,16 @@ func (r *Repository) ConsumeLuckyGiftReward(ctx context.Context, event LuckyGift
if _, err := tx.ExecContext(ctx, `
UPDATE stat_lucky_gift_pool_day_country
SET payout_coin = payout_coin + ?, updated_at_ms = ?
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND pool_id = ?
`, event.Amount, nowMS, appcode.Normalize(event.AppCode), day, countryID, poolID); err != nil {
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ? AND pool_id = ?
`, event.Amount, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID, poolID); err != nil {
return err
}
}
_, err := tx.ExecContext(ctx, `
UPDATE stat_app_day_country
SET lucky_gift_payout = lucky_gift_payout + ?, updated_at_ms = ?
WHERE app_code = ? AND stat_day = ? AND country_id = ?
`, event.Amount, nowMS, appcode.Normalize(event.AppCode), day, countryID)
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ?
`, event.Amount, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID)
return err
})
}
@ -490,16 +541,16 @@ func (r *Repository) ConsumeGameOrder(ctx context.Context, event GameOrderEvent)
UPDATE stat_game_day_country
SET turnover_coin = turnover_coin + ?, payout_coin = payout_coin + ?, refund_coin = refund_coin + ?,
player_count = player_count + ?, updated_at_ms = ?
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND platform_code = ? AND game_id = ?
`, turnover, payout, refund, playerDelta, nowMS, appcode.Normalize(event.AppCode), day, countryID, event.PlatformCode, event.GameID); err != nil {
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ? AND platform_code = ? AND game_id = ?
`, turnover, payout, refund, playerDelta, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID, event.PlatformCode, event.GameID); err != nil {
return err
}
_, err := tx.ExecContext(ctx, `
UPDATE stat_app_day_country
SET game_turnover = game_turnover + ?, game_payout = game_payout + ?, game_refund = game_refund + ?,
game_players = game_players + ?, updated_at_ms = ?
WHERE app_code = ? AND stat_day = ? AND country_id = ?
`, turnover, payout, refund, playerDelta, nowMS, appcode.Normalize(event.AppCode), day, countryID)
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ?
`, turnover, payout, refund, playerDelta, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID)
return err
})
}
@ -596,8 +647,8 @@ func applyActive(ctx context.Context, tx *sql.Tx, app string, day string, countr
_, err = tx.ExecContext(ctx, `
UPDATE stat_app_day_country
SET active_users = active_users + 1, updated_at_ms = ?
WHERE app_code = ? AND stat_day = ? AND country_id = ?
`, nowMS, appcode.Normalize(app), day, countryID)
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ?
`, nowMS, appcode.Normalize(app), day, countryID, regionID)
return err
}
@ -606,7 +657,6 @@ func ensureAppDay(ctx context.Context, tx *sql.Tx, app string, day string, count
INSERT INTO stat_app_day_country (app_code, stat_day, country_id, region_id, updated_at_ms)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
region_id = IF(region_id = 0 AND VALUES(region_id) > 0, VALUES(region_id), region_id),
updated_at_ms = VALUES(updated_at_ms)
`, appcode.Normalize(app), day, countryID, regionID, nowMS)
return err
@ -617,7 +667,6 @@ func ensureGameDay(ctx context.Context, tx *sql.Tx, app string, day string, coun
INSERT INTO stat_game_day_country (app_code, stat_day, country_id, region_id, platform_code, game_id, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
region_id = IF(region_id = 0 AND VALUES(region_id) > 0, VALUES(region_id), region_id),
updated_at_ms = VALUES(updated_at_ms)
`, appcode.Normalize(app), day, countryID, regionID, platformCode, gameID, nowMS)
return err
@ -628,7 +677,6 @@ func ensureLuckyGiftPoolDay(ctx context.Context, tx *sql.Tx, app string, day str
INSERT INTO stat_lucky_gift_pool_day_country (app_code, stat_day, country_id, region_id, pool_id, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
region_id = IF(region_id = 0 AND VALUES(region_id) > 0, VALUES(region_id), region_id),
updated_at_ms = VALUES(updated_at_ms)
`, appcode.Normalize(app), day, countryID, regionID, strings.TrimSpace(poolID), nowMS)
return err

View File

@ -243,14 +243,15 @@ type HostSalaryProgress struct {
// CoinSellerTransferCommand 是币商给玩家转普通金币的账务命令。
type CoinSellerTransferCommand struct {
AppCode string
CommandID string
SellerUserID int64
TargetUserID int64
SellerRegionID int64
TargetRegionID int64
Amount int64
Reason string
AppCode string
CommandID string
SellerUserID int64
TargetUserID int64
TargetCountryID int64
SellerRegionID int64
TargetRegionID int64
Amount int64
Reason string
}
// CoinSellerTransferReceipt 是币商转账完成后的稳定回执。
@ -335,6 +336,8 @@ type CoinSellerStockCreditCommand struct {
AppCode string
CommandID string
SellerUserID int64
SellerCountryID int64
SellerRegionID int64
StockType string
CoinAmount int64
PaidCurrencyCode string
@ -349,6 +352,8 @@ type CoinSellerStockCreditCommand struct {
type CoinSellerStockCreditReceipt struct {
TransactionID string
SellerUserID int64
SellerCountryID int64
SellerRegionID int64
StockType string
CoinAmount int64
PaidCurrencyCode string

View File

@ -315,6 +315,9 @@ func (s *Service) AdminCreditCoinSellerStock(ctx context.Context, command ledger
if command.CommandID == "" || command.SellerUserID <= 0 || command.OperatorUserID <= 0 {
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.InvalidArgument, "coin seller stock command is incomplete")
}
if command.SellerCountryID <= 0 || command.SellerRegionID <= 0 {
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.InvalidArgument, "seller country and region are required")
}
if len(command.CommandID) > 128 {
return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.InvalidArgument, "command_id is too long")
}
@ -364,6 +367,9 @@ func (s *Service) TransferCoinFromSeller(ctx context.Context, command ledger.Coi
if command.SellerRegionID <= 0 || command.TargetRegionID <= 0 {
return ledger.CoinSellerTransferReceipt{}, xerr.New(xerr.InvalidArgument, "seller and target region are required")
}
if command.TargetCountryID <= 0 {
return ledger.CoinSellerTransferReceipt{}, xerr.New(xerr.InvalidArgument, "target country is required")
}
if command.SellerRegionID != command.TargetRegionID {
return ledger.CoinSellerTransferReceipt{}, xerr.New(xerr.Conflict, "seller and target region must match")
}

View File

@ -1448,14 +1448,15 @@ func TestWalletOutboxClaimUsesRealMySQLStatusFlow(t *testing.T) {
ctx := appcode.WithContext(context.Background(), "lalu")
receipt, err := svc.TransferCoinFromSeller(ctx, ledger.CoinSellerTransferCommand{
AppCode: "lalu",
CommandID: "cmd-real-outbox-transfer",
SellerUserID: 30001,
TargetUserID: 30002,
SellerRegionID: 86,
TargetRegionID: 86,
Amount: 200,
Reason: "real outbox claim test",
AppCode: "lalu",
CommandID: "cmd-real-outbox-transfer",
SellerUserID: 30001,
TargetUserID: 30002,
TargetCountryID: 63,
SellerRegionID: 86,
TargetRegionID: 86,
Amount: 200,
Reason: "real outbox claim test",
})
if err != nil {
t.Fatalf("TransferCoinFromSeller failed: %v", err)
@ -1593,13 +1594,14 @@ func TestTransferCoinFromSellerMovesDedicatedAssetToPlayerCoin(t *testing.T) {
}
command := ledger.CoinSellerTransferCommand{
CommandID: "cmd-seller-transfer",
SellerUserID: 30001,
TargetUserID: 30002,
SellerRegionID: 1001,
TargetRegionID: 1001,
Amount: 80000,
Reason: "player recharge",
CommandID: "cmd-seller-transfer",
SellerUserID: 30001,
TargetUserID: 30002,
TargetCountryID: 63,
SellerRegionID: 1001,
TargetRegionID: 1001,
Amount: 80000,
Reason: "player recharge",
}
first, err := svc.TransferCoinFromSeller(context.Background(), command)
if err != nil {
@ -1639,6 +1641,9 @@ func TestTransferCoinFromSellerMovesDedicatedAssetToPlayerCoin(t *testing.T) {
if got := repository.CountRows("wallet_outbox", "transaction_id = ?", first.TransactionID); got != 4 {
t.Fatalf("seller transfer should write two balance events, one transfer event and one recharge event, got %d", got)
}
if got := repository.CountRows("wallet_outbox", "transaction_id = ? AND event_type = ? AND JSON_EXTRACT(payload_json, '$.target_country_id') = 63 AND JSON_EXTRACT(payload_json, '$.country_id') = 63 AND JSON_EXTRACT(payload_json, '$.target_region_id') = 1001", first.TransactionID, "WalletRechargeRecorded"); got != 1 {
t.Fatalf("seller transfer recharge event must carry target country/region for statistics, got %d", got)
}
}
// TestTransferCoinFromSellerAllowsSelfTransfer 验证币商可以把自己的专用库存转入自己的普通金币账户。
@ -1659,13 +1664,14 @@ func TestTransferCoinFromSellerAllowsSelfTransfer(t *testing.T) {
}
command := ledger.CoinSellerTransferCommand{
CommandID: "cmd-seller-self-transfer",
SellerUserID: 30003,
TargetUserID: 30003,
SellerRegionID: 1001,
TargetRegionID: 1001,
Amount: 30000,
Reason: "self recharge",
CommandID: "cmd-seller-self-transfer",
SellerUserID: 30003,
TargetUserID: 30003,
TargetCountryID: 63,
SellerRegionID: 1001,
TargetRegionID: 1001,
Amount: 30000,
Reason: "self recharge",
}
first, err := svc.TransferCoinFromSeller(context.Background(), command)
if err != nil {
@ -1718,13 +1724,14 @@ func TestTransferCoinFromSellerRecordsCoinsWithoutUSDConversion(t *testing.T) {
}
receipt, err := svc.TransferCoinFromSeller(context.Background(), ledger.CoinSellerTransferCommand{
CommandID: "cmd-seller-small-transfer",
SellerUserID: 30004,
TargetUserID: 30004,
SellerRegionID: 1001,
TargetRegionID: 1001,
Amount: 1,
Reason: "self recharge",
CommandID: "cmd-seller-small-transfer",
SellerUserID: 30004,
TargetUserID: 30004,
TargetCountryID: 63,
SellerRegionID: 1001,
TargetRegionID: 1001,
Amount: 1,
Reason: "self recharge",
})
if err != nil {
t.Fatalf("TransferCoinFromSeller small amount failed: %v", err)
@ -1750,13 +1757,14 @@ func TestTransferCoinFromSellerInsufficientBalance(t *testing.T) {
svc := walletservice.New(repository)
_, err := svc.TransferCoinFromSeller(context.Background(), ledger.CoinSellerTransferCommand{
CommandID: "cmd-seller-low",
SellerUserID: 31001,
TargetUserID: 31002,
SellerRegionID: 1001,
TargetRegionID: 1001,
Amount: 1,
Reason: "player recharge",
CommandID: "cmd-seller-low",
SellerUserID: 31001,
TargetUserID: 31002,
TargetCountryID: 63,
SellerRegionID: 1001,
TargetRegionID: 1001,
Amount: 1,
Reason: "player recharge",
})
if !xerr.IsCode(err, xerr.InsufficientBalance) {
t.Fatalf("expected INSUFFICIENT_BALANCE, got %v", err)
@ -1784,13 +1792,14 @@ func TestTransferCoinFromSellerDoesNotRequireRechargePolicy(t *testing.T) {
}
receipt, err := svc.TransferCoinFromSeller(context.Background(), ledger.CoinSellerTransferCommand{
CommandID: "cmd-seller-no-policy",
SellerUserID: 32001,
TargetUserID: 32002,
SellerRegionID: 2001,
TargetRegionID: 2001,
Amount: 80000,
Reason: "player recharge",
CommandID: "cmd-seller-no-policy",
SellerUserID: 32001,
TargetUserID: 32002,
TargetCountryID: 63,
SellerRegionID: 2001,
TargetRegionID: 2001,
Amount: 80000,
Reason: "player recharge",
})
if err != nil {
t.Fatalf("TransferCoinFromSeller without recharge policy failed: %v", err)
@ -1945,6 +1954,8 @@ func TestAdminCreditCoinSellerStockPurchaseCreditsDedicatedBalance(t *testing.T)
command := ledger.CoinSellerStockCreditCommand{
CommandID: "cmd-stock-usdt",
SellerUserID: 33001,
SellerCountryID: 63,
SellerRegionID: 7,
StockType: ledger.StockTypeUSDTPurchase,
CoinAmount: 8000000,
PaidAmountMicro: 100000000,
@ -1982,6 +1993,9 @@ func TestAdminCreditCoinSellerStockPurchaseCreditsDedicatedBalance(t *testing.T)
if got := repository.CountRows("wallet_outbox", "transaction_id = ?", first.TransactionID); got != 2 {
t.Fatalf("stock purchase should write balance and stock outbox events, got %d", got)
}
if got := repository.CountRows("wallet_outbox", "transaction_id = ? AND event_type = ? AND JSON_EXTRACT(payload_json, '$.seller_country_id') = 63 AND JSON_EXTRACT(payload_json, '$.seller_region_id') = 7 AND JSON_EXTRACT(payload_json, '$.country_id') = 63 AND JSON_EXTRACT(payload_json, '$.region_id') = 7", first.TransactionID, "WalletCoinSellerStockPurchased"); got != 1 {
t.Fatalf("stock outbox must carry seller country/region for statistics, got %d", got)
}
}
// TestAdminCreditCoinSellerStockCompensationRejectsPaidAmount 锁定补偿不能携带充值金额的边界。
@ -1991,6 +2005,8 @@ func TestAdminCreditCoinSellerStockCompensationRejectsPaidAmount(t *testing.T) {
_, err := svc.AdminCreditCoinSellerStock(context.Background(), ledger.CoinSellerStockCreditCommand{
CommandID: "cmd-stock-comp-invalid",
SellerUserID: 34001,
SellerCountryID: 63,
SellerRegionID: 7,
StockType: ledger.StockTypeCoinCompensation,
CoinAmount: 50000,
PaidCurrencyCode: ledger.PaidCurrencyUSDT,
@ -2010,11 +2026,13 @@ func TestAdminCreditCoinSellerStockCompensationCreditsWithoutRechargeAmount(t *t
repository := mysqltest.NewRepository(t)
svc := walletservice.New(repository)
receipt, err := svc.AdminCreditCoinSellerStock(context.Background(), ledger.CoinSellerStockCreditCommand{
CommandID: "cmd-stock-comp",
SellerUserID: 35001,
StockType: ledger.StockTypeCoinCompensation,
CoinAmount: 50000,
OperatorUserID: 90001,
CommandID: "cmd-stock-comp",
SellerUserID: 35001,
SellerCountryID: 63,
SellerRegionID: 7,
StockType: ledger.StockTypeCoinCompensation,
CoinAmount: 50000,
OperatorUserID: 90001,
})
if err != nil {
t.Fatalf("AdminCreditCoinSellerStock compensation failed: %v", err)
@ -2034,6 +2052,8 @@ func TestAdminCreditCoinSellerStockRejectsDuplicatePaymentRef(t *testing.T) {
command := ledger.CoinSellerStockCreditCommand{
CommandID: "cmd-stock-ref-1",
SellerUserID: 36001,
SellerCountryID: 63,
SellerRegionID: 7,
StockType: ledger.StockTypeUSDTPurchase,
CoinAmount: 100,
PaidAmountMicro: 1000000,
@ -2047,6 +2067,8 @@ func TestAdminCreditCoinSellerStockRejectsDuplicatePaymentRef(t *testing.T) {
}
command.CommandID = "cmd-stock-ref-2"
command.SellerUserID = 36002
command.SellerCountryID = 971
command.SellerRegionID = 8
command.EvidenceRef = "oss://receipt-dup-2"
_, err := svc.AdminCreditCoinSellerStock(context.Background(), command)
if !xerr.IsCode(err, xerr.CoinSellerPaymentRefDuplicated) {
@ -2064,6 +2086,8 @@ func TestAdminCreditCoinSellerStockRejectsIdempotencyConflict(t *testing.T) {
command := ledger.CoinSellerStockCreditCommand{
CommandID: "cmd-stock-conflict",
SellerUserID: 37001,
SellerCountryID: 63,
SellerRegionID: 7,
StockType: ledger.StockTypeUSDTPurchase,
CoinAmount: 100,
PaidAmountMicro: 1000000,

View File

@ -1142,6 +1142,8 @@ func (r *Repository) AdminCreditCoinSellerStock(ctx context.Context, command led
metadata := coinSellerStockMetadata{
AppCode: command.AppCode,
SellerUserID: command.SellerUserID,
SellerCountryID: command.SellerCountryID,
SellerRegionID: command.SellerRegionID,
StockType: command.StockType,
CoinAmount: command.CoinAmount,
PaidCurrencyCode: command.PaidCurrencyCode,
@ -1241,6 +1243,7 @@ func (r *Repository) TransferCoinFromSeller(ctx context.Context, command ledger.
AppCode: command.AppCode,
SellerUserID: command.SellerUserID,
TargetUserID: command.TargetUserID,
TargetCountryID: command.TargetCountryID,
SellerRegionID: command.SellerRegionID,
TargetRegionID: command.TargetRegionID,
Amount: command.Amount,
@ -2676,6 +2679,7 @@ type coinSellerTransferMetadata struct {
AppCode string `json:"app_code"`
SellerUserID int64 `json:"seller_user_id"`
TargetUserID int64 `json:"target_user_id"`
TargetCountryID int64 `json:"target_country_id"`
SellerRegionID int64 `json:"seller_region_id"`
TargetRegionID int64 `json:"target_region_id"`
Amount int64 `json:"amount"`
@ -2728,6 +2732,8 @@ type salaryTransferToCoinSellerMetadata struct {
type coinSellerStockMetadata struct {
AppCode string `json:"app_code"`
SellerUserID int64 `json:"seller_user_id"`
SellerCountryID int64 `json:"seller_country_id"`
SellerRegionID int64 `json:"seller_region_id"`
StockType string `json:"stock_type"`
CoinAmount int64 `json:"coin_amount"`
PaidCurrencyCode string `json:"paid_currency_code"`
@ -2889,13 +2895,14 @@ func receiptFromGameMetadata(transactionID string, metadata gameCoinMetadata, id
func (r *Repository) receiptForCoinSellerStockTransaction(ctx context.Context, tx *sql.Tx, transactionID string) (ledger.CoinSellerStockCreditReceipt, error) {
row := tx.QueryRowContext(ctx,
`SELECT seller_user_id, stock_type, coin_amount, paid_currency_code, paid_amount_micro,
counts_as_seller_recharge, balance_after, created_at_ms
counts_as_seller_recharge, balance_after, metadata_json, created_at_ms
FROM coin_seller_stock_records
WHERE app_code = ? AND transaction_id = ?`,
appcode.FromContext(ctx),
transactionID,
)
var receipt ledger.CoinSellerStockCreditReceipt
var metadataJSON string
if err := row.Scan(
&receipt.SellerUserID,
&receipt.StockType,
@ -2904,10 +2911,18 @@ func (r *Repository) receiptForCoinSellerStockTransaction(ctx context.Context, t
&receipt.PaidAmountMicro,
&receipt.CountsAsSellerRecharge,
&receipt.BalanceAfter,
&metadataJSON,
&receipt.CreatedAtMS,
); err != nil {
return ledger.CoinSellerStockCreditReceipt{}, err
}
if metadataJSON != "" {
var metadata coinSellerStockMetadata
if err := json.Unmarshal([]byte(metadataJSON), &metadata); err == nil {
receipt.SellerCountryID = metadata.SellerCountryID
receipt.SellerRegionID = metadata.SellerRegionID
}
}
receipt.TransactionID = transactionID
return receipt, nil
}
@ -2916,6 +2931,8 @@ func receiptFromCoinSellerStockMetadata(transactionID string, metadata coinSelle
return ledger.CoinSellerStockCreditReceipt{
TransactionID: transactionID,
SellerUserID: metadata.SellerUserID,
SellerCountryID: metadata.SellerCountryID,
SellerRegionID: metadata.SellerRegionID,
StockType: metadata.StockType,
CoinAmount: metadata.CoinAmount,
PaidCurrencyCode: metadata.PaidCurrencyCode,
@ -3070,6 +3087,34 @@ func coinSellerTransferredEvent(transactionID string, commandID string, sellerUs
}
func rechargeRecordedEvent(transactionID string, commandID string, targetUserID int64, availableDelta int64, payload any, nowMs int64) walletOutboxEvent {
if metadata, ok := payload.(coinSellerTransferMetadata); ok {
// WalletRechargeRecorded 是统计服务的充值入口;币商转账没有 USDT 金额,但必须带目标用户真实国家/区域,避免统计继续把 region 当 country。
payload = map[string]any{
"app_code": metadata.AppCode,
"seller_user_id": metadata.SellerUserID,
"target_user_id": metadata.TargetUserID,
"target_country_id": metadata.TargetCountryID,
"country_id": metadata.TargetCountryID,
"seller_region_id": metadata.SellerRegionID,
"target_region_id": metadata.TargetRegionID,
"region_id": metadata.TargetRegionID,
"amount": metadata.Amount,
"reason": metadata.Reason,
"seller_asset_type": metadata.SellerAssetType,
"target_asset_type": metadata.TargetAssetType,
"seller_balance_after": metadata.SellerBalanceAfter,
"target_balance_after": metadata.TargetBalanceAfter,
"recharge_sequence": metadata.RechargeSequence,
"recharge_usd_minor": metadata.RechargeUSDMinor,
"recharge_currency_code": metadata.RechargeCurrencyCode,
"recharge_policy_id": metadata.RechargePolicyID,
"recharge_policy_version": metadata.RechargePolicyVersion,
"recharge_policy_coin_amount": metadata.RechargePolicyCoinAmount,
"recharge_policy_usd_minor_amount": metadata.RechargePolicyUSDMinorAmount,
"recharge_type": "coin_seller_transfer",
"created_at_ms": nowMs,
}
}
return walletOutboxEvent{
EventID: eventID(transactionID, "WalletRechargeRecorded", targetUserID, ledger.AssetCoin),
EventType: "WalletRechargeRecorded",
@ -3184,6 +3229,10 @@ func coinSellerStockCreditedEvent(transactionID string, commandID string, metada
"transaction_id": transactionID,
"command_id": commandID,
"seller_user_id": metadata.SellerUserID,
"seller_country_id": metadata.SellerCountryID,
"seller_region_id": metadata.SellerRegionID,
"country_id": metadata.SellerCountryID,
"region_id": metadata.SellerRegionID,
"stock_type": metadata.StockType,
"coin_amount": metadata.CoinAmount,
"paid_currency_code": metadata.PaidCurrencyCode,
@ -3451,9 +3500,11 @@ func gameBizTypeAndDelta(opType string, coinAmount int64) (string, int64, error)
}
func coinSellerStockRequestHash(command ledger.CoinSellerStockCreditCommand) string {
return stableHash(fmt.Sprintf("coin_seller_stock|%s|%d|%s|%d|%s|%d|%s|%s|%d|%s",
return stableHash(fmt.Sprintf("coin_seller_stock|%s|%d|%d|%d|%s|%d|%s|%d|%s|%s|%d|%s",
appcode.Normalize(command.AppCode),
command.SellerUserID,
command.SellerCountryID,
command.SellerRegionID,
command.StockType,
command.CoinAmount,
command.PaidCurrencyCode,

View File

@ -842,6 +842,8 @@ func (s *Server) AdminCreditCoinSellerStock(ctx context.Context, req *walletv1.A
AppCode: req.GetAppCode(),
CommandID: req.GetCommandId(),
SellerUserID: req.GetSellerUserId(),
SellerCountryID: req.GetSellerCountryId(),
SellerRegionID: req.GetSellerRegionId(),
StockType: req.GetStockType(),
CoinAmount: req.GetCoinAmount(),
PaidCurrencyCode: req.GetPaidCurrencyCode(),
@ -872,14 +874,15 @@ func (s *Server) AdminCreditCoinSellerStock(ctx context.Context, req *walletv1.A
func (s *Server) TransferCoinFromSeller(ctx context.Context, req *walletv1.TransferCoinFromSellerRequest) (*walletv1.TransferCoinFromSellerResponse, error) {
ctx = appcode.WithContext(ctx, req.GetAppCode())
receipt, err := s.svc.TransferCoinFromSeller(ctx, ledger.CoinSellerTransferCommand{
AppCode: req.GetAppCode(),
CommandID: req.GetCommandId(),
SellerUserID: req.GetSellerUserId(),
TargetUserID: req.GetTargetUserId(),
SellerRegionID: req.GetSellerRegionId(),
TargetRegionID: req.GetTargetRegionId(),
Amount: req.GetAmount(),
Reason: req.GetReason(),
AppCode: req.GetAppCode(),
CommandID: req.GetCommandId(),
SellerUserID: req.GetSellerUserId(),
TargetUserID: req.GetTargetUserId(),
TargetCountryID: req.GetTargetCountryId(),
SellerRegionID: req.GetSellerRegionId(),
TargetRegionID: req.GetTargetRegionId(),
Amount: req.GetAmount(),
Reason: req.GetReason(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)