feat(lucky-gift): isolate strategy pools and audit balance adjustments

This commit is contained in:
zhx 2026-07-15 16:09:54 +08:00
parent 0cdbd28d46
commit eaeff155f8
30 changed files with 2119 additions and 302 deletions

View File

@ -2273,15 +2273,18 @@ type LuckyGiftPoolBalance struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
PoolId string `protobuf:"bytes,2,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
Balance int64 `protobuf:"varint,3,opt,name=balance,proto3" json:"balance,omitempty"`
ReserveFloor int64 `protobuf:"varint,4,opt,name=reserve_floor,json=reserveFloor,proto3" json:"reserve_floor,omitempty"`
AvailableBalance int64 `protobuf:"varint,5,opt,name=available_balance,json=availableBalance,proto3" json:"available_balance,omitempty"`
TotalIn int64 `protobuf:"varint,6,opt,name=total_in,json=totalIn,proto3" json:"total_in,omitempty"`
TotalOut int64 `protobuf:"varint,7,opt,name=total_out,json=totalOut,proto3" json:"total_out,omitempty"`
Materialized bool `protobuf:"varint,8,opt,name=materialized,proto3" json:"materialized,omitempty"`
UpdatedAtMs int64 `protobuf:"varint,9,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
PoolId string `protobuf:"bytes,2,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
Balance int64 `protobuf:"varint,3,opt,name=balance,proto3" json:"balance,omitempty"`
ReserveFloor int64 `protobuf:"varint,4,opt,name=reserve_floor,json=reserveFloor,proto3" json:"reserve_floor,omitempty"`
AvailableBalance int64 `protobuf:"varint,5,opt,name=available_balance,json=availableBalance,proto3" json:"available_balance,omitempty"`
TotalIn int64 `protobuf:"varint,6,opt,name=total_in,json=totalIn,proto3" json:"total_in,omitempty"`
TotalOut int64 `protobuf:"varint,7,opt,name=total_out,json=totalOut,proto3" json:"total_out,omitempty"`
Materialized bool `protobuf:"varint,8,opt,name=materialized,proto3" json:"materialized,omitempty"`
UpdatedAtMs int64 `protobuf:"varint,9,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
StrategyVersion string `protobuf:"bytes,10,opt,name=strategy_version,json=strategyVersion,proto3" json:"strategy_version,omitempty"`
ManualCreditTotal int64 `protobuf:"varint,11,opt,name=manual_credit_total,json=manualCreditTotal,proto3" json:"manual_credit_total,omitempty"`
ManualDebitTotal int64 `protobuf:"varint,12,opt,name=manual_debit_total,json=manualDebitTotal,proto3" json:"manual_debit_total,omitempty"`
}
func (x *LuckyGiftPoolBalance) Reset() {
@ -2377,6 +2380,27 @@ func (x *LuckyGiftPoolBalance) GetUpdatedAtMs() int64 {
return 0
}
func (x *LuckyGiftPoolBalance) GetStrategyVersion() string {
if x != nil {
return x.StrategyVersion
}
return ""
}
func (x *LuckyGiftPoolBalance) GetManualCreditTotal() int64 {
if x != nil {
return x.ManualCreditTotal
}
return 0
}
func (x *LuckyGiftPoolBalance) GetManualDebitTotal() int64 {
if x != nil {
return x.ManualDebitTotal
}
return 0
}
type GetLuckyGiftDrawSummaryRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -2528,8 +2552,9 @@ type ListLuckyGiftPoolBalancesRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
PoolId string `protobuf:"bytes,2,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
PoolId string `protobuf:"bytes,2,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
StrategyVersion string `protobuf:"bytes,3,opt,name=strategy_version,json=strategyVersion,proto3" json:"strategy_version,omitempty"`
}
func (x *ListLuckyGiftPoolBalancesRequest) Reset() {
@ -2576,6 +2601,13 @@ func (x *ListLuckyGiftPoolBalancesRequest) GetPoolId() string {
return ""
}
func (x *ListLuckyGiftPoolBalancesRequest) GetStrategyVersion() string {
if x != nil {
return x.StrategyVersion
}
return ""
}
type ListLuckyGiftPoolBalancesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -2700,6 +2732,295 @@ func (x *LuckyGiftHit) GetRewardCoins() int64 {
return 0
}
// 调账消息也只追加在文件尾,保持所有既有 protobuf message index 稳定。
// adjustment_id 是业务幂等键RequestMeta.request_id 只做链路追踪,不能替代资金幂等语义。
type AdjustLuckyGiftPoolBalanceRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
PoolId string `protobuf:"bytes,2,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
StrategyVersion string `protobuf:"bytes,3,opt,name=strategy_version,json=strategyVersion,proto3" json:"strategy_version,omitempty"`
AdjustmentId string `protobuf:"bytes,4,opt,name=adjustment_id,json=adjustmentId,proto3" json:"adjustment_id,omitempty"`
Direction string `protobuf:"bytes,5,opt,name=direction,proto3" json:"direction,omitempty"`
AmountCoins int64 `protobuf:"varint,6,opt,name=amount_coins,json=amountCoins,proto3" json:"amount_coins,omitempty"`
Reason string `protobuf:"bytes,7,opt,name=reason,proto3" json:"reason,omitempty"`
OperatorAdminId int64 `protobuf:"varint,8,opt,name=operator_admin_id,json=operatorAdminId,proto3" json:"operator_admin_id,omitempty"`
}
func (x *AdjustLuckyGiftPoolBalanceRequest) Reset() {
*x = AdjustLuckyGiftPoolBalanceRequest{}
mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AdjustLuckyGiftPoolBalanceRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AdjustLuckyGiftPoolBalanceRequest) ProtoMessage() {}
func (x *AdjustLuckyGiftPoolBalanceRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[29]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AdjustLuckyGiftPoolBalanceRequest.ProtoReflect.Descriptor instead.
func (*AdjustLuckyGiftPoolBalanceRequest) Descriptor() ([]byte, []int) {
return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{29}
}
func (x *AdjustLuckyGiftPoolBalanceRequest) GetMeta() *RequestMeta {
if x != nil {
return x.Meta
}
return nil
}
func (x *AdjustLuckyGiftPoolBalanceRequest) GetPoolId() string {
if x != nil {
return x.PoolId
}
return ""
}
func (x *AdjustLuckyGiftPoolBalanceRequest) GetStrategyVersion() string {
if x != nil {
return x.StrategyVersion
}
return ""
}
func (x *AdjustLuckyGiftPoolBalanceRequest) GetAdjustmentId() string {
if x != nil {
return x.AdjustmentId
}
return ""
}
func (x *AdjustLuckyGiftPoolBalanceRequest) GetDirection() string {
if x != nil {
return x.Direction
}
return ""
}
func (x *AdjustLuckyGiftPoolBalanceRequest) GetAmountCoins() int64 {
if x != nil {
return x.AmountCoins
}
return 0
}
func (x *AdjustLuckyGiftPoolBalanceRequest) GetReason() string {
if x != nil {
return x.Reason
}
return ""
}
func (x *AdjustLuckyGiftPoolBalanceRequest) GetOperatorAdminId() int64 {
if x != nil {
return x.OperatorAdminId
}
return 0
}
type LuckyGiftPoolAdjustment struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
AdjustmentId string `protobuf:"bytes,1,opt,name=adjustment_id,json=adjustmentId,proto3" json:"adjustment_id,omitempty"`
AppCode string `protobuf:"bytes,2,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
PoolId string `protobuf:"bytes,3,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
StrategyVersion string `protobuf:"bytes,4,opt,name=strategy_version,json=strategyVersion,proto3" json:"strategy_version,omitempty"`
Direction string `protobuf:"bytes,5,opt,name=direction,proto3" json:"direction,omitempty"`
AmountCoins int64 `protobuf:"varint,6,opt,name=amount_coins,json=amountCoins,proto3" json:"amount_coins,omitempty"`
Reason string `protobuf:"bytes,7,opt,name=reason,proto3" json:"reason,omitempty"`
OperatorAdminId int64 `protobuf:"varint,8,opt,name=operator_admin_id,json=operatorAdminId,proto3" json:"operator_admin_id,omitempty"`
BalanceBefore int64 `protobuf:"varint,9,opt,name=balance_before,json=balanceBefore,proto3" json:"balance_before,omitempty"`
BalanceAfter int64 `protobuf:"varint,10,opt,name=balance_after,json=balanceAfter,proto3" json:"balance_after,omitempty"`
CreatedAtMs int64 `protobuf:"varint,11,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
}
func (x *LuckyGiftPoolAdjustment) Reset() {
*x = LuckyGiftPoolAdjustment{}
mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LuckyGiftPoolAdjustment) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LuckyGiftPoolAdjustment) ProtoMessage() {}
func (x *LuckyGiftPoolAdjustment) ProtoReflect() protoreflect.Message {
mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[30]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LuckyGiftPoolAdjustment.ProtoReflect.Descriptor instead.
func (*LuckyGiftPoolAdjustment) Descriptor() ([]byte, []int) {
return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{30}
}
func (x *LuckyGiftPoolAdjustment) GetAdjustmentId() string {
if x != nil {
return x.AdjustmentId
}
return ""
}
func (x *LuckyGiftPoolAdjustment) GetAppCode() string {
if x != nil {
return x.AppCode
}
return ""
}
func (x *LuckyGiftPoolAdjustment) GetPoolId() string {
if x != nil {
return x.PoolId
}
return ""
}
func (x *LuckyGiftPoolAdjustment) GetStrategyVersion() string {
if x != nil {
return x.StrategyVersion
}
return ""
}
func (x *LuckyGiftPoolAdjustment) GetDirection() string {
if x != nil {
return x.Direction
}
return ""
}
func (x *LuckyGiftPoolAdjustment) GetAmountCoins() int64 {
if x != nil {
return x.AmountCoins
}
return 0
}
func (x *LuckyGiftPoolAdjustment) GetReason() string {
if x != nil {
return x.Reason
}
return ""
}
func (x *LuckyGiftPoolAdjustment) GetOperatorAdminId() int64 {
if x != nil {
return x.OperatorAdminId
}
return 0
}
func (x *LuckyGiftPoolAdjustment) GetBalanceBefore() int64 {
if x != nil {
return x.BalanceBefore
}
return 0
}
func (x *LuckyGiftPoolAdjustment) GetBalanceAfter() int64 {
if x != nil {
return x.BalanceAfter
}
return 0
}
func (x *LuckyGiftPoolAdjustment) GetCreatedAtMs() int64 {
if x != nil {
return x.CreatedAtMs
}
return 0
}
type AdjustLuckyGiftPoolBalanceResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Adjustment *LuckyGiftPoolAdjustment `protobuf:"bytes,1,opt,name=adjustment,proto3" json:"adjustment,omitempty"`
Pool *LuckyGiftPoolBalance `protobuf:"bytes,2,opt,name=pool,proto3" json:"pool,omitempty"`
IdempotentReplay bool `protobuf:"varint,3,opt,name=idempotent_replay,json=idempotentReplay,proto3" json:"idempotent_replay,omitempty"`
}
func (x *AdjustLuckyGiftPoolBalanceResponse) Reset() {
*x = AdjustLuckyGiftPoolBalanceResponse{}
mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AdjustLuckyGiftPoolBalanceResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AdjustLuckyGiftPoolBalanceResponse) ProtoMessage() {}
func (x *AdjustLuckyGiftPoolBalanceResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[31]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AdjustLuckyGiftPoolBalanceResponse.ProtoReflect.Descriptor instead.
func (*AdjustLuckyGiftPoolBalanceResponse) Descriptor() ([]byte, []int) {
return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{31}
}
func (x *AdjustLuckyGiftPoolBalanceResponse) GetAdjustment() *LuckyGiftPoolAdjustment {
if x != nil {
return x.Adjustment
}
return nil
}
func (x *AdjustLuckyGiftPoolBalanceResponse) GetPool() *LuckyGiftPoolBalance {
if x != nil {
return x.Pool
}
return nil
}
func (x *AdjustLuckyGiftPoolBalanceResponse) GetIdempotentReplay() bool {
if x != nil {
return x.IdempotentReplay
}
return false
}
var File_proto_luckygift_v1_luckygift_proto protoreflect.FileDescriptor
var file_proto_luckygift_v1_luckygift_proto_rawDesc = []byte{
@ -3184,7 +3505,7 @@ var file_proto_luckygift_v1_luckygift_proto_rawDesc = []byte{
0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x44, 0x72,
0x61, 0x77, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x64, 0x72,
0x61, 0x77, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x66, 0x61, 0x69, 0x6c, 0x65,
0x64, 0x44, 0x72, 0x61, 0x77, 0x73, 0x22, 0xb6, 0x02, 0x0a, 0x14, 0x4c, 0x75, 0x63, 0x6b, 0x79,
0x64, 0x44, 0x72, 0x61, 0x77, 0x73, 0x22, 0xbf, 0x03, 0x0a, 0x14, 0x4c, 0x75, 0x63, 0x6b, 0x79,
0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12,
0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f,
@ -3203,137 +3524,217 @@ var file_proto_luckygift_v1_luckygift_proto_rawDesc = []byte{
0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6d,
0x61, 0x74, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x75,
0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x09, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x22,
0xa0, 0x02, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74,
0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x33, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x1f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69,
0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74,
0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f,
0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64,
0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28,
0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6f,
0x6d, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6f, 0x6d,
0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01,
0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f,
0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f,
0x6c, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x65,
0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a,
0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x09,
0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4f, 0x6e,
0x6c, 0x79, 0x22, 0x65, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69,
0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c,
0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b,
0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79,
0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x22, 0x70, 0x0a, 0x20, 0x4c, 0x69, 0x73,
0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61,
0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a,
0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31,
0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65,
0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x22, 0x63, 0x0a, 0x21, 0x4c,
0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c,
0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x3e, 0x0a, 0x05, 0x70, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x28, 0x03, 0x52, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12,
0x29, 0x0a, 0x10, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x74, 0x72, 0x61, 0x74,
0x65, 0x67, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x6d, 0x61,
0x6e, 0x75, 0x61, 0x6c, 0x5f, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x5f, 0x74, 0x6f, 0x74, 0x61,
0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x6d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x43,
0x72, 0x65, 0x64, 0x69, 0x74, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61,
0x6e, 0x75, 0x61, 0x6c, 0x5f, 0x64, 0x65, 0x62, 0x69, 0x74, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c,
0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x6d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x44, 0x65,
0x62, 0x69, 0x74, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0xa0, 0x02, 0x0a, 0x1e, 0x47, 0x65, 0x74,
0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d,
0x6d, 0x61, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x04, 0x6d,
0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61,
0x12, 0x17, 0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72,
0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x6f, 0x6d, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73,
0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x06,
0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10,
0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64,
0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e,
0x61, 0x6c, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x65,
0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x65, 0x0a, 0x1f, 0x47,
0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53,
0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42,
0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66,
0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f,
0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x05, 0x70, 0x6f, 0x6f, 0x6c, 0x73,
0x22, 0xba, 0x01, 0x0a, 0x0c, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x48, 0x69,
0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18,
0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78,
0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x06, 0x64, 0x72, 0x61, 0x77, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x65, 0x6c,
0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x54, 0x69, 0x65,
0x72, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65,
0x72, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6d, 0x75, 0x6c,
0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x50, 0x70, 0x6d, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65,
0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0b, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x32, 0xf7, 0x03,
0x0a, 0x10, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x12, 0x67, 0x0a, 0x0e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x75, 0x63, 0x6b, 0x79,
0x47, 0x69, 0x66, 0x74, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63,
0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c,
0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66,
0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47,
0x69, 0x66, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x79, 0x0a, 0x14, 0x45,
0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44,
0x72, 0x61, 0x77, 0x12, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b,
0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65,
0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63,
0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74,
0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x19, 0x42, 0x61, 0x74, 0x63, 0x68,
0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74,
0x44, 0x72, 0x61, 0x77, 0x12, 0x34, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63,
0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x45,
0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44,
0x72, 0x61, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x68, 0x79, 0x61,
0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e,
0x42, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b,
0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x74, 0x0a, 0x17, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x45, 0x78, 0x74, 0x65,
0x72, 0x6e, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x12, 0x2b, 0x2e, 0x68,
0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72,
0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61,
0x72, 0x79, 0x22, 0x9b, 0x01, 0x0a, 0x20, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79,
0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75,
0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07,
0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70,
0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67,
0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x22, 0x63, 0x0a, 0x21, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66,
0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x05, 0x70, 0x6f, 0x6f, 0x6c, 0x73, 0x18, 0x01,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63,
0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47,
0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x05,
0x70, 0x6f, 0x6f, 0x6c, 0x73, 0x22, 0xba, 0x01, 0x0a, 0x0c, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47,
0x69, 0x66, 0x74, 0x48, 0x69, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69,
0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74,
0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x69, 0x64,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x72, 0x61, 0x77, 0x49, 0x64, 0x12, 0x28,
0x0a, 0x10, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x65, 0x72, 0x5f,
0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
0x65, 0x64, 0x54, 0x69, 0x65, 0x72, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x75, 0x6c, 0x74,
0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0d, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x50, 0x70, 0x6d, 0x12,
0x21, 0x0a, 0x0c, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18,
0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x72, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69,
0x6e, 0x73, 0x22, 0xc6, 0x02, 0x0a, 0x21, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x4c, 0x75, 0x63,
0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c,
0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a,
0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65,
0x67, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x5f,
0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x64, 0x6a, 0x75, 0x73, 0x74,
0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63,
0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63,
0x6f, 0x69, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x6d, 0x6f, 0x75,
0x6e, 0x74, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f,
0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12,
0x2a, 0x0a, 0x11, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x61, 0x64, 0x6d, 0x69,
0x6e, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6f, 0x70, 0x65, 0x72,
0x61, 0x74, 0x6f, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x49, 0x64, 0x22, 0x92, 0x03, 0x0a, 0x17,
0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x64, 0x6a,
0x75, 0x73, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x64, 0x6a, 0x75, 0x73,
0x74, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
0x61, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08,
0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f,
0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64,
0x12, 0x29, 0x0a, 0x10, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x5f, 0x76, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x74, 0x72, 0x61,
0x74, 0x65, 0x67, 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x64,
0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x6d, 0x6f,
0x75, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0b, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x16, 0x0a, 0x06,
0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65,
0x61, 0x73, 0x6f, 0x6e, 0x12, 0x2a, 0x0a, 0x11, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72,
0x5f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x49, 0x64,
0x12, 0x25, 0x0a, 0x0e, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x62, 0x65, 0x66, 0x6f,
0x72, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63,
0x65, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e,
0x63, 0x65, 0x5f, 0x61, 0x66, 0x74, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c,
0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x66, 0x74, 0x65, 0x72, 0x12, 0x22, 0x0a, 0x0d,
0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0b, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73,
0x22, 0xdc, 0x01, 0x0a, 0x22, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79,
0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0a, 0x61, 0x64, 0x6a, 0x75, 0x73,
0x74, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31,
0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x41, 0x64,
0x6a, 0x75, 0x73, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0a, 0x61, 0x64, 0x6a, 0x75, 0x73, 0x74,
0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3c, 0x0a, 0x04, 0x70, 0x6f, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79,
0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66,
0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x04, 0x70, 0x6f,
0x6f, 0x6c, 0x12, 0x2b, 0x0a, 0x11, 0x69, 0x64, 0x65, 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x74,
0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x69,
0x64, 0x65, 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x32,
0xf7, 0x03, 0x0a, 0x10, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x12, 0x67, 0x0a, 0x0e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x75, 0x63,
0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c,
0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63,
0x6b, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67,
0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x75, 0x63, 0x6b,
0x79, 0x47, 0x69, 0x66, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x79, 0x0a,
0x14, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66,
0x74, 0x44, 0x72, 0x61, 0x77, 0x12, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75,
0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75,
0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c,
0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63,
0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x19, 0x42, 0x61, 0x74,
0x63, 0x68, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69,
0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x12, 0x34, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c,
0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63,
0x68, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66,
0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x68,
0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76,
0x31, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72,
0x61, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45,
0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x8a, 0x06, 0x0a, 0x15, 0x41, 0x64, 0x6d, 0x69,
0x6e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x12, 0x73, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66,
0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e,
0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74,
0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c,
0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c,
0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x15, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74,
0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
0x30, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66,
0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79,
0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x31, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67,
0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75,
0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x74, 0x0a, 0x17, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x45, 0x78,
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x12, 0x2b,
0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74,
0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74,
0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x68, 0x79,
0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31,
0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61,
0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x98, 0x07, 0x0a, 0x15, 0x41, 0x64,
0x6d, 0x69, 0x6e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47,
0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47,
0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70,
0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65,
0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x15, 0x55, 0x70, 0x73, 0x65,
0x72, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x12, 0x30, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67,
0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x4c, 0x75, 0x63,
0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x79, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b,
0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x2f, 0x2e, 0x68,
0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76,
0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43,
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e,
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e,
0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74,
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x73, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74,
0x44, 0x72, 0x61, 0x77, 0x73, 0x12, 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75,
0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c,
0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x73, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63,
0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75,
0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b,
0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79,
0x12, 0x32, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69,
0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69,
0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63,
0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63,
0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72,
0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x19, 0x4c, 0x69,
0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42,
0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x34, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e,
0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b,
0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x4c,
0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x79, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75,
0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x2f,
0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74,
0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66,
0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x30, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66,
0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69,
0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x73, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69,
0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x73, 0x12, 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e,
0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73,
0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c,
0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4c, 0x75,
0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61,
0x72, 0x79, 0x12, 0x32, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79,
0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79,
0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c,
0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c,
0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d,
0x61, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x19,
0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f,
0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x34, 0x2e, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c,
0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c,
0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x35, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66,
0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69,
0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8b, 0x01, 0x0a, 0x1a, 0x41, 0x64, 0x6a, 0x75, 0x73,
0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61,
0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e,
0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e,
0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74,
0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x35, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75,
0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x6a, 0x75, 0x73,
0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61,
0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x68,
0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76,
0x31, 0x2e, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66,
0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x42, 0x30, 0x5a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f,
0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6c, 0x75,
0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x6c, 0x75, 0x63, 0x6b, 0x79,
@ -3352,37 +3753,40 @@ func file_proto_luckygift_v1_luckygift_proto_rawDescGZIP() []byte {
return file_proto_luckygift_v1_luckygift_proto_rawDescData
}
var file_proto_luckygift_v1_luckygift_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
var file_proto_luckygift_v1_luckygift_proto_msgTypes = make([]protoimpl.MessageInfo, 32)
var file_proto_luckygift_v1_luckygift_proto_goTypes = []any{
(*RequestMeta)(nil), // 0: hyapp.luckygift.v1.RequestMeta
(*LuckyGiftMeta)(nil), // 1: hyapp.luckygift.v1.LuckyGiftMeta
(*LuckyGiftRuleTier)(nil), // 2: hyapp.luckygift.v1.LuckyGiftRuleTier
(*LuckyGiftRuleStage)(nil), // 3: hyapp.luckygift.v1.LuckyGiftRuleStage
(*LuckyGiftRuleConfig)(nil), // 4: hyapp.luckygift.v1.LuckyGiftRuleConfig
(*CheckLuckyGiftRequest)(nil), // 5: hyapp.luckygift.v1.CheckLuckyGiftRequest
(*CheckLuckyGiftResponse)(nil), // 6: hyapp.luckygift.v1.CheckLuckyGiftResponse
(*LuckyGiftDrawResult)(nil), // 7: hyapp.luckygift.v1.LuckyGiftDrawResult
(*ExecuteLuckyGiftDrawRequest)(nil), // 8: hyapp.luckygift.v1.ExecuteLuckyGiftDrawRequest
(*ExecuteLuckyGiftDrawResponse)(nil), // 9: hyapp.luckygift.v1.ExecuteLuckyGiftDrawResponse
(*BatchExecuteLuckyGiftDrawRequest)(nil), // 10: hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawRequest
(*BatchExecuteLuckyGiftDrawResponse)(nil), // 11: hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawResponse
(*ExternalGiftDrawRequest)(nil), // 12: hyapp.luckygift.v1.ExternalGiftDrawRequest
(*ExternalGiftDrawResponse)(nil), // 13: hyapp.luckygift.v1.ExternalGiftDrawResponse
(*GetLuckyGiftConfigRequest)(nil), // 14: hyapp.luckygift.v1.GetLuckyGiftConfigRequest
(*GetLuckyGiftConfigResponse)(nil), // 15: hyapp.luckygift.v1.GetLuckyGiftConfigResponse
(*UpsertLuckyGiftConfigRequest)(nil), // 16: hyapp.luckygift.v1.UpsertLuckyGiftConfigRequest
(*UpsertLuckyGiftConfigResponse)(nil), // 17: hyapp.luckygift.v1.UpsertLuckyGiftConfigResponse
(*ListLuckyGiftConfigsRequest)(nil), // 18: hyapp.luckygift.v1.ListLuckyGiftConfigsRequest
(*ListLuckyGiftConfigsResponse)(nil), // 19: hyapp.luckygift.v1.ListLuckyGiftConfigsResponse
(*ListLuckyGiftDrawsRequest)(nil), // 20: hyapp.luckygift.v1.ListLuckyGiftDrawsRequest
(*ListLuckyGiftDrawsResponse)(nil), // 21: hyapp.luckygift.v1.ListLuckyGiftDrawsResponse
(*LuckyGiftDrawSummary)(nil), // 22: hyapp.luckygift.v1.LuckyGiftDrawSummary
(*LuckyGiftPoolBalance)(nil), // 23: hyapp.luckygift.v1.LuckyGiftPoolBalance
(*GetLuckyGiftDrawSummaryRequest)(nil), // 24: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest
(*GetLuckyGiftDrawSummaryResponse)(nil), // 25: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse
(*ListLuckyGiftPoolBalancesRequest)(nil), // 26: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest
(*ListLuckyGiftPoolBalancesResponse)(nil), // 27: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse
(*LuckyGiftHit)(nil), // 28: hyapp.luckygift.v1.LuckyGiftHit
(*RequestMeta)(nil), // 0: hyapp.luckygift.v1.RequestMeta
(*LuckyGiftMeta)(nil), // 1: hyapp.luckygift.v1.LuckyGiftMeta
(*LuckyGiftRuleTier)(nil), // 2: hyapp.luckygift.v1.LuckyGiftRuleTier
(*LuckyGiftRuleStage)(nil), // 3: hyapp.luckygift.v1.LuckyGiftRuleStage
(*LuckyGiftRuleConfig)(nil), // 4: hyapp.luckygift.v1.LuckyGiftRuleConfig
(*CheckLuckyGiftRequest)(nil), // 5: hyapp.luckygift.v1.CheckLuckyGiftRequest
(*CheckLuckyGiftResponse)(nil), // 6: hyapp.luckygift.v1.CheckLuckyGiftResponse
(*LuckyGiftDrawResult)(nil), // 7: hyapp.luckygift.v1.LuckyGiftDrawResult
(*ExecuteLuckyGiftDrawRequest)(nil), // 8: hyapp.luckygift.v1.ExecuteLuckyGiftDrawRequest
(*ExecuteLuckyGiftDrawResponse)(nil), // 9: hyapp.luckygift.v1.ExecuteLuckyGiftDrawResponse
(*BatchExecuteLuckyGiftDrawRequest)(nil), // 10: hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawRequest
(*BatchExecuteLuckyGiftDrawResponse)(nil), // 11: hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawResponse
(*ExternalGiftDrawRequest)(nil), // 12: hyapp.luckygift.v1.ExternalGiftDrawRequest
(*ExternalGiftDrawResponse)(nil), // 13: hyapp.luckygift.v1.ExternalGiftDrawResponse
(*GetLuckyGiftConfigRequest)(nil), // 14: hyapp.luckygift.v1.GetLuckyGiftConfigRequest
(*GetLuckyGiftConfigResponse)(nil), // 15: hyapp.luckygift.v1.GetLuckyGiftConfigResponse
(*UpsertLuckyGiftConfigRequest)(nil), // 16: hyapp.luckygift.v1.UpsertLuckyGiftConfigRequest
(*UpsertLuckyGiftConfigResponse)(nil), // 17: hyapp.luckygift.v1.UpsertLuckyGiftConfigResponse
(*ListLuckyGiftConfigsRequest)(nil), // 18: hyapp.luckygift.v1.ListLuckyGiftConfigsRequest
(*ListLuckyGiftConfigsResponse)(nil), // 19: hyapp.luckygift.v1.ListLuckyGiftConfigsResponse
(*ListLuckyGiftDrawsRequest)(nil), // 20: hyapp.luckygift.v1.ListLuckyGiftDrawsRequest
(*ListLuckyGiftDrawsResponse)(nil), // 21: hyapp.luckygift.v1.ListLuckyGiftDrawsResponse
(*LuckyGiftDrawSummary)(nil), // 22: hyapp.luckygift.v1.LuckyGiftDrawSummary
(*LuckyGiftPoolBalance)(nil), // 23: hyapp.luckygift.v1.LuckyGiftPoolBalance
(*GetLuckyGiftDrawSummaryRequest)(nil), // 24: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest
(*GetLuckyGiftDrawSummaryResponse)(nil), // 25: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse
(*ListLuckyGiftPoolBalancesRequest)(nil), // 26: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest
(*ListLuckyGiftPoolBalancesResponse)(nil), // 27: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse
(*LuckyGiftHit)(nil), // 28: hyapp.luckygift.v1.LuckyGiftHit
(*AdjustLuckyGiftPoolBalanceRequest)(nil), // 29: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest
(*LuckyGiftPoolAdjustment)(nil), // 30: hyapp.luckygift.v1.LuckyGiftPoolAdjustment
(*AdjustLuckyGiftPoolBalanceResponse)(nil), // 31: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse
}
var file_proto_luckygift_v1_luckygift_proto_depIdxs = []int32{
0, // 0: hyapp.luckygift.v1.LuckyGiftMeta.meta:type_name -> hyapp.luckygift.v1.RequestMeta
@ -3408,31 +3812,36 @@ var file_proto_luckygift_v1_luckygift_proto_depIdxs = []int32{
22, // 20: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse.summary:type_name -> hyapp.luckygift.v1.LuckyGiftDrawSummary
0, // 21: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta
23, // 22: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse.pools:type_name -> hyapp.luckygift.v1.LuckyGiftPoolBalance
5, // 23: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:input_type -> hyapp.luckygift.v1.CheckLuckyGiftRequest
8, // 24: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawRequest
10, // 25: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawRequest
12, // 26: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:input_type -> hyapp.luckygift.v1.ExternalGiftDrawRequest
14, // 27: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:input_type -> hyapp.luckygift.v1.GetLuckyGiftConfigRequest
16, // 28: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:input_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigRequest
18, // 29: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:input_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsRequest
20, // 30: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:input_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsRequest
24, // 31: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:input_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest
26, // 32: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:input_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest
6, // 33: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:output_type -> hyapp.luckygift.v1.CheckLuckyGiftResponse
9, // 34: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawResponse
11, // 35: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawResponse
13, // 36: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:output_type -> hyapp.luckygift.v1.ExternalGiftDrawResponse
15, // 37: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:output_type -> hyapp.luckygift.v1.GetLuckyGiftConfigResponse
17, // 38: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:output_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigResponse
19, // 39: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:output_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsResponse
21, // 40: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:output_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsResponse
25, // 41: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:output_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse
27, // 42: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:output_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse
33, // [33:43] is the sub-list for method output_type
23, // [23:33] is the sub-list for method input_type
23, // [23:23] is the sub-list for extension type_name
23, // [23:23] is the sub-list for extension extendee
0, // [0:23] is the sub-list for field type_name
0, // 23: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta
30, // 24: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse.adjustment:type_name -> hyapp.luckygift.v1.LuckyGiftPoolAdjustment
23, // 25: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse.pool:type_name -> hyapp.luckygift.v1.LuckyGiftPoolBalance
5, // 26: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:input_type -> hyapp.luckygift.v1.CheckLuckyGiftRequest
8, // 27: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawRequest
10, // 28: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawRequest
12, // 29: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:input_type -> hyapp.luckygift.v1.ExternalGiftDrawRequest
14, // 30: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:input_type -> hyapp.luckygift.v1.GetLuckyGiftConfigRequest
16, // 31: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:input_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigRequest
18, // 32: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:input_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsRequest
20, // 33: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:input_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsRequest
24, // 34: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:input_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest
26, // 35: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:input_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest
29, // 36: hyapp.luckygift.v1.AdminLuckyGiftService.AdjustLuckyGiftPoolBalance:input_type -> hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest
6, // 37: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:output_type -> hyapp.luckygift.v1.CheckLuckyGiftResponse
9, // 38: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawResponse
11, // 39: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawResponse
13, // 40: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:output_type -> hyapp.luckygift.v1.ExternalGiftDrawResponse
15, // 41: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:output_type -> hyapp.luckygift.v1.GetLuckyGiftConfigResponse
17, // 42: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:output_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigResponse
19, // 43: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:output_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsResponse
21, // 44: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:output_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsResponse
25, // 45: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:output_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse
27, // 46: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:output_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse
31, // 47: hyapp.luckygift.v1.AdminLuckyGiftService.AdjustLuckyGiftPoolBalance:output_type -> hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse
37, // [37:48] is the sub-list for method output_type
26, // [26:37] is the sub-list for method input_type
26, // [26:26] is the sub-list for extension type_name
26, // [26:26] is the sub-list for extension extendee
0, // [0:26] is the sub-list for field type_name
}
func init() { file_proto_luckygift_v1_luckygift_proto_init() }
@ -3446,7 +3855,7 @@ func file_proto_luckygift_v1_luckygift_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_luckygift_v1_luckygift_proto_rawDesc,
NumEnums: 0,
NumMessages: 29,
NumMessages: 32,
NumExtensions: 0,
NumServices: 2,
},

View File

@ -269,6 +269,9 @@ message LuckyGiftPoolBalance {
int64 total_out = 7;
bool materialized = 8;
int64 updated_at_ms = 9;
string strategy_version = 10;
int64 manual_credit_total = 11;
int64 manual_debit_total = 12;
}
message GetLuckyGiftDrawSummaryRequest {
@ -289,6 +292,7 @@ message GetLuckyGiftDrawSummaryResponse {
message ListLuckyGiftPoolBalancesRequest {
RequestMeta meta = 1;
string pool_id = 2;
string strategy_version = 3;
}
message ListLuckyGiftPoolBalancesResponse {
@ -309,6 +313,7 @@ service AdminLuckyGiftService {
rpc ListLuckyGiftDraws(ListLuckyGiftDrawsRequest) returns (ListLuckyGiftDrawsResponse);
rpc GetLuckyGiftDrawSummary(GetLuckyGiftDrawSummaryRequest) returns (GetLuckyGiftDrawSummaryResponse);
rpc ListLuckyGiftPoolBalances(ListLuckyGiftPoolBalancesRequest) returns (ListLuckyGiftPoolBalancesResponse);
rpc AdjustLuckyGiftPoolBalance(AdjustLuckyGiftPoolBalanceRequest) returns (AdjustLuckyGiftPoolBalanceResponse);
}
// LuckyGiftHit
@ -320,3 +325,36 @@ message LuckyGiftHit {
int64 multiplier_ppm = 4;
int64 reward_coins = 5;
}
// protobuf message index
// adjustment_id RequestMeta.request_id
message AdjustLuckyGiftPoolBalanceRequest {
RequestMeta meta = 1;
string pool_id = 2;
string strategy_version = 3;
string adjustment_id = 4;
string direction = 5;
int64 amount_coins = 6;
string reason = 7;
int64 operator_admin_id = 8;
}
message LuckyGiftPoolAdjustment {
string adjustment_id = 1;
string app_code = 2;
string pool_id = 3;
string strategy_version = 4;
string direction = 5;
int64 amount_coins = 6;
string reason = 7;
int64 operator_admin_id = 8;
int64 balance_before = 9;
int64 balance_after = 10;
int64 created_at_ms = 11;
}
message AdjustLuckyGiftPoolBalanceResponse {
LuckyGiftPoolAdjustment adjustment = 1;
LuckyGiftPoolBalance pool = 2;
bool idempotent_replay = 3;
}

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc v5.29.2
// - protoc v7.35.0
// source: proto/luckygift/v1/luckygift.proto
package luckygiftv1
@ -235,12 +235,13 @@ var LuckyGiftService_ServiceDesc = grpc.ServiceDesc{
}
const (
AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftConfig"
AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/UpsertLuckyGiftConfig"
AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftConfigs"
AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftDraws"
AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftDrawSummary"
AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftPoolBalances"
AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftConfig"
AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/UpsertLuckyGiftConfig"
AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftConfigs"
AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftDraws"
AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftDrawSummary"
AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftPoolBalances"
AdminLuckyGiftService_AdjustLuckyGiftPoolBalance_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/AdjustLuckyGiftPoolBalance"
)
// AdminLuckyGiftServiceClient is the client API for AdminLuckyGiftService service.
@ -253,6 +254,7 @@ type AdminLuckyGiftServiceClient interface {
ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error)
GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error)
ListLuckyGiftPoolBalances(ctx context.Context, in *ListLuckyGiftPoolBalancesRequest, opts ...grpc.CallOption) (*ListLuckyGiftPoolBalancesResponse, error)
AdjustLuckyGiftPoolBalance(ctx context.Context, in *AdjustLuckyGiftPoolBalanceRequest, opts ...grpc.CallOption) (*AdjustLuckyGiftPoolBalanceResponse, error)
}
type adminLuckyGiftServiceClient struct {
@ -323,6 +325,16 @@ func (c *adminLuckyGiftServiceClient) ListLuckyGiftPoolBalances(ctx context.Cont
return out, nil
}
func (c *adminLuckyGiftServiceClient) AdjustLuckyGiftPoolBalance(ctx context.Context, in *AdjustLuckyGiftPoolBalanceRequest, opts ...grpc.CallOption) (*AdjustLuckyGiftPoolBalanceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdjustLuckyGiftPoolBalanceResponse)
err := c.cc.Invoke(ctx, AdminLuckyGiftService_AdjustLuckyGiftPoolBalance_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AdminLuckyGiftServiceServer is the server API for AdminLuckyGiftService service.
// All implementations must embed UnimplementedAdminLuckyGiftServiceServer
// for forward compatibility.
@ -333,6 +345,7 @@ type AdminLuckyGiftServiceServer interface {
ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error)
GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error)
ListLuckyGiftPoolBalances(context.Context, *ListLuckyGiftPoolBalancesRequest) (*ListLuckyGiftPoolBalancesResponse, error)
AdjustLuckyGiftPoolBalance(context.Context, *AdjustLuckyGiftPoolBalanceRequest) (*AdjustLuckyGiftPoolBalanceResponse, error)
mustEmbedUnimplementedAdminLuckyGiftServiceServer()
}
@ -361,6 +374,9 @@ func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftDrawSummary(context.
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftPoolBalances(context.Context, *ListLuckyGiftPoolBalancesRequest) (*ListLuckyGiftPoolBalancesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftPoolBalances not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) AdjustLuckyGiftPoolBalance(context.Context, *AdjustLuckyGiftPoolBalanceRequest) (*AdjustLuckyGiftPoolBalanceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdjustLuckyGiftPoolBalance not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) mustEmbedUnimplementedAdminLuckyGiftServiceServer() {}
func (UnimplementedAdminLuckyGiftServiceServer) testEmbeddedByValue() {}
@ -490,6 +506,24 @@ func _AdminLuckyGiftService_ListLuckyGiftPoolBalances_Handler(srv interface{}, c
return interceptor(ctx, in, info, handler)
}
func _AdminLuckyGiftService_AdjustLuckyGiftPoolBalance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdjustLuckyGiftPoolBalanceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminLuckyGiftServiceServer).AdjustLuckyGiftPoolBalance(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdminLuckyGiftService_AdjustLuckyGiftPoolBalance_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminLuckyGiftServiceServer).AdjustLuckyGiftPoolBalance(ctx, req.(*AdjustLuckyGiftPoolBalanceRequest))
}
return interceptor(ctx, in, info, handler)
}
// AdminLuckyGiftService_ServiceDesc is the grpc.ServiceDesc for AdminLuckyGiftService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -521,6 +555,10 @@ var AdminLuckyGiftService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListLuckyGiftPoolBalances",
Handler: _AdminLuckyGiftService_ListLuckyGiftPoolBalances_Handler,
},
{
MethodName: "AdjustLuckyGiftPoolBalance",
Handler: _AdminLuckyGiftService_AdjustLuckyGiftPoolBalance_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/luckygift/v1/luckygift.proto",

View File

@ -17,6 +17,7 @@ type Client interface {
ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error)
GetLuckyGiftDrawSummary(ctx context.Context, req *luckygiftv1.GetLuckyGiftDrawSummaryRequest) (*luckygiftv1.GetLuckyGiftDrawSummaryResponse, error)
ListLuckyGiftPoolBalances(ctx context.Context, req *luckygiftv1.ListLuckyGiftPoolBalancesRequest) (*luckygiftv1.ListLuckyGiftPoolBalancesResponse, error)
AdjustLuckyGiftPoolBalance(ctx context.Context, req *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest) (*luckygiftv1.AdjustLuckyGiftPoolBalanceResponse, error)
}
type GRPCClient struct {
@ -50,3 +51,7 @@ func (c *GRPCClient) GetLuckyGiftDrawSummary(ctx context.Context, req *luckygift
func (c *GRPCClient) ListLuckyGiftPoolBalances(ctx context.Context, req *luckygiftv1.ListLuckyGiftPoolBalancesRequest) (*luckygiftv1.ListLuckyGiftPoolBalancesResponse, error) {
return c.client.ListLuckyGiftPoolBalances(ctx, req)
}
func (c *GRPCClient) AdjustLuckyGiftPoolBalance(ctx context.Context, req *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest) (*luckygiftv1.AdjustLuckyGiftPoolBalanceResponse, error) {
return c.client.AdjustLuckyGiftPoolBalance(ctx, req)
}

View File

@ -14,6 +14,8 @@ import (
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
)
type Handler struct {
@ -128,15 +130,44 @@ type drawSummaryDTO struct {
}
type poolBalanceDTO struct {
AppCode string `json:"app_code"`
PoolID string `json:"pool_id"`
Balance int64 `json:"balance"`
ReserveFloor int64 `json:"reserve_floor"`
AvailableBalance int64 `json:"available_balance"`
TotalIn int64 `json:"total_in"`
TotalOut int64 `json:"total_out"`
Materialized bool `json:"materialized"`
UpdatedAtMS int64 `json:"updated_at_ms"`
AppCode string `json:"app_code"`
PoolID string `json:"pool_id"`
StrategyVersion string `json:"strategy_version"`
Balance int64 `json:"balance"`
ReserveFloor int64 `json:"reserve_floor"`
AvailableBalance int64 `json:"available_balance"`
TotalIn int64 `json:"total_in"`
TotalOut int64 `json:"total_out"`
ManualCreditTotal int64 `json:"manual_credit_total"`
ManualDebitTotal int64 `json:"manual_debit_total"`
Materialized bool `json:"materialized"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type poolAdjustmentRequest struct {
AdjustmentID string `json:"adjustment_id"`
AmountCoins int64 `json:"amount_coins"`
Reason string `json:"reason"`
}
type poolAdjustmentDTO struct {
AdjustmentID string `json:"adjustment_id"`
AppCode string `json:"app_code"`
PoolID string `json:"pool_id"`
StrategyVersion string `json:"strategy_version"`
Direction string `json:"direction"`
AmountCoins int64 `json:"amount_coins"`
Reason string `json:"reason"`
OperatorAdminID int64 `json:"operator_admin_id"`
BalanceBefore int64 `json:"balance_before"`
BalanceAfter int64 `json:"balance_after"`
CreatedAtMS int64 `json:"created_at_ms"`
}
type poolAdjustmentResponseDTO struct {
Adjustment poolAdjustmentDTO `json:"adjustment"`
Pool poolBalanceDTO `json:"pool"`
IdempotentReplay bool `json:"idempotent_replay"`
}
func (h *Handler) GetLuckyGiftConfig(c *gin.Context) {
@ -273,8 +304,9 @@ func (h *Handler) ListLuckyGiftPoolBalances(c *gin.Context) {
ctx, cancel := h.luckyGiftContext(c)
defer cancel()
resp, err := h.luckyGift.ListLuckyGiftPoolBalances(ctx, &luckygiftv1.ListLuckyGiftPoolBalancesRequest{
Meta: h.meta(c),
PoolId: strings.TrimSpace(c.Query("pool_id")),
Meta: h.meta(c),
PoolId: strings.TrimSpace(c.Query("pool_id")),
StrategyVersion: strings.ToLower(strings.TrimSpace(c.Query("strategy_version"))),
})
if err != nil {
response.ServerError(c, "获取幸运礼物奖池余额失败")
@ -287,6 +319,75 @@ func (h *Handler) ListLuckyGiftPoolBalances(c *gin.Context) {
response.OK(c, gin.H{"items": items, "total": len(items)})
}
func (h *Handler) CreditLuckyGiftPoolBalance(c *gin.Context) {
h.adjustLuckyGiftPoolBalance(c, "in")
}
func (h *Handler) DebitLuckyGiftPoolBalance(c *gin.Context) {
h.adjustLuckyGiftPoolBalance(c, "out")
}
func (h *Handler) adjustLuckyGiftPoolBalance(c *gin.Context, direction string) {
appCode := strings.ToLower(strings.TrimSpace(c.Query("app_code")))
poolID := strings.TrimSpace(c.Query("pool_id"))
strategyVersion := strings.ToLower(strings.TrimSpace(c.Query("strategy_version")))
if appCode == "" || poolID == "" || (strategyVersion != "fixed_v2" && strategyVersion != "dynamic_v3") {
response.BadRequest(c, "app_code, pool_id and strategy_version are required")
return
}
var req poolAdjustmentRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "幸运礼物奖池调整参数不正确")
return
}
req.AdjustmentID = strings.TrimSpace(req.AdjustmentID)
req.Reason = strings.TrimSpace(req.Reason)
if req.AdjustmentID == "" || req.AmountCoins <= 0 || req.Reason == "" {
response.BadRequest(c, "adjustment_id, positive amount_coins and reason are required")
return
}
ctx, cancel := h.luckyGiftContext(c)
defer cancel()
resp, err := h.luckyGift.AdjustLuckyGiftPoolBalance(ctx, &luckygiftv1.AdjustLuckyGiftPoolBalanceRequest{
Meta: h.metaForApp(c, appCode),
PoolId: poolID,
StrategyVersion: strategyVersion,
AdjustmentId: req.AdjustmentID,
Direction: direction,
AmountCoins: req.AmountCoins,
Reason: req.Reason,
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
h.writePoolAdjustmentError(c, err)
return
}
item := poolAdjustmentResponseDTO{
Adjustment: poolAdjustmentFromProto(resp.GetAdjustment()),
Pool: poolBalanceFromProto(resp.GetPool()),
IdempotentReplay: resp.GetIdempotentReplay(),
}
shared.OperationLogWithResourceID(
c, h.audit, "adjust-lucky-gift-pool-"+direction, "lucky_pool_adjustments", item.Adjustment.AdjustmentID, "success",
fmt.Sprintf("app_code=%s pool_id=%s strategy_version=%s direction=%s amount_coins=%d reason=%s", appCode, poolID, strategyVersion, direction, req.AmountCoins, req.Reason),
)
response.OK(c, item)
}
func (h *Handler) writePoolAdjustmentError(c *gin.Context, err error) {
status := grpcstatus.Convert(err)
switch status.Code() {
case codes.InvalidArgument:
response.BadRequest(c, status.Message())
case codes.NotFound:
response.NotFound(c, status.Message())
case codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
response.Conflict(c, status.Message())
default:
response.ServerError(c, "调整幸运礼物奖池余额失败")
}
}
func (h *Handler) meta(c *gin.Context) *luckygiftv1.RequestMeta {
return h.metaForApp(c, "")
}
@ -311,6 +412,12 @@ func (h *Handler) luckyGiftContext(c *gin.Context) (context.Context, context.Can
}
func configToProto(req configRequest) *luckygiftv1.LuckyGiftRuleConfig {
strategyVersion := strings.ToLower(strings.TrimSpace(req.StrategyVersion))
initialPoolCoins := req.InitialPoolCoins
if strategyVersion == "dynamic_v3" {
// V3 启动资金已经迁移为独立人工 credit旧页面残留的 1,000,000 不能在再次保存时回写不可变配置。
initialPoolCoins = 0
}
stages := make([]*luckygiftv1.LuckyGiftRuleStage, 0, len(req.Stages))
for _, stage := range req.Stages {
stageName := strings.TrimSpace(stage.Stage)
@ -339,7 +446,7 @@ func configToProto(req configRequest) *luckygiftv1.LuckyGiftRuleConfig {
return &luckygiftv1.LuckyGiftRuleConfig{
AppCode: strings.ToLower(strings.TrimSpace(req.AppCode)),
PoolId: strings.TrimSpace(req.PoolID),
StrategyVersion: strings.ToLower(strings.TrimSpace(req.StrategyVersion)),
StrategyVersion: strategyVersion,
Enabled: req.Enabled,
TargetRtpPpm: percentToPPM(req.TargetRTPPercent),
PoolRatePpm: percentToPPM(req.PoolRatePercent),
@ -351,7 +458,7 @@ func configToProto(req configRequest) *luckygiftv1.LuckyGiftRuleConfig {
NoviceMaxEquivalentDraws: req.NoviceMaxEquivalentDraws,
NormalMaxEquivalentDraws: req.NormalMaxEquivalentDraws,
EffectiveFromMs: req.EffectiveFromMS,
InitialPoolCoins: req.InitialPoolCoins,
InitialPoolCoins: initialPoolCoins,
LossStreakGuarantee: req.LossStreakGuarantee,
LowWatermarkCoins: req.LowWatermarkCoins,
LowWaterNonzeroFactorPpm: percentToPPM(req.LowWaterNonzeroFactorPercent),
@ -399,6 +506,12 @@ func configFromProto(config *luckygiftv1.LuckyGiftRuleConfig) configDTO {
})
}
poolID := strings.TrimSpace(config.GetPoolId())
strategyVersion := strings.ToLower(strings.TrimSpace(config.GetStrategyVersion()))
initialPoolCoins := config.GetInitialPoolCoins()
if strategyVersion == "dynamic_v3" {
// 兼容读取迁移前 V3 快照时也展示 0避免运营误以为规则字段仍会自动形成账本资金。
initialPoolCoins = 0
}
return configDTO{
RuleVersion: config.GetRuleVersion(),
CreatedByAdminID: config.GetCreatedByAdminId(),
@ -406,7 +519,7 @@ func configFromProto(config *luckygiftv1.LuckyGiftRuleConfig) configDTO {
configRequest: configRequest{
AppCode: config.GetAppCode(),
PoolID: poolID,
StrategyVersion: config.GetStrategyVersion(),
StrategyVersion: strategyVersion,
Enabled: config.GetEnabled(),
TargetRTPPercent: ppmToPercent(config.GetTargetRtpPpm()),
PoolRatePercent: ppmToPercent(config.GetPoolRatePpm()),
@ -418,7 +531,7 @@ func configFromProto(config *luckygiftv1.LuckyGiftRuleConfig) configDTO {
NoviceMaxEquivalentDraws: config.GetNoviceMaxEquivalentDraws(),
NormalMaxEquivalentDraws: config.GetNormalMaxEquivalentDraws(),
EffectiveFromMS: config.GetEffectiveFromMs(),
InitialPoolCoins: config.GetInitialPoolCoins(),
InitialPoolCoins: initialPoolCoins,
LossStreakGuarantee: config.GetLossStreakGuarantee(),
LowWatermarkCoins: config.GetLowWatermarkCoins(),
LowWaterNonzeroFactorPercent: ppmToPercent(config.GetLowWaterNonzeroFactorPpm()),
@ -551,15 +664,37 @@ func poolBalanceFromProto(pool *luckygiftv1.LuckyGiftPoolBalance) poolBalanceDTO
return poolBalanceDTO{}
}
return poolBalanceDTO{
AppCode: pool.GetAppCode(),
PoolID: pool.GetPoolId(),
Balance: pool.GetBalance(),
ReserveFloor: pool.GetReserveFloor(),
AvailableBalance: pool.GetAvailableBalance(),
TotalIn: pool.GetTotalIn(),
TotalOut: pool.GetTotalOut(),
Materialized: pool.GetMaterialized(),
UpdatedAtMS: pool.GetUpdatedAtMs(),
AppCode: pool.GetAppCode(),
PoolID: pool.GetPoolId(),
StrategyVersion: pool.GetStrategyVersion(),
Balance: pool.GetBalance(),
ReserveFloor: pool.GetReserveFloor(),
AvailableBalance: pool.GetAvailableBalance(),
TotalIn: pool.GetTotalIn(),
TotalOut: pool.GetTotalOut(),
ManualCreditTotal: pool.GetManualCreditTotal(),
ManualDebitTotal: pool.GetManualDebitTotal(),
Materialized: pool.GetMaterialized(),
UpdatedAtMS: pool.GetUpdatedAtMs(),
}
}
func poolAdjustmentFromProto(adjustment *luckygiftv1.LuckyGiftPoolAdjustment) poolAdjustmentDTO {
if adjustment == nil {
return poolAdjustmentDTO{}
}
return poolAdjustmentDTO{
AdjustmentID: adjustment.GetAdjustmentId(),
AppCode: adjustment.GetAppCode(),
PoolID: adjustment.GetPoolId(),
StrategyVersion: adjustment.GetStrategyVersion(),
Direction: adjustment.GetDirection(),
AmountCoins: adjustment.GetAmountCoins(),
Reason: adjustment.GetReason(),
OperatorAdminID: adjustment.GetOperatorAdminId(),
BalanceBefore: adjustment.GetBalanceBefore(),
BalanceAfter: adjustment.GetBalanceAfter(),
CreatedAtMS: adjustment.GetCreatedAtMs(),
}
}

View File

@ -1,11 +1,35 @@
package luckygift
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"hyapp-admin-server/internal/integration/luckygiftadmin"
"hyapp-admin-server/internal/middleware"
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type poolAdjustmentClientStub struct {
luckygiftadmin.Client
request *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest
response *luckygiftv1.AdjustLuckyGiftPoolBalanceResponse
err error
}
func (s *poolAdjustmentClientStub) AdjustLuckyGiftPoolBalance(_ context.Context, request *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest) (*luckygiftv1.AdjustLuckyGiftPoolBalanceResponse, error) {
s.request = request
return s.response, s.err
}
func TestConfigToProtoConvertsBusinessUnitsToPPM(t *testing.T) {
config := configToProto(configRequest{
PoolID: " lucky_100 ",
@ -62,6 +86,63 @@ func TestConfigToProtoConvertsBusinessUnitsToPPM(t *testing.T) {
}
}
func TestCreditLuckyGiftPoolBalanceForwardsAuditedOwnerCommand(t *testing.T) {
gin.SetMode(gin.TestMode)
client := &poolAdjustmentClientStub{response: &luckygiftv1.AdjustLuckyGiftPoolBalanceResponse{
Adjustment: &luckygiftv1.LuckyGiftPoolAdjustment{
AdjustmentId: "adjustment-1", AppCode: "aslan", PoolId: "lucky", StrategyVersion: "dynamic_v3",
Direction: "in", AmountCoins: 100, Reason: "launch budget", OperatorAdminId: 7, BalanceBefore: 0, BalanceAfter: 100,
},
Pool: &luckygiftv1.LuckyGiftPoolBalance{
AppCode: "aslan", PoolId: "lucky", StrategyVersion: "dynamic_v3", Balance: 100,
ManualCreditTotal: 100, Materialized: true,
},
}}
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/admin/ops-center/lucky-gifts/pools/credit?app_code=aslan&pool_id=lucky&strategy_version=dynamic_v3", bytes.NewBufferString(`{"adjustment_id":"adjustment-1","amount_coins":100,"reason":"launch budget"}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Set(middleware.ContextUserID, uint(7))
c.Set(middleware.ContextRequestID, "request-1")
New(client, time.Second, nil).CreditLuckyGiftPoolBalance(c)
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
if client.request == nil || client.request.GetDirection() != "in" || client.request.GetOperatorAdminId() != 7 ||
client.request.GetMeta().GetAppCode() != "aslan" || client.request.GetMeta().GetRequestId() != "request-1" {
t.Fatalf("owner request=%+v", client.request)
}
var envelope struct {
Code int `json:"code"`
Data struct {
Pool struct {
StrategyVersion string `json:"strategy_version"`
ManualCreditTotal int64 `json:"manual_credit_total"`
} `json:"pool"`
} `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode response: %v", err)
}
if envelope.Code != 0 || envelope.Data.Pool.StrategyVersion != "dynamic_v3" || envelope.Data.Pool.ManualCreditTotal != 100 {
t.Fatalf("response=%s", recorder.Body.String())
}
}
func TestDebitLuckyGiftPoolBalanceMapsOwnerConflictToHTTP409(t *testing.T) {
gin.SetMode(gin.TestMode)
client := &poolAdjustmentClientStub{err: status.Error(codes.FailedPrecondition, "debit would cross reserve floor")}
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/admin/ops-center/lucky-gifts/pools/debit?app_code=aslan&pool_id=lucky&strategy_version=dynamic_v3", bytes.NewBufferString(`{"adjustment_id":"adjustment-2","amount_coins":101,"reason":"return budget"}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Set(middleware.ContextUserID, uint(7))
New(client, time.Second, nil).DebitLuckyGiftPoolBalance(c)
if recorder.Code != http.StatusConflict {
t.Fatalf("status=%d want=409 body=%s", recorder.Code, recorder.Body.String())
}
}
func TestConfigToProtoGeneratesUniqueTierIDs(t *testing.T) {
config := configToProto(configRequest{
PoolID: "lucky",
@ -182,6 +263,9 @@ func TestDynamicConfigAdminMappingRoundTrip(t *testing.T) {
if protoConfig.GetStrategyVersion() != "dynamic_v3" || protoConfig.GetProfitRatePpm() != 10_000 || protoConfig.GetAnchorRatePpm() != 10_000 {
t.Fatalf("strategy/fund split proto mismatch: %+v", protoConfig)
}
if protoConfig.GetInitialPoolCoins() != 0 {
t.Fatalf("dynamic_v3 legacy initial pool was forwarded: %d", protoConfig.GetInitialPoolCoins())
}
if protoConfig.GetLowWaterNonzeroFactorPpm() != 700_000 || protoConfig.GetHighWaterNonzeroFactorPpm() != 1_300_000 || protoConfig.GetRechargeBoostFactorPpm() != 1_100_000 {
t.Fatalf("dynamic factors lost precision in admin mapping: %+v", protoConfig)
}
@ -197,6 +281,9 @@ func TestDynamicConfigAdminMappingRoundTrip(t *testing.T) {
if dto.StrategyVersion != "dynamic_v3" || dto.ProfitRatePercent != 1 || dto.AnchorRatePercent != 1 {
t.Fatalf("strategy/fund split DTO mismatch: %+v", dto)
}
if dto.InitialPoolCoins != 0 {
t.Fatalf("dynamic_v3 DTO exposed legacy initial pool: %d", dto.InitialPoolCoins)
}
if dto.LowWaterNonzeroFactorPercent != 70 || dto.HighWaterNonzeroFactorPercent != 130 || dto.RechargeBoostFactorPercent != 110 {
t.Fatalf("dynamic factor DTO mismatch: %+v", dto)
}

View File

@ -25,4 +25,6 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
group.GET("/lucky-gifts/draws", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftDraws)
group.GET("/lucky-gifts/summary", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.GetLuckyGiftDrawSummary)
group.GET("/lucky-gifts/pools", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftPoolBalances)
group.POST("/lucky-gifts/pools/credit", middleware.RequirePermission("lucky-gift:pool-credit"), h.luckyGift.CreditLuckyGiftPoolBalance)
group.POST("/lucky-gifts/pools/debit", middleware.RequirePermission("lucky-gift:pool-debit"), h.luckyGift.DebitLuckyGiftPoolBalance)
}

View File

@ -0,0 +1,87 @@
package opscenter
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"hyapp-admin-server/internal/integration/luckygiftadmin"
"hyapp-admin-server/internal/middleware"
luckygiftmodule "hyapp-admin-server/internal/modules/luckygift"
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
"github.com/gin-gonic/gin"
)
type poolPermissionClient struct {
luckygiftadmin.Client
}
func (poolPermissionClient) AdjustLuckyGiftPoolBalance(_ context.Context, req *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest) (*luckygiftv1.AdjustLuckyGiftPoolBalanceResponse, error) {
return &luckygiftv1.AdjustLuckyGiftPoolBalanceResponse{
Adjustment: &luckygiftv1.LuckyGiftPoolAdjustment{
AdjustmentId: req.GetAdjustmentId(), AppCode: req.GetMeta().GetAppCode(), PoolId: req.GetPoolId(),
StrategyVersion: req.GetStrategyVersion(), Direction: req.GetDirection(), AmountCoins: req.GetAmountCoins(),
},
Pool: &luckygiftv1.LuckyGiftPoolBalance{
AppCode: req.GetMeta().GetAppCode(), PoolId: req.GetPoolId(), StrategyVersion: req.GetStrategyVersion(), Materialized: true,
},
}, nil
}
func TestRegisterRoutesExposesStrategyPoolAdjustmentEndpoints(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
RegisterRoutes(engine.Group("/api/v1"), New(nil, luckygiftmodule.New(poolPermissionClient{}, 0, nil)))
routes := make(map[string]struct{})
for _, route := range engine.Routes() {
routes[route.Method+" "+route.Path] = struct{}{}
}
for _, expected := range []string{
"POST /api/v1/admin/ops-center/lucky-gifts/pools/credit",
"POST /api/v1/admin/ops-center/lucky-gifts/pools/debit",
} {
if _, ok := routes[expected]; !ok {
t.Fatalf("missing route %s; actual=%v", expected, routes)
}
}
}
func TestPoolAdjustmentRoutesRequireDirectionSpecificPermission(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
path string
permission string
wantStatus int
}{
{name: "credit allowed", path: "credit", permission: "lucky-gift:pool-credit", wantStatus: http.StatusOK},
{name: "credit rejects debit permission", path: "credit", permission: "lucky-gift:pool-debit", wantStatus: http.StatusForbidden},
{name: "debit allowed", path: "debit", permission: "lucky-gift:pool-debit", wantStatus: http.StatusOK},
{name: "debit rejects view permission", path: "debit", permission: "lucky-gift:view", wantStatus: http.StatusForbidden},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
engine := gin.New()
engine.Use(func(c *gin.Context) {
c.Set(middleware.ContextPermissions, []string{tt.permission})
c.Set(middleware.ContextUserID, uint(7))
c.Next()
})
RegisterRoutes(engine.Group("/api/v1"), New(nil, luckygiftmodule.New(poolPermissionClient{}, 0, nil)))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(
http.MethodPost,
"/api/v1/admin/ops-center/lucky-gifts/pools/"+tt.path+"?app_code=aslan&pool_id=lucky&strategy_version=dynamic_v3",
strings.NewReader(`{"adjustment_id":"route-adjustment","amount_coins":1,"reason":"permission test"}`),
)
request.Header.Set("Content-Type", "application/json")
engine.ServeHTTP(recorder, request)
if recorder.Code != tt.wantStatus {
t.Fatalf("status=%d want=%d body=%s", recorder.Code, tt.wantStatus, recorder.Body.String())
}
})
}
}

View File

@ -129,7 +129,7 @@ var (
"daily-task:view", "first-recharge-reward:view", "cumulative-recharge-reward:view",
"invite-activity-reward:view", "seven-day-checkin:view", "room-rocket:view", "room-turnover-reward:view",
"wheel:view", "weekly-star:view", "agency-opening:view", "user-leaderboard:view", "red-packet:view",
"cp-config:view", "cp-weekly-rank:view", "vip-config:view", "achievement:view",
"cp-config:view", "cp-weekly-rank:view", "vip-config:view", "achievement:view", "lucky-gift:view",
}
activityManage = []string{
"activity-template:create", "activity-template:update", "activity-template:publish", "activity-template:delete",
@ -140,8 +140,10 @@ var (
"wheel:update", "weekly-star:create", "weekly-star:update", "weekly-star:settle",
"agency-opening:create", "agency-opening:update", "red-packet:update",
"cp-config:update", "cp-weekly-rank:update", "vip-config:update",
"achievement:create", "achievement:update",
"achievement:create", "achievement:update", "lucky-gift:update",
}
// 奖池人工增减属于资金高风险动作只授予运营负责人platform-admin 仍由全权限同步自动获得。
luckyGiftPoolManage = []string{"lucky-gift:pool-credit", "lucky-gift:pool-debit"}
gameRead = []string{
"game-catalog:view", "self-game:view", "game-robot:view", "room-rps-config:view", "room-rps-order:view",
@ -172,7 +174,7 @@ func defaultRolePermissionCodes(code string) []string {
roomRead, roomManage,
[]string{"app-config:view"}, appConfigManage,
resourceRead, resourceManage,
operationsRead, operationsLeadManage,
operationsRead, operationsLeadManage, luckyGiftPoolManage,
paymentRead, paymentBillExport, paymentBillRefresh, paymentOperationsManage,
activityRead, activityManage,
gameRead, gameManage,

View File

@ -197,6 +197,8 @@ var defaultPermissions = []model.Permission{
{Name: "七日签到更新", Code: "seven-day-checkin:update", Kind: "button"},
{Name: "幸运礼物查看", Code: "lucky-gift:view", Kind: "menu"},
{Name: "幸运礼物更新", Code: "lucky-gift:update", Kind: "button"},
{Name: "幸运礼物奖池注资", Code: "lucky-gift:pool-credit", Kind: "button"},
{Name: "幸运礼物奖池扣资", Code: "lucky-gift:pool-debit", Kind: "button"},
{Name: "转盘抽奖查看", Code: "wheel:view", Kind: "menu"},
{Name: "转盘抽奖更新", Code: "wheel:update", Kind: "button"},
{Name: "房间火箭查看", Code: "room-rocket:view", Kind: "menu"},

View File

@ -153,6 +153,21 @@ func TestManagedDefaultRoleModuleBoundaries(t *testing.T) {
)
}
func TestLuckyGiftPermissionsStayInManagedRoleSync(t *testing.T) {
assertRolePermissions(t, roleCodeOperationsLead,
[]string{"lucky-gift:view", "lucky-gift:update", "lucky-gift:pool-credit", "lucky-gift:pool-debit"}, nil,
)
assertRolePermissions(t, roleCodeOperationsSpecialist,
[]string{"lucky-gift:view"}, []string{"lucky-gift:update", "lucky-gift:pool-credit", "lucky-gift:pool-debit"},
)
assertRolePermissions(t, roleCodeProductLead,
[]string{"lucky-gift:view", "lucky-gift:update"}, []string{"lucky-gift:pool-credit", "lucky-gift:pool-debit"},
)
assertRolePermissions(t, roleCodeProductSpecialist,
[]string{"lucky-gift:view"}, []string{"lucky-gift:update", "lucky-gift:pool-credit", "lucky-gift:pool-debit"},
)
}
func TestRolePermissionMigrationMatchesManagedMatrix(t *testing.T) {
content, err := os.ReadFile("../../migrations/093_role_permission_matrix.sql")
if err != nil {
@ -221,6 +236,22 @@ func TestActivityTemplatePermissionMigrationExtendsManagedRoles(t *testing.T) {
}
}
func TestLuckyGiftPoolAdjustmentPermissionMigrationRepairsManagedRoles(t *testing.T) {
content, err := os.ReadFile("../../migrations/098_lucky_gift_pool_adjust_permissions.sql")
if err != nil {
t.Fatalf("read lucky gift pool permission migration: %v", err)
}
sqlText := string(content)
for _, token := range []string{
"'lucky-gift:view'", "'lucky-gift:update'", "'lucky-gift:pool-credit'", "'lucky-gift:pool-debit'",
"'platform-admin', 'ops-admin'", "'operations-specialist'", "'product-lead'", "'product-specialist'",
} {
if !strings.Contains(sqlText, token) {
t.Fatalf("lucky gift pool permission migration missing %s", token)
}
}
}
func assertMigrationPermissionCodes(t *testing.T, roleCode string, sqlList string) {
t.Helper()
quotedCode := regexp.MustCompile(`'([a-z0-9:-]+)'`)
@ -236,6 +267,7 @@ func assertMigrationPermissionCodes(t *testing.T, roleCode string, sqlList strin
"gift-record:view",
"activity-template:view", "activity-template:create", "activity-template:update", "activity-template:publish",
"activity-template:delete", "activity-template:data", "activity-template:export", "activity-template:retry",
"lucky-gift:view", "lucky-gift:update", "lucky-gift:pool-credit", "lucky-gift:pool-debit",
})
sort.Strings(actual)
sort.Strings(expected)

View File

@ -0,0 +1,34 @@
-- 奖池人工增减是资金高风险动作,使用独立按钮权限;迁移仅按唯一 code 连接小型 RBAC 元数据表,不扫描业务流水。
SET @now_ms = CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED);
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
('幸运礼物奖池注资', 'lucky-gift:pool-credit', 'button', '允许对指定策略奖池执行带幂等审计的人工注资', @now_ms, @now_ms),
('幸运礼物奖池扣资', 'lucky-gift:pool-debit', 'button', '允许在安全水位之上执行带幂等审计的人工扣资', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
kind = VALUES(kind),
description = VALUES(description),
updated_at_ms = @now_ms;
-- 093 的岗位同步矩阵曾漏掉既有 lucky-gift:view/update增量迁移按当前岗位边界补齐避免下次同步再次丢失。
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT role.id, permission.id
FROM admin_roles role
JOIN admin_permissions permission
WHERE role.code IN ('platform-admin', 'ops-admin', 'operations-specialist', 'product-lead', 'product-specialist')
AND permission.code = 'lucky-gift:view';
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT role.id, permission.id
FROM admin_roles role
JOIN admin_permissions permission
WHERE role.code IN ('platform-admin', 'ops-admin', 'product-lead')
AND permission.code = 'lucky-gift:update';
-- 实际资金操作只授予超级管理员和运营负责人;查看/规则编辑权限不能隐式获得调账能力。
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
SELECT role.id, permission.id
FROM admin_roles role
JOIN admin_permissions permission
WHERE role.code IN ('platform-admin', 'ops-admin')
AND permission.code IN ('lucky-gift:pool-credit', 'lucky-gift:pool-debit');

View File

@ -159,6 +159,8 @@ CREATE TABLE IF NOT EXISTS lucky_pools (
reserve_floor BIGINT NOT NULL DEFAULT 0 COMMENT '安全水位',
total_in BIGINT NOT NULL DEFAULT 0 COMMENT '累计入池',
total_out BIGINT NOT NULL DEFAULT 0 COMMENT '累计基础返奖支出',
manual_credit_total BIGINT NOT NULL DEFAULT 0 COMMENT '累计人工注资金币,不计入业务 total_in',
manual_debit_total BIGINT NOT NULL DEFAULT 0 COMMENT '累计人工扣资金币,不计入业务 total_out',
profit_total BIGINT NOT NULL DEFAULT 0 COMMENT 'dynamic_v3 累计平台盈利拆账金币',
anchor_income_total BIGINT NOT NULL DEFAULT 0 COMMENT 'dynamic_v3 累计主播收益拆账金币',
initial_seed_total BIGINT NOT NULL DEFAULT 0 COMMENT 'dynamic_v3 累计初始注资金币,只在首次物化时增加',
@ -167,6 +169,28 @@ CREATE TABLE IF NOT EXISTS lucky_pools (
PRIMARY KEY (app_code, scope_type, scope_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物单一基础奖池';
CREATE TABLE IF NOT EXISTS lucky_pool_adjustments (
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
adjustment_id VARCHAR(128) NOT NULL COMMENT '调用方资金幂等 ID',
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
strategy_version VARCHAR(32) NOT NULL COMMENT 'fixed_v2/dynamic_v3',
scope_type VARCHAR(32) NOT NULL COMMENT '实际资金账本 scope_type',
direction VARCHAR(16) NOT NULL COMMENT 'in/out',
amount_coins BIGINT NOT NULL COMMENT '本次人工调整金币',
reason VARCHAR(512) NOT NULL COMMENT '管理员填写的调整原因',
operator_admin_id BIGINT NOT NULL COMMENT '操作管理员 ID',
request_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '链路 request_id不作为幂等键',
insert_token VARCHAR(64) NOT NULL COMMENT '首次插入事务随机令牌,用于并发幂等判定',
balance_before BIGINT NOT NULL DEFAULT 0 COMMENT '首次应用前余额',
balance_after BIGINT NOT NULL DEFAULT 0 COMMENT '首次应用后余额',
pool_snapshot_json JSON NULL COMMENT '首次应用后的完整奖池快照,供幂等重放',
status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/applied',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, adjustment_id),
KEY idx_lucky_pool_adjustments_pool (app_code, pool_id, strategy_version, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物人工奖池调整事实';
CREATE TABLE IF NOT EXISTS lucky_risk_counters (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码',
scope_type VARCHAR(32) NOT NULL COMMENT 'user/device/room/anchor',

View File

@ -0,0 +1,61 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
USE hyapp_lucky_gift;
-- 每列独立探测使迁移在“全新库、004 后旧库、曾执行一半的旧库”上都可重复运行。
-- 人工资金必须与正常送礼入池/返奖分账,不能污染 total_in/total_out 的 RTP 经营口径。
SET @ddl := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lucky_pools' AND COLUMN_NAME = 'manual_credit_total') = 0,
'ALTER TABLE lucky_pools ADD COLUMN manual_credit_total BIGINT NOT NULL DEFAULT 0 COMMENT ''累计人工注资金币,不计入业务 total_in'' AFTER total_out', 'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lucky_pools' AND COLUMN_NAME = 'manual_debit_total') = 0,
'ALTER TABLE lucky_pools ADD COLUMN manual_debit_total BIGINT NOT NULL DEFAULT 0 COMMENT ''累计人工扣资金币,不计入业务 total_out'' AFTER manual_credit_total', 'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS lucky_pool_adjustments (
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
adjustment_id VARCHAR(128) NOT NULL COMMENT '调用方资金幂等 ID',
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
strategy_version VARCHAR(32) NOT NULL COMMENT 'fixed_v2/dynamic_v3',
scope_type VARCHAR(32) NOT NULL COMMENT '实际资金账本 scope_type',
direction VARCHAR(16) NOT NULL COMMENT 'in/out',
amount_coins BIGINT NOT NULL COMMENT '本次人工调整金币',
reason VARCHAR(512) NOT NULL COMMENT '管理员填写的调整原因',
operator_admin_id BIGINT NOT NULL COMMENT '操作管理员 ID',
request_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '链路 request_id不作为幂等键',
insert_token VARCHAR(64) NOT NULL COMMENT '首次插入事务随机令牌,用于并发幂等判定',
balance_before BIGINT NOT NULL DEFAULT 0 COMMENT '首次应用前余额',
balance_after BIGINT NOT NULL DEFAULT 0 COMMENT '首次应用后余额',
pool_snapshot_json JSON NULL COMMENT '首次应用后的完整奖池快照,供幂等重放',
status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/applied',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, adjustment_id),
KEY idx_lucky_pool_adjustments_pool (app_code, pool_id, strategy_version, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物人工奖池调整事实';
SET @ddl := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lucky_pool_adjustments' AND COLUMN_NAME = 'insert_token') = 0,
'ALTER TABLE lucky_pool_adjustments ADD COLUMN insert_token VARCHAR(64) NOT NULL DEFAULT '''' COMMENT ''首次插入事务随机令牌,用于并发幂等判定'' AFTER request_id', 'SELECT 1');
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 只看每个 app+pool 当前最新不可变版本。若它已经切换到 dynamic_v3就创建一口独立的零余额账本
-- ON DUPLICATE 分支绝不更新余额、累计或时间,迁移重跑以及后续 V3 版本都不会重置真实资金。
INSERT INTO lucky_pools (
app_code, scope_type, scope_id, balance, reserve_floor, total_in, total_out,
manual_credit_total, manual_debit_total, profit_total, anchor_income_total, initial_seed_total,
created_at_ms, updated_at_ms
)
SELECT
current_rule.app_code, 'pool_dynamic_v3', current_rule.pool_id, 0, 0, 0, 0,
0, 0, 0, 0, 0, current_rule.created_at_ms, current_rule.created_at_ms
FROM lucky_gift_rule_versions current_rule
INNER JOIN (
SELECT app_code, pool_id, MAX(rule_version) AS rule_version
FROM lucky_gift_rule_versions
GROUP BY app_code, pool_id
) latest
ON latest.app_code = current_rule.app_code
AND latest.pool_id = current_rule.pool_id
AND latest.rule_version = current_rule.rule_version
WHERE current_rule.strategy_version = 'dynamic_v3'
ON DUPLICATE KEY UPDATE scope_id = lucky_pools.scope_id;

View File

@ -23,6 +23,9 @@ const (
StrategyFixedV2 = "fixed_v2"
StrategyDynamicV3 = "dynamic_v3"
PoolAdjustmentIn = "in"
PoolAdjustmentOut = "out"
EventTypeLuckyGiftDrawn = "LuckyGiftDrawn"
EventTypeLuckyGiftRewardSettlement = "LuckyGiftRewardSettlement"
EventTypeExternalLuckyGiftDrawn = "ExternalLuckyGiftDrawn"
@ -251,20 +254,58 @@ type DrawSummary struct {
// PoolBalance 是后台看到的 app+pool_id 单池账本快照。
// materialized=false 表示该池还没有真实 lucky_pools 行,展示的是规则默认初始水位,不能当成已经入库的资金流水。
type PoolBalance struct {
AppCode string
PoolID string
Balance int64
ReserveFloor int64
AvailableBalance int64
TotalIn int64
TotalOut int64
Materialized bool
UpdatedAtMS int64
AppCode string
PoolID string
StrategyVersion string
Balance int64
ReserveFloor int64
AvailableBalance int64
TotalIn int64
TotalOut int64
ManualCreditTotal int64
ManualDebitTotal int64
Materialized bool
UpdatedAtMS int64
}
type PoolBalanceQuery struct {
AppCode string
PoolID string
AppCode string
PoolID string
StrategyVersion string
}
// PoolAdjustmentCommand 是人工注资/扣资的 owner 命令。AdjustmentID 由调用方生成并在 App 内唯一,
// 同一个 ID 的重试只能复用完全相同的资金参数,避免网络重试演变成重复记账。
type PoolAdjustmentCommand struct {
AppCode string
PoolID string
StrategyVersion string
AdjustmentID string
Direction string
AmountCoins int64
Reason string
OperatorAdminID int64
RequestID string
}
type PoolAdjustment struct {
AdjustmentID string
AppCode string
PoolID string
StrategyVersion string
Direction string
AmountCoins int64
Reason string
OperatorAdminID int64
BalanceBefore int64
BalanceAfter int64
CreatedAtMS int64
}
type PoolAdjustmentResult struct {
Adjustment PoolAdjustment
Pool PoolBalance
IdempotentReplay bool
}
type AdminConfigQuery struct {

View File

@ -281,7 +281,7 @@ func DefaultLuckyGiftStrategyConfig() StrategyConfig {
{ID: "1000x", MultiplierPPM: 1_000_000_000, BaseWeightPPM: 0, Jackpot: true, JackpotWeight: 1, Enabled: true},
},
PublicPoolRatePPM: 980_000, ProfitPoolRatePPM: 10_000, AnchorReturnRatePPM: 10_000,
ColdStartPoolCoins: 1_000_000,
ColdStartPoolCoins: 0,
LowWaterThresholdCoins: 10_000_000, HighWaterThresholdCoins: 20_000_000,
LowWaterFactorPPM: 700_000, HighWaterFactorPPM: 1_300_000,
RechargeFactorPPM: 1_100_000, RechargeBoostStartMS: 0, RechargeBoostEndMS: 5 * 60 * 1000,

View File

@ -18,18 +18,19 @@ const (
func DefaultRuleConfig(appCode, poolID string) domain.RuleConfig {
poolID = normalizePoolID(poolID)
return domain.RuleConfig{
AppCode: appCode,
PoolID: poolID,
StrategyVersion: domain.StrategyDynamicV3,
Enabled: false,
TargetRTPPPM: 980_000,
PoolRatePPM: 980_000,
ProfitRatePPM: 10_000,
AnchorRatePPM: 10_000,
SettlementWindowWager: 1_000_000,
ControlBandPPM: 30_000,
GiftPriceReference: 100,
InitialPoolCoins: 1_000_000,
AppCode: appCode,
PoolID: poolID,
StrategyVersion: domain.StrategyDynamicV3,
Enabled: false,
TargetRTPPPM: 980_000,
PoolRatePPM: 980_000,
ProfitRatePPM: 10_000,
AnchorRatePPM: 10_000,
SettlementWindowWager: 1_000_000,
ControlBandPPM: 30_000,
GiftPriceReference: 100,
// dynamic_v3 的账本从 0 开始,任何启动资金都必须经过带管理员和原因的人工注资事实。
InitialPoolCoins: 0,
LossStreakGuarantee: 5,
LowWatermarkCoins: 10_000_000,
LowWaterNonzeroFactorPPM: 700_000,
@ -177,8 +178,8 @@ func validateDynamicRuleConfig(config domain.RuleConfig, stages map[string]domai
if config.ProfitRatePPM < 0 || config.AnchorRatePPM < 0 || config.PoolRatePPM+config.ProfitRatePPM+config.AnchorRatePPM != ppmScale {
return xerr.New(xerr.InvalidArgument, "dynamic_v3 pool, profit and anchor rates must be non-negative and sum to 100%")
}
if config.InitialPoolCoins <= 0 {
return xerr.New(xerr.InvalidArgument, "dynamic_v3 initial pool coins must be positive")
if config.InitialPoolCoins != 0 {
return xerr.New(xerr.InvalidArgument, "dynamic_v3 initial pool coins must be zero; fund the pool through an audited credit adjustment")
}
if config.LossStreakGuarantee <= 0 {
return xerr.New(xerr.InvalidArgument, "dynamic_v3 loss streak guarantee must be positive")

View File

@ -15,7 +15,7 @@ func TestDefaultRuleConfigUsesSafeDynamicV3Draft(t *testing.T) {
if config.TargetRTPPPM != 980_000 || config.PoolRatePPM != 980_000 || config.ProfitRatePPM != 10_000 || config.AnchorRatePPM != 10_000 {
t.Fatalf("default RTP/fund split mismatch: %+v", config)
}
if config.ControlBandPPM != 30_000 || config.InitialPoolCoins != 1_000_000 || config.LossStreakGuarantee != 5 {
if config.ControlBandPPM != 30_000 || config.InitialPoolCoins != 0 || config.LossStreakGuarantee != 5 {
t.Fatalf("default control values mismatch: %+v", config)
}
if config.LowWatermarkCoins != 10_000_000 || config.LowWaterNonzeroFactorPPM != 700_000 ||
@ -53,6 +53,14 @@ func TestDefaultRuleConfigUsesSafeDynamicV3Draft(t *testing.T) {
}
}
func TestValidateDynamicV3RejectsImplicitInitialFunding(t *testing.T) {
config := DefaultRuleConfig("lalu", "lucky")
config.InitialPoolCoins = 1_000_000
if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), "audited credit adjustment") {
t.Fatalf("implicit V3 seed error=%v, want audited credit guidance", err)
}
}
func TestValidateDynamicV3RequiresExplicitMonetaryRiskLimitsWhenEnabled(t *testing.T) {
config := DefaultRuleConfig("lalu", "lucky")
config.Enabled = true

View File

@ -0,0 +1,71 @@
package luckygift
import (
"context"
"testing"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
)
type poolAdjustmentRepositoryStub struct {
Repository
command domain.PoolAdjustmentCommand
nowMS int64
result domain.PoolAdjustmentResult
err error
calls int
}
func (s *poolAdjustmentRepositoryStub) AdjustLuckyGiftPoolBalance(_ context.Context, command domain.PoolAdjustmentCommand, nowMS int64) (domain.PoolAdjustmentResult, error) {
s.calls++
s.command = command
s.nowMS = nowMS
return s.result, s.err
}
func TestAdjustPoolBalanceNormalizesAndForwardsOwnerCommand(t *testing.T) {
stub := &poolAdjustmentRepositoryStub{result: domain.PoolAdjustmentResult{Pool: domain.PoolBalance{Balance: 50}}}
service := New(stub)
fixedNow := time.Date(2026, 7, 15, 1, 2, 3, 0, time.UTC)
service.SetClock(func() time.Time { return fixedNow })
ctx := appcode.WithContext(context.Background(), " AsLaN ")
result, err := service.AdjustPoolBalance(ctx, domain.PoolAdjustmentCommand{
PoolID: " lucky ", StrategyVersion: " DYNAMIC_V3 ", AdjustmentID: " adjustment-1 ",
Direction: " IN ", AmountCoins: 50, Reason: " approved budget ", OperatorAdminID: 9,
RequestID: " request-1 ",
})
if err != nil {
t.Fatalf("adjust pool balance: %v", err)
}
if result.Pool.Balance != 50 || stub.calls != 1 {
t.Fatalf("result=%+v calls=%d", result, stub.calls)
}
if stub.command.AppCode != "aslan" || stub.command.PoolID != "lucky" || stub.command.StrategyVersion != domain.StrategyDynamicV3 ||
stub.command.Direction != domain.PoolAdjustmentIn || stub.command.Reason != "approved budget" || stub.nowMS != fixedNow.UnixMilli() {
t.Fatalf("normalized command=%+v now=%d", stub.command, stub.nowMS)
}
}
func TestAdjustPoolBalanceRejectsInvalidMoneyCommandBeforeRepository(t *testing.T) {
stub := &poolAdjustmentRepositoryStub{}
service := New(stub)
tests := []domain.PoolAdjustmentCommand{
{StrategyVersion: domain.StrategyDynamicV3, AdjustmentID: "a", Direction: "in", AmountCoins: 1, Reason: "r", OperatorAdminID: 1},
{PoolID: "lucky", StrategyVersion: "v4", AdjustmentID: "a", Direction: "in", AmountCoins: 1, Reason: "r", OperatorAdminID: 1},
{PoolID: "lucky", StrategyVersion: domain.StrategyDynamicV3, Direction: "in", AmountCoins: 1, Reason: "r", OperatorAdminID: 1},
{PoolID: "lucky", StrategyVersion: domain.StrategyDynamicV3, AdjustmentID: "a", Direction: "sideways", AmountCoins: 1, Reason: "r", OperatorAdminID: 1},
{PoolID: "lucky", StrategyVersion: domain.StrategyDynamicV3, AdjustmentID: "a", Direction: "out", AmountCoins: 0, Reason: "r", OperatorAdminID: 1},
{PoolID: "lucky", StrategyVersion: domain.StrategyDynamicV3, AdjustmentID: "a", Direction: "out", AmountCoins: 1, OperatorAdminID: 1},
}
for index, command := range tests {
if _, err := service.AdjustPoolBalance(context.Background(), command); !xerr.IsCode(err, xerr.InvalidArgument) {
t.Fatalf("case %d error=%v, want invalid argument", index, err)
}
}
if stub.calls != 0 {
t.Fatalf("repository calls=%d, want 0 for invalid commands", stub.calls)
}
}

View File

@ -10,6 +10,7 @@ import (
"strings"
"sync"
"time"
"unicode/utf8"
"hyapp/pkg/appcode"
"hyapp/pkg/giftlimits"
@ -37,6 +38,7 @@ type Repository interface {
ListLuckyGiftDraws(ctx context.Context, query domain.DrawQuery) ([]domain.DrawResult, int64, error)
GetLuckyGiftDrawSummary(ctx context.Context, query domain.DrawQuery) (domain.DrawSummary, error)
ListLuckyGiftPoolBalances(ctx context.Context, query domain.PoolBalanceQuery) ([]domain.PoolBalance, error)
AdjustLuckyGiftPoolBalance(ctx context.Context, command domain.PoolAdjustmentCommand, nowMS int64) (domain.PoolAdjustmentResult, error)
GetLuckyGiftDrawRewardState(ctx context.Context, appCode string, drawIDs []string) (domain.DrawRewardState, error)
ClaimPendingLuckyGiftOutbox(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int) ([]domain.DrawOutbox, error)
DeleteDeliveredLuckyGiftOutboxBefore(ctx context.Context, cutoffMS int64, limit int) (int64, error)
@ -780,9 +782,57 @@ func (s *Service) ListPoolBalances(ctx context.Context, query domain.PoolBalance
if strings.TrimSpace(query.PoolID) != "" {
query.PoolID = normalizePoolID(query.PoolID)
}
query.StrategyVersion = strings.ToLower(strings.TrimSpace(query.StrategyVersion))
if query.StrategyVersion != "" && query.StrategyVersion != domain.StrategyFixedV2 && query.StrategyVersion != domain.StrategyDynamicV3 {
return nil, xerr.New(xerr.InvalidArgument, "unsupported lucky gift strategy version")
}
return s.repository.ListLuckyGiftPoolBalances(ctx, query)
}
// AdjustPoolBalance 校验管理员资金命令并交给 owner 仓储执行。request_id 只记录链路,
// adjustment_id 才是可跨超时重试的业务幂等键。
func (s *Service) AdjustPoolBalance(ctx context.Context, command domain.PoolAdjustmentCommand) (domain.PoolAdjustmentResult, error) {
if err := s.requireRepository(); err != nil {
return domain.PoolAdjustmentResult{}, err
}
command.AppCode = appcode.FromContext(ctx)
rawPoolID := strings.TrimSpace(command.PoolID)
if rawPoolID == "" {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment pool_id is required")
}
command.PoolID = normalizePoolID(rawPoolID)
command.StrategyVersion = strings.ToLower(strings.TrimSpace(command.StrategyVersion))
command.AdjustmentID = strings.TrimSpace(command.AdjustmentID)
command.Direction = strings.ToLower(strings.TrimSpace(command.Direction))
command.Reason = strings.TrimSpace(command.Reason)
command.RequestID = strings.TrimSpace(command.RequestID)
if utf8.RuneCountInString(command.AppCode) > 32 || utf8.RuneCountInString(command.PoolID) > 96 {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment app_code or pool_id is too long")
}
if command.StrategyVersion != domain.StrategyFixedV2 && command.StrategyVersion != domain.StrategyDynamicV3 {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.InvalidArgument, "lucky gift pool strategy_version must be fixed_v2 or dynamic_v3")
}
if command.AdjustmentID == "" || utf8.RuneCountInString(command.AdjustmentID) > 128 {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment_id is required and must not exceed 128 characters")
}
if command.Direction != domain.PoolAdjustmentIn && command.Direction != domain.PoolAdjustmentOut {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment direction must be in or out")
}
if command.AmountCoins <= 0 {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment amount_coins must be positive")
}
if command.Reason == "" || utf8.RuneCountInString(command.Reason) > 512 {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment reason is required and must not exceed 512 characters")
}
if command.OperatorAdminID <= 0 {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment operator_admin_id must be positive")
}
if utf8.RuneCountInString(command.RequestID) > 128 {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment request_id must not exceed 128 characters")
}
return s.repository.AdjustLuckyGiftPoolBalance(ctx, command, s.now().UTC().UnixMilli())
}
func (s *Service) requireRepository() error {
if s == nil || s.repository == nil {
return xerr.New(xerr.Unavailable, "lucky gift repository is not configured")

View File

@ -83,7 +83,8 @@ func (r *Repository) executeDynamicLuckyGiftDrawBatch(ctx context.Context, tx *s
if err != nil {
return nil, err
}
basePool, err := r.getOrCreateLuckyPool(ctx, tx, appCode, luckyBasePoolScopeType, luckyBasePoolScopeID(config), config.InitialPoolCoins, 0, nowMS)
// dynamic_v3 永远从独立的零余额账本启动;运营启动资金必须走人工 credit不能由规则版本发布隐式注资。
basePool, err := r.getOrCreateLuckyPool(ctx, tx, appCode, luckyDynamicPoolScopeType, luckyBasePoolScopeID(config), 0, 0, nowMS)
if err != nil {
return nil, err
}

View File

@ -32,6 +32,14 @@ func TestDynamicLuckyGiftMySQLBatchAndExternalPaths(t *testing.T) {
if _, err := repo.PublishLuckyGiftRuleConfig(ctx, internalRule, now-1_000); err != nil {
t.Fatalf("publish internal dynamic rule: %v", err)
}
// 同名 fixed_v2 旧账本模拟策略切换前历史水位dynamic draw 必须只碰 pool_dynamic_v3。
if _, err := schema.DB.Exec(`
INSERT INTO lucky_pools (
app_code, scope_type, scope_id, balance, reserve_floor, total_in, total_out,
profit_total, anchor_income_total, initial_seed_total, created_at_ms, updated_at_ms
) VALUES ('lalu', 'pool', ?, 4321, 321, 4000, 100, 0, 0, 4321, ?, ?)`, internalRule.PoolID, now-2_000, now-2_000); err != nil {
t.Fatalf("seed fixed strategy ledger: %v", err)
}
checked, err := repo.CheckLuckyGift(ctx, domain.CheckCommand{PoolID: internalRule.PoolID, GiftID: "super-lucky", UserID: 1001, RoomID: "room-1"})
if err != nil || checked.StrategyVersion != domain.StrategyDynamicV3 {
t.Fatalf("check did not expose immutable strategy version: result=%+v err=%v", checked, err)
@ -103,6 +111,13 @@ func TestDynamicLuckyGiftMySQLBatchAndExternalPaths(t *testing.T) {
t.Fatalf("internal command lock count=%d err=%v", internalLockCount, err)
}
assertDynamicMySQLBatchFacts(t, schema.DB, "lalu", internalRule.PoolID, command.UserID, command.CommandID, 99, 100, 98, 1, 1)
var fixedBalance, fixedTotalIn, fixedTotalOut int64
if err := schema.DB.QueryRow(`
SELECT balance, total_in, total_out FROM lucky_pools
WHERE app_code='lalu' AND scope_type='pool' AND scope_id=?`, internalRule.PoolID,
).Scan(&fixedBalance, &fixedTotalIn, &fixedTotalOut); err != nil || fixedBalance != 4321 || fixedTotalIn != 4000 || fixedTotalOut != 100 {
t.Fatalf("dynamic draw changed fixed ledger balance/in/out=%d/%d/%d err=%v", fixedBalance, fixedTotalIn, fixedTotalOut, err)
}
assertDynamicRTPWindowRollover(t, schema.DB, internalRule.PoolID)
assertExactRolling72HourBoundary(t, repo, schema.DB, ctx, internalRule.PoolID, now)
@ -301,8 +316,10 @@ func assertDynamicTokenSurvivesUTCDayBoundary(t *testing.T, repo *Repository, db
t.Fatalf("earn milestone token: %v", err)
}
assertPendingTokenCount(t, db, rule.PoolID, userID, 1)
if _, err := db.Exec(`UPDATE lucky_pools SET balance=0 WHERE app_code='lalu' AND scope_type='pool' AND scope_id=?`, rule.PoolID); err != nil {
if result, err := db.Exec(`UPDATE lucky_pools SET balance=0 WHERE app_code='lalu' AND scope_type=? AND scope_id=?`, luckyDynamicPoolScopeType, rule.PoolID); err != nil {
t.Fatalf("drain token test pool: %v", err)
} else if rows, _ := result.RowsAffected(); rows != 1 {
t.Fatalf("drain token test pool affected=%d, want 1", rows)
}
dayTwoMS := time.UnixMilli(dayOneMS).UTC().Add(24 * time.Hour).UnixMilli()
retain := earn
@ -314,8 +331,10 @@ func assertDynamicTokenSurvivesUTCDayBoundary(t *testing.T, repo *Repository, db
t.Fatalf("retain token across UTC day: %v", err)
}
assertPendingTokenCount(t, db, rule.PoolID, userID, 1)
if _, err := db.Exec(`UPDATE lucky_pools SET balance=1000000 WHERE app_code='lalu' AND scope_type='pool' AND scope_id=?`, rule.PoolID); err != nil {
if result, err := db.Exec(`UPDATE lucky_pools SET balance=1000000 WHERE app_code='lalu' AND scope_type=? AND scope_id=?`, luckyDynamicPoolScopeType, rule.PoolID); err != nil {
t.Fatalf("refill token test pool: %v", err)
} else if rows, _ := result.RowsAffected(); rows != 1 {
t.Fatalf("refill token test pool affected=%d, want 1", rows)
}
consume := retain
consume.CommandID = "token-consume-next-day"
@ -466,7 +485,7 @@ func dynamicMySQLTestRule(poolID string) domain.RuleConfig {
PoolID: poolID, StrategyVersion: domain.StrategyDynamicV3, Enabled: true,
TargetRTPPPM: 980_000, PoolRatePPM: 980_000, ProfitRatePPM: 10_000, AnchorRatePPM: 10_000,
SettlementWindowWager: 1_000_000, ControlBandPPM: 30_000, GiftPriceReference: 1,
InitialPoolCoins: 1_000_000, LossStreakGuarantee: 5,
InitialPoolCoins: 0, LossStreakGuarantee: 5,
LowWatermarkCoins: 10_000_000, LowWaterNonzeroFactorPPM: 700_000,
HighWatermarkCoins: 20_000_000, HighWaterNonzeroFactorPPM: 1_300_000,
RechargeBoostWindowMS: 300_000, RechargeBoostFactorPPM: 1_100_000,

View File

@ -272,7 +272,7 @@ func (r *Repository) updateLuckyDynamicPoolNet(ctx context.Context, tx *sql.Tx,
profit_total = profit_total + ?, anchor_income_total = anchor_income_total + ?, updated_at_ms = ?
WHERE app_code = ? AND scope_type = ? AND scope_id = ?`,
publicIn, payout, publicIn, payout, profitIn, anchorIncome, nowMS,
appCode, luckyBasePoolScopeType, scopeID,
appCode, luckyDynamicPoolScopeType, scopeID,
)
return err
}

View File

@ -22,6 +22,9 @@ import (
const (
luckyPPMScale int64 = 1_000_000
luckyBasePoolScopeType = "pool"
// dynamic_v3 与 fixed_v2 的资金算法和启动规则不同,必须使用独立主键空间;
// 任何兼容读取都不能把两种策略重新折叠到同一口 pool 账本。
luckyDynamicPoolScopeType = "pool_dynamic_v3"
)
type luckyRTPWindow struct {
@ -47,12 +50,14 @@ func (w luckyRTPWindow) withCurrentWager(spent int64) luckyRTPWindow {
}
type luckyPool struct {
ScopeType string
ScopeID string
Balance int64
ReserveFloor int64
TotalIn int64
TotalOut int64
ScopeType string
ScopeID string
Balance int64
ReserveFloor int64
TotalIn int64
TotalOut int64
ManualCreditTotal int64
ManualDebitTotal int64
}
func (p luckyPool) capacity() int64 {
@ -1938,15 +1943,29 @@ func (r *Repository) ListLuckyGiftPoolBalances(ctx context.Context, query domain
if r == nil || r.db == nil {
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
appCode := appcode.Normalize(query.AppCode)
// 空 app_code 是后台“全部应用”筛选,不能经过 Normalize 变成默认 lalu只有明确值才参与租户条件。
appCode := strings.ToLower(strings.TrimSpace(query.AppCode))
poolID := strings.TrimSpace(query.PoolID)
if poolID != "" {
poolID = luckyPoolID(poolID)
}
strategyVersion := strings.ToLower(strings.TrimSpace(query.StrategyVersion))
// 先读真实单池账本。这个查询不会 FOR UPDATE也不会调用 getOrCreate数据汇总刷新不能制造奖池行或改变余额。
where := []string{"scope_type = ?"}
args := []any{luckyBasePoolScopeType}
// 先读两套真实账本。这个查询不会 FOR UPDATE也不会调用 getOrCreate后台列表不能制造奖池行或改变余额。
// strategy_version 由内部 scope_type 显式映射;固定与动态绝不能再按 app+pool 折叠覆盖。
where := make([]string, 0, 3)
args := make([]any, 0, 4)
if strategyVersion != "" {
scopeType, err := luckyPoolScopeForStrategy(strategyVersion)
if err != nil {
return nil, err
}
where = append(where, "scope_type = ?")
args = append(args, scopeType)
} else {
where = append(where, "scope_type IN (?, ?)")
args = append(args, luckyBasePoolScopeType, luckyDynamicPoolScopeType)
}
if appCode != "" {
where = append(where, "app_code = ?")
args = append(args, appCode)
@ -1956,10 +1975,11 @@ func (r *Repository) ListLuckyGiftPoolBalances(ctx context.Context, query domain
args = append(args, poolID)
}
rows, err := r.db.QueryContext(ctx, `
SELECT app_code, scope_id, balance, reserve_floor, total_in, total_out, updated_at_ms
SELECT app_code, scope_type, scope_id, balance, reserve_floor, total_in, total_out,
manual_credit_total, manual_debit_total, updated_at_ms
FROM lucky_pools
WHERE `+strings.Join(where, " AND ")+`
ORDER BY app_code ASC, scope_id ASC`, args...)
ORDER BY app_code ASC, scope_id ASC, scope_type ASC`, args...)
if err != nil {
return nil, err
}
@ -1968,12 +1988,21 @@ func (r *Repository) ListLuckyGiftPoolBalances(ctx context.Context, query domain
byKey := make(map[string]domain.PoolBalance)
for rows.Next() {
var item domain.PoolBalance
if err := rows.Scan(&item.AppCode, &item.PoolID, &item.Balance, &item.ReserveFloor, &item.TotalIn, &item.TotalOut, &item.UpdatedAtMS); err != nil {
var scopeType string
if err := rows.Scan(
&item.AppCode, &scopeType, &item.PoolID, &item.Balance, &item.ReserveFloor, &item.TotalIn, &item.TotalOut,
&item.ManualCreditTotal, &item.ManualDebitTotal, &item.UpdatedAtMS,
); err != nil {
return nil, err
}
mappedStrategy, ok := luckyStrategyForPoolScope(scopeType)
if !ok {
continue
}
item.StrategyVersion = mappedStrategy
item.Materialized = true
item.AvailableBalance = luckyAvailableBalance(item.Balance, item.ReserveFloor)
byKey[luckyPoolBalanceKey(item.AppCode, item.PoolID)] = item
byKey[luckyPoolBalanceKey(item.AppCode, item.PoolID, item.StrategyVersion)] = item
}
if err := rows.Err(); err != nil {
return nil, err
@ -1988,7 +2017,14 @@ func (r *Repository) ListLuckyGiftPoolBalances(ctx context.Context, query domain
if poolID != "" && luckyPoolID(config.PoolID) != poolID {
continue
}
key := luckyPoolBalanceKey(config.AppCode, config.PoolID)
configStrategy := strings.ToLower(strings.TrimSpace(config.StrategyVersion))
if configStrategy == "" {
configStrategy = domain.StrategyFixedV2
}
if strategyVersion != "" && configStrategy != strategyVersion {
continue
}
key := luckyPoolBalanceKey(config.AppCode, config.PoolID, configStrategy)
if _, exists := byKey[key]; exists {
continue
}
@ -1996,12 +2032,20 @@ func (r *Repository) ListLuckyGiftPoolBalances(ctx context.Context, query domain
if err != nil {
return nil, err
}
initialBalance := runtimeConfig.InitialBasePool
reserveFloor := runtimeConfig.BasePoolReserve
if configStrategy == domain.StrategyDynamicV3 {
// V3 规则即使来自迁移前旧数据,也只能展示独立零余额冷启动;配置里的历史 initial_pool_coins 不再形成资金。
initialBalance = 0
reserveFloor = 0
}
byKey[key] = domain.PoolBalance{
AppCode: appcode.Normalize(config.AppCode),
PoolID: runtimeConfig.PoolID,
Balance: runtimeConfig.InitialBasePool,
ReserveFloor: runtimeConfig.BasePoolReserve,
AvailableBalance: luckyAvailableBalance(runtimeConfig.InitialBasePool, runtimeConfig.BasePoolReserve),
StrategyVersion: configStrategy,
Balance: initialBalance,
ReserveFloor: reserveFloor,
AvailableBalance: luckyAvailableBalance(initialBalance, reserveFloor),
Materialized: false,
UpdatedAtMS: config.CreatedAtMS,
}
@ -2009,14 +2053,22 @@ func (r *Repository) ListLuckyGiftPoolBalances(ctx context.Context, query domain
// 没有任何配置的新应用仍然需要在 ops-center 看到 default 池。这里用当前默认规则推导初始水位,但不写库。
if appCode != "" && (poolID == "" || poolID == domain.DefaultPoolID) {
key := luckyPoolBalanceKey(appCode, domain.DefaultPoolID)
defaultStrategy := strategyVersion
if defaultStrategy == "" {
defaultStrategy = domain.StrategyDynamicV3
}
key := luckyPoolBalanceKey(appCode, domain.DefaultPoolID, defaultStrategy)
if _, exists := byKey[key]; !exists {
const defaultReferencePrice = int64(100)
defaultBalance := defaultReferencePrice * (950 + 500 + 2_375)
defaultReserve := defaultReferencePrice * (100 + 10 + 100)
var defaultBalance, defaultReserve int64
if defaultStrategy == domain.StrategyFixedV2 {
const defaultReferencePrice = int64(100)
defaultBalance = defaultReferencePrice * (950 + 500 + 2_375)
defaultReserve = defaultReferencePrice * (100 + 10 + 100)
}
byKey[key] = domain.PoolBalance{
AppCode: appCode,
PoolID: domain.DefaultPoolID,
StrategyVersion: defaultStrategy,
Balance: defaultBalance,
ReserveFloor: defaultReserve,
AvailableBalance: luckyAvailableBalance(defaultBalance, defaultReserve),
@ -2033,7 +2085,10 @@ func (r *Repository) ListLuckyGiftPoolBalances(ctx context.Context, query domain
if items[i].AppCode != items[j].AppCode {
return items[i].AppCode < items[j].AppCode
}
return items[i].PoolID < items[j].PoolID
if items[i].PoolID != items[j].PoolID {
return items[i].PoolID < items[j].PoolID
}
return items[i].StrategyVersion < items[j].StrategyVersion
})
return items, nil
}
@ -2077,8 +2132,8 @@ func luckyDrawSummaryCanUseStats(query domain.DrawQuery) bool {
return query.UserID <= 0 && strings.TrimSpace(query.RoomID) == "" && strings.TrimSpace(query.Status) == ""
}
func luckyPoolBalanceKey(appCode string, poolID string) string {
return appcode.Normalize(appCode) + "\x00" + luckyPoolID(poolID)
func luckyPoolBalanceKey(appCode string, poolID string, strategyVersion string) string {
return appcode.Normalize(appCode) + "\x00" + luckyPoolID(poolID) + "\x00" + strings.ToLower(strings.TrimSpace(strategyVersion))
}
func luckyAvailableBalance(balance int64, reserveFloor int64) int64 {
@ -2297,8 +2352,9 @@ func luckyRuntimeConfigFromRuleConfig(ruleConfig domain.RuleConfig) (domain.Conf
config.StrategyVersion = domain.StrategyFixedV2
}
if config.StrategyVersion == domain.StrategyDynamicV3 {
// V3 的冷启动资金是绝对金币值且 P=W 可以支付到 0旧版按礼物参考价推导的 reserve 不能继续套用。
config.InitialBasePool = config.InitialPoolCoins
// V3 只能由人工 credit 形成启动资金;旧配置残留的 initial_pool_coins 既不能注入账本,也不能进入策略冷启动状态。
config.InitialPoolCoins = 0
config.InitialBasePool = 0
config.BasePoolReserve = 0
}
config.MultiplierPPMs = luckyMultiplierPPMsFromTiers(config.Tiers, config.GiftPrice)
@ -2509,12 +2565,14 @@ func (r *Repository) getOrCreateLuckyPool(ctx context.Context, tx *sql.Tx, appCo
}
var pool luckyPool
err := tx.QueryRowContext(ctx, `
SELECT scope_type, scope_id, balance, reserve_floor, total_in, total_out
SELECT scope_type, scope_id, balance, reserve_floor, total_in, total_out,
manual_credit_total, manual_debit_total
FROM lucky_pools
WHERE app_code = ? AND scope_type = ? AND scope_id = ?
FOR UPDATE`,
appCode, scopeType, scopeID,
).Scan(&pool.ScopeType, &pool.ScopeID, &pool.Balance, &pool.ReserveFloor, &pool.TotalIn, &pool.TotalOut)
).Scan(&pool.ScopeType, &pool.ScopeID, &pool.Balance, &pool.ReserveFloor, &pool.TotalIn, &pool.TotalOut,
&pool.ManualCreditTotal, &pool.ManualDebitTotal)
return pool, err
}

View File

@ -45,6 +45,10 @@ func (r *Repository) PublishLuckyGiftRuleConfig(ctx context.Context, config doma
// 升级前的发布请求没有 strategy_version存储边界再次兜底为 fixed_v2避免绕过 service 的调用写成动态规则。
config.StrategyVersion = domain.StrategyFixedV2
}
if config.StrategyVersion == domain.StrategyDynamicV3 && config.InitialPoolCoins != 0 {
// 仓储边界再次守住零冷启动,即使内部调用绕过 service 校验,也不能发布一个会暗示自动注资的 V3 快照。
return domain.RuleConfig{}, xerr.New(xerr.InvalidArgument, "dynamic_v3 initial pool coins must be zero")
}
if config.JackpotMultiplierPPMs == nil {
config.JackpotMultiplierPPMs = []int64{}
}
@ -121,6 +125,21 @@ func (r *Repository) PublishLuckyGiftRuleConfig(ctx context.Context, config doma
}
}
}
if config.StrategyVersion == domain.StrategyDynamicV3 {
// 发布首个 V3 版本只物化一口 0 余额账本;后续 V3 版本命中唯一键后必须保持余额、累计和时间不变。
// 这也保证 fixed_v2 -> dynamic_v3 切换不会继承旧 scope_type=pool 的历史资金。
if _, err := tx.ExecContext(ctx, `
INSERT INTO lucky_pools (
app_code, scope_type, scope_id, balance, reserve_floor, total_in, total_out,
manual_credit_total, manual_debit_total, profit_total, anchor_income_total, initial_seed_total,
created_at_ms, updated_at_ms
) VALUES (?, ?, ?, 0, 0, 0, 0, 0, 0, 0, 0, 0, ?, ?)
ON DUPLICATE KEY UPDATE scope_id = scope_id`,
config.AppCode, luckyDynamicPoolScopeType, config.PoolID, nowMS, nowMS,
); err != nil {
return domain.RuleConfig{}, err
}
}
if err := tx.Commit(); err != nil {
return domain.RuleConfig{}, err
}

View File

@ -78,7 +78,7 @@ func TestPublishLuckyGiftRuleConfigPersistsVersionsAndTiers(t *testing.T) {
dynamic.StrategyVersion = domain.StrategyDynamicV3
dynamic.ProfitRatePPM = 10_000
dynamic.AnchorRatePPM = 10_000
dynamic.InitialPoolCoins = 1_000_000
dynamic.InitialPoolCoins = 0
dynamic.LossStreakGuarantee = 5
dynamic.LowWatermarkCoins = 10_000_000
dynamic.LowWaterNonzeroFactorPPM = 700_000
@ -112,7 +112,7 @@ func TestPublishLuckyGiftRuleConfigPersistsVersionsAndTiers(t *testing.T) {
if err != nil || !exists {
t.Fatalf("get dynamic rule: exists=%v err=%v", exists, err)
}
if loaded.StrategyVersion != domain.StrategyDynamicV3 || loaded.InitialPoolCoins != 1_000_000 || loaded.AnchorDailyPayoutCap != 6_000_000 {
if loaded.StrategyVersion != domain.StrategyDynamicV3 || loaded.InitialPoolCoins != 0 || loaded.AnchorDailyPayoutCap != 6_000_000 {
t.Fatalf("dynamic scalar fields were not persisted: %+v", loaded)
}
if len(loaded.JackpotMultiplierPPMs) != 3 || loaded.JackpotMultiplierPPMs[2] != 1_000_000_000 {
@ -121,6 +121,25 @@ func TestPublishLuckyGiftRuleConfigPersistsVersionsAndTiers(t *testing.T) {
if len(loaded.Stages) != 1 || loaded.Stages[0].MinRecharge7DCoins != 1 || loaded.Stages[0].MinRecharge30DCoins != 2 {
t.Fatalf("dynamic stage thresholds were not persisted: %+v", loaded.Stages)
}
var dynamicBalance int64
if err := schema.DB.QueryRow(`
SELECT balance FROM lucky_pools
WHERE app_code='lalu' AND scope_type='pool_dynamic_v3' AND scope_id='lucky'`).Scan(&dynamicBalance); err != nil || dynamicBalance != 0 {
t.Fatalf("first dynamic publish balance=%d err=%v, want materialized zero ledger", dynamicBalance, err)
}
if _, err := schema.DB.Exec(`
UPDATE lucky_pools SET balance=777, manual_credit_total=777
WHERE app_code='lalu' AND scope_type='pool_dynamic_v3' AND scope_id='lucky'`); err != nil {
t.Fatalf("fund dynamic ledger before republish: %v", err)
}
if _, err := repo.PublishLuckyGiftRuleConfig(ctx, dynamic, 1_700_000_300_000); err != nil {
t.Fatalf("publish later dynamic version: %v", err)
}
if err := schema.DB.QueryRow(`
SELECT balance FROM lucky_pools
WHERE app_code='lalu' AND scope_type='pool_dynamic_v3' AND scope_id='lucky'`).Scan(&dynamicBalance); err != nil || dynamicBalance != 777 {
t.Fatalf("later dynamic publish balance=%d err=%v, want existing 777 preserved", dynamicBalance, err)
}
}
func TestDynamicStrategyMigrationIsIdempotent(t *testing.T) {
@ -185,3 +204,52 @@ func TestDynamicStrategyMigrationIsIdempotent(t *testing.T) {
t.Fatalf("pre-dynamic row decoded as strategy=%q jackpot=%v, want fixed_v2 and empty jackpot", legacy.StrategyVersion, legacy.JackpotMultiplierPPMs)
}
}
func TestLuckyPoolBalanceMigrationMaterializesLatestDynamicV3AtZeroWithoutReset(t *testing.T) {
caller := mysqlschema.CallerFile(t, 1)
schema := mysqlschema.New(t, mysqlschema.Config{
EnvVar: "LUCKY_GIFT_SERVICE_MYSQL_TEST_DSN",
InitDBPath: mysqlschema.InitDBPath(t, caller, "..", "..", "..", "deploy", "mysql", "initdb", "001_lucky_gift_service.sql"),
DatabasePrefix: "hy_lucky_gift",
})
repo := &Repository{db: schema.DB}
ctx := appcode.WithContext(context.Background(), "aslan")
rule := dynamicMySQLTestRule("migration-v3")
if _, err := repo.PublishLuckyGiftRuleConfig(ctx, rule, 1_700_001_000_000); err != nil {
t.Fatalf("publish dynamic config: %v", err)
}
// 模拟现网已经发布的旧 V3 快照:配置里仍残留 1,000,000但 005 必须忽略它并创建零余额新 scope。
if _, err := schema.DB.Exec(`UPDATE lucky_gift_rule_versions SET initial_pool_coins=1000000 WHERE app_code='aslan' AND pool_id='migration-v3'`); err != nil {
t.Fatalf("seed legacy dynamic initial_pool_coins: %v", err)
}
if _, err := schema.DB.Exec(`
INSERT INTO lucky_pools (
app_code, scope_type, scope_id, balance, reserve_floor, total_in, total_out,
profit_total, anchor_income_total, initial_seed_total, created_at_ms, updated_at_ms
) VALUES ('aslan', 'pool', 'migration-v3', 888, 88, 1000, 112, 0, 0, 888, 1, 2)`); err != nil {
t.Fatalf("seed legacy fixed scope: %v", err)
}
if _, err := schema.DB.Exec(`DELETE FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool_dynamic_v3' AND scope_id='migration-v3'`); err != nil {
t.Fatalf("simulate pre-005 database: %v", err)
}
migrationPath := mysqlschema.InitDBPath(t, caller, "..", "..", "..", "deploy", "mysql", "migrations", "005_lucky_pool_balance.sql")
runLuckyGiftSQLScript(t, schema.DB, migrationPath)
var balance int64
if err := schema.DB.QueryRow(`SELECT balance FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool_dynamic_v3' AND scope_id='migration-v3'`).Scan(&balance); err != nil || balance != 0 {
t.Fatalf("005 materialized balance=%d err=%v, want 0", balance, err)
}
var legacyBalance int64
if err := schema.DB.QueryRow(`SELECT balance FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool' AND scope_id='migration-v3'`).Scan(&legacyBalance); err != nil || legacyBalance != 888 {
t.Fatalf("005 changed legacy fixed scope balance=%d err=%v, want 888", legacyBalance, err)
}
if _, err := schema.DB.Exec(`UPDATE lucky_pools SET balance=321, manual_credit_total=321 WHERE app_code='aslan' AND scope_type='pool_dynamic_v3' AND scope_id='migration-v3'`); err != nil {
t.Fatalf("fund migrated pool: %v", err)
}
runLuckyGiftSQLScript(t, schema.DB, migrationPath)
if err := schema.DB.QueryRow(`SELECT balance FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool_dynamic_v3' AND scope_id='migration-v3'`).Scan(&balance); err != nil || balance != 321 {
t.Fatalf("rerun 005 balance=%d err=%v, want existing 321 preserved", balance, err)
}
if err := schema.DB.QueryRow(`SELECT balance FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool' AND scope_id='migration-v3'`).Scan(&legacyBalance); err != nil || legacyBalance != 888 {
t.Fatalf("rerun 005 changed legacy fixed scope balance=%d err=%v, want 888", legacyBalance, err)
}
}

View File

@ -0,0 +1,261 @@
package mysql
import (
"context"
"crypto/rand"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"math"
"strings"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
)
func luckyPoolScopeForStrategy(strategyVersion string) (string, error) {
switch strings.ToLower(strings.TrimSpace(strategyVersion)) {
case domain.StrategyFixedV2:
return luckyBasePoolScopeType, nil
case domain.StrategyDynamicV3:
return luckyDynamicPoolScopeType, nil
default:
return "", xerr.New(xerr.InvalidArgument, "unsupported lucky gift strategy version")
}
}
func luckyStrategyForPoolScope(scopeType string) (string, bool) {
switch scopeType {
case luckyBasePoolScopeType:
return domain.StrategyFixedV2, true
case luckyDynamicPoolScopeType:
return domain.StrategyDynamicV3, true
default:
return "", false
}
}
// AdjustLuckyGiftPoolBalance 是人工资金调整的唯一持久化入口。同一个事务先锁 adjustment_id
// 再锁 app+strategy+pool 资金行;这样并发重试只能重放首次结果,不能在两个事务中重复增减余额。
func (r *Repository) AdjustLuckyGiftPoolBalance(ctx context.Context, command domain.PoolAdjustmentCommand, nowMS int64) (domain.PoolAdjustmentResult, error) {
if r == nil || r.db == nil {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
appCode := appcode.Normalize(command.AppCode)
poolID := luckyPoolID(command.PoolID)
scopeType, err := luckyPoolScopeForStrategy(command.StrategyVersion)
if err != nil {
return domain.PoolAdjustmentResult{}, err
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return domain.PoolAdjustmentResult{}, err
}
defer func() { _ = tx.Rollback() }()
var tokenEntropy [16]byte
if _, err := rand.Read(tokenEntropy[:]); err != nil {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.Unavailable, "cannot generate lucky gift pool adjustment insert token")
}
insertToken := hex.EncodeToString(tokenEntropy[:])
// 唯一键的 ON DUPLICATE 分支只做 no-op随后比较库内首次随机令牌来判断本事务是否首写
// 因而既不依赖 clientFoundRows也避免多个 duplicate INSERT 错误事务互相持锁形成死锁。
_, err = tx.ExecContext(ctx, `
INSERT INTO lucky_pool_adjustments (
app_code, adjustment_id, pool_id, strategy_version, scope_type, direction,
amount_coins, reason, operator_admin_id, request_id, insert_token,
balance_before, balance_after, pool_snapshot_json, status, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, 'pending', ?, ?)
ON DUPLICATE KEY UPDATE adjustment_id = adjustment_id`,
appCode, command.AdjustmentID, poolID, command.StrategyVersion, scopeType, command.Direction,
command.AmountCoins, command.Reason, command.OperatorAdminID, command.RequestID, insertToken, nowMS, nowMS,
)
if err != nil {
return domain.PoolAdjustmentResult{}, err
}
var stored domain.PoolAdjustment
var storedScopeType string
var storedRequestID string
var storedInsertToken string
var storedStatus string
var storedSnapshot sql.NullString
if err := tx.QueryRowContext(ctx, `
SELECT adjustment_id, app_code, pool_id, strategy_version, scope_type, direction,
amount_coins, reason, operator_admin_id, request_id, insert_token,
balance_before, balance_after, pool_snapshot_json, status, created_at_ms
FROM lucky_pool_adjustments
WHERE app_code = ? AND adjustment_id = ?
FOR UPDATE`, appCode, command.AdjustmentID,
).Scan(
&stored.AdjustmentID, &stored.AppCode, &stored.PoolID, &stored.StrategyVersion, &storedScopeType, &stored.Direction,
&stored.AmountCoins, &stored.Reason, &stored.OperatorAdminID, &storedRequestID, &storedInsertToken,
&stored.BalanceBefore, &stored.BalanceAfter, &storedSnapshot, &storedStatus, &stored.CreatedAtMS,
); err != nil {
return domain.PoolAdjustmentResult{}, err
}
inserted := storedInsertToken == insertToken
// request_id 允许随网络重试变化;其余字段共同定义资金命令,任何不一致都属于幂等冲突。
if stored.PoolID != poolID || stored.StrategyVersion != command.StrategyVersion || storedScopeType != scopeType ||
stored.Direction != command.Direction || stored.AmountCoins != command.AmountCoins || stored.Reason != command.Reason ||
stored.OperatorAdminID != command.OperatorAdminID {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.IdempotencyConflict, "lucky gift pool adjustment payload does not match the existing adjustment_id")
}
if !inserted {
if storedStatus != "applied" || !storedSnapshot.Valid {
// 正常事务不会留下 pending 行;若人工修复或异常导入造成该状态,宁可阻断也不能猜测是否已经动账。
return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool adjustment is not in an applied state")
}
var snapshot domain.PoolBalance
if err := json.Unmarshal([]byte(storedSnapshot.String), &snapshot); err != nil {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool adjustment snapshot is invalid")
}
if err := tx.Commit(); err != nil {
return domain.PoolAdjustmentResult{}, err
}
return domain.PoolAdjustmentResult{Adjustment: stored, Pool: snapshot, IdempotentReplay: true}, nil
}
pool, err := r.getOrCreateAdjustmentPool(ctx, tx, appCode, poolID, command.StrategyVersion, scopeType, nowMS)
if err != nil {
return domain.PoolAdjustmentResult{}, err
}
stored.BalanceBefore = pool.Balance
switch command.Direction {
case domain.PoolAdjustmentIn:
if pool.Balance > math.MaxInt64-command.AmountCoins || pool.ManualCreditTotal > math.MaxInt64-command.AmountCoins {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool credit would overflow the ledger")
}
pool.Balance += command.AmountCoins
pool.ManualCreditTotal += command.AmountCoins
case domain.PoolAdjustmentOut:
if pool.ManualDebitTotal > math.MaxInt64-command.AmountCoins || command.AmountCoins > pool.Balance || pool.Balance-command.AmountCoins < pool.ReserveFloor {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool debit would cross the reserve floor")
}
pool.Balance -= command.AmountCoins
pool.ManualDebitTotal += command.AmountCoins
default:
return domain.PoolAdjustmentResult{}, xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment direction is invalid")
}
stored.BalanceAfter = pool.Balance
// 手工资金只更新 balance 和独立累计列;业务 total_in/total_out 仍只表达送礼入池与实际返奖。
updateResult, err := tx.ExecContext(ctx, `
UPDATE lucky_pools
SET balance = ?, manual_credit_total = ?, manual_debit_total = ?, updated_at_ms = ?
WHERE app_code = ? AND scope_type = ? AND scope_id = ?`,
pool.Balance, pool.ManualCreditTotal, pool.ManualDebitTotal, nowMS, appCode, scopeType, poolID,
)
if err != nil {
return domain.PoolAdjustmentResult{}, err
}
if rows, err := updateResult.RowsAffected(); err != nil {
return domain.PoolAdjustmentResult{}, err
} else if rows != 1 {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool changed during adjustment")
}
snapshot := domain.PoolBalance{
AppCode: appCode,
PoolID: poolID,
StrategyVersion: command.StrategyVersion,
Balance: pool.Balance,
ReserveFloor: pool.ReserveFloor,
AvailableBalance: luckyAvailableBalance(pool.Balance, pool.ReserveFloor),
TotalIn: pool.TotalIn,
TotalOut: pool.TotalOut,
ManualCreditTotal: pool.ManualCreditTotal,
ManualDebitTotal: pool.ManualDebitTotal,
Materialized: true,
UpdatedAtMS: nowMS,
}
snapshotJSON, err := json.Marshal(snapshot)
if err != nil {
return domain.PoolAdjustmentResult{}, err
}
finalizeResult, err := tx.ExecContext(ctx, `
UPDATE lucky_pool_adjustments
SET balance_before = ?, balance_after = ?, pool_snapshot_json = ?, status = 'applied', updated_at_ms = ?
WHERE app_code = ? AND adjustment_id = ? AND status = 'pending'`,
stored.BalanceBefore, stored.BalanceAfter, snapshotJSON, nowMS, appCode, command.AdjustmentID,
)
if err != nil {
return domain.PoolAdjustmentResult{}, err
}
if rows, err := finalizeResult.RowsAffected(); err != nil {
return domain.PoolAdjustmentResult{}, err
} else if rows != 1 {
return domain.PoolAdjustmentResult{}, xerr.New(xerr.Conflict, "lucky gift pool adjustment could not be finalized")
}
if err := tx.Commit(); err != nil {
return domain.PoolAdjustmentResult{}, err
}
return domain.PoolAdjustmentResult{Adjustment: stored, Pool: snapshot}, nil
}
func (r *Repository) getOrCreateAdjustmentPool(ctx context.Context, tx *sql.Tx, appCode, poolID, strategyVersion, scopeType string, nowMS int64) (luckyPool, error) {
// 人工资金不能凭任意 pool_id 创建孤儿账本;指定策略至少要有一个已发布不可变版本。
var configured int
err := tx.QueryRowContext(ctx, `
SELECT 1
FROM lucky_gift_rule_versions
WHERE app_code = ? AND pool_id = ?
AND COALESCE(NULLIF(strategy_version, ''), 'fixed_v2') = ?
LIMIT 1`, appCode, poolID, strategyVersion,
).Scan(&configured)
if errors.Is(err, sql.ErrNoRows) {
return luckyPool{}, xerr.New(xerr.NotFound, "lucky gift pool strategy is not configured")
}
if err != nil {
return luckyPool{}, err
}
if strategyVersion == domain.StrategyDynamicV3 {
return r.getOrCreateLuckyPool(ctx, tx, appCode, scopeType, poolID, 0, 0, nowMS)
}
var existing luckyPool
err = tx.QueryRowContext(ctx, `
SELECT scope_type, scope_id, balance, reserve_floor, total_in, total_out,
manual_credit_total, manual_debit_total
FROM lucky_pools
WHERE app_code = ? AND scope_type = ? AND scope_id = ?
FOR UPDATE`, appCode, scopeType, poolID,
).Scan(
&existing.ScopeType, &existing.ScopeID, &existing.Balance, &existing.ReserveFloor, &existing.TotalIn, &existing.TotalOut,
&existing.ManualCreditTotal, &existing.ManualDebitTotal,
)
if err == nil {
return existing, nil
}
if !errors.Is(err, sql.ErrNoRows) {
return luckyPool{}, err
}
// fixed_v2 首次人工调整仍需保持旧算法的启动水位。只读取最新 fixed_v2 规则的参考价格,
// 不使用当前最新 dynamic_v3 配置,也不把旧固定账本从 0 重新解释。
var referencePrice int64
err = tx.QueryRowContext(ctx, `
SELECT gift_price_reference
FROM lucky_gift_rule_versions
WHERE app_code = ? AND pool_id = ?
AND COALESCE(NULLIF(strategy_version, ''), 'fixed_v2') = 'fixed_v2'
ORDER BY rule_version DESC
LIMIT 1`, appCode, poolID,
).Scan(&referencePrice)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return luckyPool{}, err
}
if errors.Is(err, sql.ErrNoRows) || referencePrice <= 0 {
return luckyPool{}, xerr.New(xerr.NotFound, "fixed_v2 lucky gift pool is not configured")
}
if referencePrice > math.MaxInt64/(950+500+2_375) {
return luckyPool{}, xerr.New(xerr.Conflict, "fixed_v2 lucky gift pool initial balance would overflow")
}
return r.getOrCreateLuckyPool(
ctx, tx, appCode, scopeType, poolID,
referencePrice*(950+500+2_375), referencePrice*(100+10+100), nowMS,
)
}

View File

@ -0,0 +1,163 @@
package mysql
import (
"context"
"sync"
"testing"
"hyapp/internal/testutil/mysqlschema"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
)
func TestAdjustLuckyGiftPoolBalanceSeparatesStrategiesAndReplaysIdempotently(t *testing.T) {
caller := mysqlschema.CallerFile(t, 1)
schema := mysqlschema.New(t, mysqlschema.Config{
EnvVar: "LUCKY_GIFT_SERVICE_MYSQL_TEST_DSN",
InitDBPath: mysqlschema.InitDBPath(t, caller, "..", "..", "..", "deploy", "mysql", "initdb", "001_lucky_gift_service.sql"),
DatabasePrefix: "hy_lucky_gift",
})
repo := &Repository{db: schema.DB}
ctx := appcode.WithContext(context.Background(), "aslan")
fixed := dynamicMySQLTestRule("shared-pool")
fixed.StrategyVersion = domain.StrategyFixedV2
if _, err := repo.PublishLuckyGiftRuleConfig(ctx, fixed, 1_700_010_000_000); err != nil {
t.Fatalf("publish fixed config: %v", err)
}
dynamic := dynamicMySQLTestRule("shared-pool")
if _, err := repo.PublishLuckyGiftRuleConfig(ctx, dynamic, 1_700_010_000_100); err != nil {
t.Fatalf("publish dynamic config: %v", err)
}
fixedResult, err := repo.AdjustLuckyGiftPoolBalance(ctx, domain.PoolAdjustmentCommand{
AppCode: "aslan", PoolID: "shared-pool", StrategyVersion: domain.StrategyFixedV2,
AdjustmentID: "fixed-credit-1", Direction: domain.PoolAdjustmentIn, AmountCoins: 10,
Reason: "legacy reserve", OperatorAdminID: 7,
}, 1_700_010_001_000)
if err != nil {
t.Fatalf("credit fixed pool: %v", err)
}
if fixedResult.Pool.Balance != 3_835 || fixedResult.Pool.ReserveFloor != 210 {
t.Fatalf("fixed pool snapshot=%+v, want old 3825/210 seed plus 10", fixedResult.Pool)
}
credit := domain.PoolAdjustmentCommand{
AppCode: "aslan", PoolID: "shared-pool", StrategyVersion: domain.StrategyDynamicV3,
AdjustmentID: "dynamic-credit-1", Direction: domain.PoolAdjustmentIn, AmountCoins: 100,
Reason: "approved launch budget", OperatorAdminID: 7, RequestID: "request-1",
}
first, err := repo.AdjustLuckyGiftPoolBalance(ctx, credit, 1_700_010_002_000)
if err != nil {
t.Fatalf("credit dynamic pool: %v", err)
}
if first.Adjustment.BalanceBefore != 0 || first.Adjustment.BalanceAfter != 100 || first.Pool.ManualCreditTotal != 100 || first.Pool.TotalIn != 0 {
t.Fatalf("dynamic credit result=%+v, want 0->100 isolated manual credit", first)
}
replayCommand := credit
replayCommand.RequestID = "request-retry"
replay, err := repo.AdjustLuckyGiftPoolBalance(ctx, replayCommand, 1_700_010_003_000)
if err != nil {
t.Fatalf("replay dynamic credit: %v", err)
}
if !replay.IdempotentReplay || replay.Adjustment.BalanceBefore != 0 || replay.Pool.Balance != 100 {
t.Fatalf("replay result=%+v, want original snapshot", replay)
}
mismatch := credit
mismatch.AmountCoins = 101
if _, err := repo.AdjustLuckyGiftPoolBalance(ctx, mismatch, 1_700_010_004_000); !xerr.IsCode(err, xerr.IdempotencyConflict) {
t.Fatalf("same ID with different payload error=%v, want idempotency conflict", err)
}
debit := domain.PoolAdjustmentCommand{
AppCode: "aslan", PoolID: "shared-pool", StrategyVersion: domain.StrategyDynamicV3,
AdjustmentID: "dynamic-debit-1", Direction: domain.PoolAdjustmentOut, AmountCoins: 30,
Reason: "return unused budget", OperatorAdminID: 7,
}
debitResult, err := repo.AdjustLuckyGiftPoolBalance(ctx, debit, 1_700_010_005_000)
if err != nil {
t.Fatalf("debit dynamic pool: %v", err)
}
if debitResult.Pool.Balance != 70 || debitResult.Pool.ManualDebitTotal != 30 || debitResult.Pool.TotalOut != 0 {
t.Fatalf("dynamic debit result=%+v, want manual 30 without business total_out", debitResult)
}
overDebit := debit
overDebit.AdjustmentID = "dynamic-debit-over-reserve"
overDebit.AmountCoins = 71
if _, err := repo.AdjustLuckyGiftPoolBalance(ctx, overDebit, 1_700_010_006_000); !xerr.IsCode(err, xerr.Conflict) {
t.Fatalf("over-reserve debit error=%v, want conflict", err)
}
// 八个并发重试共享一个 adjustment_id唯一键和 FOR UPDATE 必须只让一次 10 金币入账。
concurrent := credit
concurrent.AdjustmentID = "dynamic-credit-concurrent"
concurrent.AmountCoins = 10
concurrent.Reason = "single concurrent grant"
var wg sync.WaitGroup
errorsByWorker := make([]error, 8)
for index := range errorsByWorker {
wg.Add(1)
go func(worker int) {
defer wg.Done()
command := concurrent
command.RequestID = "concurrent-request"
_, errorsByWorker[worker] = repo.AdjustLuckyGiftPoolBalance(ctx, command, 1_700_010_007_000)
}(index)
}
wg.Wait()
for index, workerErr := range errorsByWorker {
if workerErr != nil {
t.Fatalf("concurrent worker %d: %v", index, workerErr)
}
}
var dynamicBalance, dynamicManualCredit, dynamicManualDebit, dynamicTotalIn, dynamicTotalOut int64
if err := schema.DB.QueryRow(`
SELECT balance, manual_credit_total, manual_debit_total, total_in, total_out
FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool_dynamic_v3' AND scope_id='shared-pool'`,
).Scan(&dynamicBalance, &dynamicManualCredit, &dynamicManualDebit, &dynamicTotalIn, &dynamicTotalOut); err != nil {
t.Fatalf("query dynamic pool: %v", err)
}
if dynamicBalance != 80 || dynamicManualCredit != 110 || dynamicManualDebit != 30 || dynamicTotalIn != 0 || dynamicTotalOut != 0 {
t.Fatalf("dynamic ledger balance/credit/debit/in/out=%d/%d/%d/%d/%d, want 80/110/30/0/0", dynamicBalance, dynamicManualCredit, dynamicManualDebit, dynamicTotalIn, dynamicTotalOut)
}
var fixedBalance int64
if err := schema.DB.QueryRow(`
SELECT balance FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool' AND scope_id='shared-pool'`,
).Scan(&fixedBalance); err != nil || fixedBalance != 3_835 {
t.Fatalf("fixed ledger balance=%d err=%v, want isolated 3835", fixedBalance, err)
}
items, err := repo.ListLuckyGiftPoolBalances(ctx, domain.PoolBalanceQuery{AppCode: "aslan", PoolID: "shared-pool"})
if err != nil {
t.Fatalf("list both strategy ledgers: %v", err)
}
if len(items) != 2 || items[0].StrategyVersion == items[1].StrategyVersion {
t.Fatalf("strategy-separated pool list=%+v, want fixed_v2 and dynamic_v3", items)
}
}
func TestAdjustLuckyGiftPoolBalanceRejectsUnconfiguredStrategy(t *testing.T) {
caller := mysqlschema.CallerFile(t, 1)
schema := mysqlschema.New(t, mysqlschema.Config{
EnvVar: "LUCKY_GIFT_SERVICE_MYSQL_TEST_DSN",
InitDBPath: mysqlschema.InitDBPath(t, caller, "..", "..", "..", "deploy", "mysql", "initdb", "001_lucky_gift_service.sql"),
DatabasePrefix: "hy_lucky_gift",
})
repo := &Repository{db: schema.DB}
ctx := appcode.WithContext(context.Background(), "aslan")
_, err := repo.AdjustLuckyGiftPoolBalance(ctx, domain.PoolAdjustmentCommand{
AppCode: "aslan", PoolID: "not-configured", StrategyVersion: domain.StrategyDynamicV3,
AdjustmentID: "orphan-credit", Direction: domain.PoolAdjustmentIn, AmountCoins: 1,
Reason: "must fail", OperatorAdminID: 7,
}, 1_700_020_000_000)
if !xerr.IsCode(err, xerr.NotFound) {
t.Fatalf("unconfigured strategy error=%v, want not found", err)
}
var count int
if scanErr := schema.DB.QueryRow(`SELECT COUNT(*) FROM lucky_pools WHERE app_code='aslan' AND scope_id='not-configured'`).Scan(&count); scanErr != nil || count != 0 {
t.Fatalf("orphan pool count=%d err=%v, want 0", count, scanErr)
}
}

View File

@ -228,8 +228,9 @@ func (s *AdminLuckyGiftServer) ListLuckyGiftPoolBalances(ctx context.Context, re
ctx = appcode.WithContext(ctx, appCode)
}
pools, err := s.svc.ListPoolBalances(ctx, domain.PoolBalanceQuery{
AppCode: appCode,
PoolID: req.GetPoolId(),
AppCode: appCode,
PoolID: req.GetPoolId(),
StrategyVersion: req.GetStrategyVersion(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
@ -241,6 +242,32 @@ func (s *AdminLuckyGiftServer) ListLuckyGiftPoolBalances(ctx context.Context, re
return resp, nil
}
func (s *AdminLuckyGiftServer) AdjustLuckyGiftPoolBalance(ctx context.Context, req *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest) (*luckygiftv1.AdjustLuckyGiftPoolBalanceResponse, error) {
if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" {
// 资金 RPC 必须 fail-closed缺失 meta 不能像普通兼容查询一样静默落到默认 lalu 租户。
return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment app_code is required"))
}
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
result, err := s.svc.AdjustPoolBalance(ctx, domain.PoolAdjustmentCommand{
PoolID: req.GetPoolId(),
StrategyVersion: req.GetStrategyVersion(),
AdjustmentID: req.GetAdjustmentId(),
Direction: req.GetDirection(),
AmountCoins: req.GetAmountCoins(),
Reason: req.GetReason(),
OperatorAdminID: req.GetOperatorAdminId(),
RequestID: req.GetMeta().GetRequestId(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &luckygiftv1.AdjustLuckyGiftPoolBalanceResponse{
Adjustment: luckyPoolAdjustmentToProto(result.Adjustment),
Pool: luckyPoolBalanceToProto(result.Pool),
IdempotentReplay: result.IdempotentReplay,
}, nil
}
func luckyRuleConfigFromProto(config *luckygiftv1.LuckyGiftRuleConfig) domain.RuleConfig {
if config == nil {
return domain.RuleConfig{}
@ -440,14 +467,33 @@ func luckyDrawSummaryToProto(summary domain.DrawSummary) *luckygiftv1.LuckyGiftD
func luckyPoolBalanceToProto(pool domain.PoolBalance) *luckygiftv1.LuckyGiftPoolBalance {
return &luckygiftv1.LuckyGiftPoolBalance{
AppCode: pool.AppCode,
PoolId: pool.PoolID,
Balance: pool.Balance,
ReserveFloor: pool.ReserveFloor,
AvailableBalance: pool.AvailableBalance,
TotalIn: pool.TotalIn,
TotalOut: pool.TotalOut,
Materialized: pool.Materialized,
UpdatedAtMs: pool.UpdatedAtMS,
AppCode: pool.AppCode,
PoolId: pool.PoolID,
StrategyVersion: pool.StrategyVersion,
Balance: pool.Balance,
ReserveFloor: pool.ReserveFloor,
AvailableBalance: pool.AvailableBalance,
TotalIn: pool.TotalIn,
TotalOut: pool.TotalOut,
ManualCreditTotal: pool.ManualCreditTotal,
ManualDebitTotal: pool.ManualDebitTotal,
Materialized: pool.Materialized,
UpdatedAtMs: pool.UpdatedAtMS,
}
}
func luckyPoolAdjustmentToProto(adjustment domain.PoolAdjustment) *luckygiftv1.LuckyGiftPoolAdjustment {
return &luckygiftv1.LuckyGiftPoolAdjustment{
AdjustmentId: adjustment.AdjustmentID,
AppCode: adjustment.AppCode,
PoolId: adjustment.PoolID,
StrategyVersion: adjustment.StrategyVersion,
Direction: adjustment.Direction,
AmountCoins: adjustment.AmountCoins,
Reason: adjustment.Reason,
OperatorAdminId: adjustment.OperatorAdminID,
BalanceBefore: adjustment.BalanceBefore,
BalanceAfter: adjustment.BalanceAfter,
CreatedAtMs: adjustment.CreatedAtMS,
}
}

View File

@ -5,8 +5,24 @@ import (
"testing"
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
service "hyapp/services/lucky-gift-service/internal/service/luckygift"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type poolAdjustmentGRPCRepository struct {
service.Repository
command domain.PoolAdjustmentCommand
result domain.PoolAdjustmentResult
}
func (r *poolAdjustmentGRPCRepository) AdjustLuckyGiftPoolBalance(_ context.Context, command domain.PoolAdjustmentCommand, _ int64) (domain.PoolAdjustmentResult, error) {
r.command = command
return r.result, nil
}
func TestBatchExecuteLuckyGiftDrawRejectsMixedAppCodes(t *testing.T) {
server := NewLuckyGiftServer(nil)
_, err := server.BatchExecuteLuckyGiftDraw(context.Background(), &luckygiftv1.BatchExecuteLuckyGiftDrawRequest{
@ -19,3 +35,42 @@ func TestBatchExecuteLuckyGiftDrawRejectsMixedAppCodes(t *testing.T) {
t.Fatal("expected mixed app_code batch to be rejected")
}
}
func TestAdjustLuckyGiftPoolBalanceMapsExplicitStrategyAndSnapshot(t *testing.T) {
repository := &poolAdjustmentGRPCRepository{result: domain.PoolAdjustmentResult{
Adjustment: domain.PoolAdjustment{
AdjustmentID: "adjustment-1", AppCode: "aslan", PoolID: "lucky", StrategyVersion: domain.StrategyDynamicV3,
Direction: domain.PoolAdjustmentIn, AmountCoins: 100, BalanceBefore: 0, BalanceAfter: 100,
},
Pool: domain.PoolBalance{
AppCode: "aslan", PoolID: "lucky", StrategyVersion: domain.StrategyDynamicV3,
Balance: 100, ManualCreditTotal: 100, Materialized: true,
},
}}
server := NewAdminLuckyGiftServer(service.New(repository))
resp, err := server.AdjustLuckyGiftPoolBalance(context.Background(), &luckygiftv1.AdjustLuckyGiftPoolBalanceRequest{
Meta: &luckygiftv1.RequestMeta{AppCode: "aslan", RequestId: "request-1"},
PoolId: "lucky", StrategyVersion: domain.StrategyDynamicV3, AdjustmentId: "adjustment-1",
Direction: domain.PoolAdjustmentIn, AmountCoins: 100, Reason: "launch", OperatorAdminId: 7,
})
if err != nil {
t.Fatalf("adjust pool RPC: %v", err)
}
if repository.command.AppCode != "aslan" || repository.command.RequestID != "request-1" || repository.command.StrategyVersion != domain.StrategyDynamicV3 {
t.Fatalf("owner command=%+v", repository.command)
}
if resp.GetPool().GetStrategyVersion() != domain.StrategyDynamicV3 || resp.GetPool().GetManualCreditTotal() != 100 || resp.GetAdjustment().GetBalanceAfter() != 100 {
t.Fatalf("RPC response=%+v", resp)
}
}
func TestAdjustLuckyGiftPoolBalanceRejectsMissingAppCodeBeforeOwnerCall(t *testing.T) {
server := NewAdminLuckyGiftServer(nil)
_, err := server.AdjustLuckyGiftPoolBalance(context.Background(), &luckygiftv1.AdjustLuckyGiftPoolBalanceRequest{
Meta: &luckygiftv1.RequestMeta{}, PoolId: "lucky", StrategyVersion: domain.StrategyDynamicV3,
AdjustmentId: "adjustment-1", Direction: domain.PoolAdjustmentIn, AmountCoins: 1, Reason: "test", OperatorAdminId: 7,
})
if status.Code(err) != codes.InvalidArgument {
t.Fatalf("missing app_code error=%v, want InvalidArgument", err)
}
}