From 427d335e4aef7f1a6f41bb54b117516c10ab72eb Mon Sep 17 00:00:00 2001 From: zhx Date: Fri, 5 Jun 2026 14:58:40 +0800 Subject: [PATCH] Update wallet resources and admin host flows --- api/proto/wallet/v1/wallet.pb.go | 149 ++++++++++-------- api/proto/wallet/v1/wallet.proto | 10 ++ .../internal/integration/userclient/client.go | 34 ++-- server/admin/internal/modules/auth/handler.go | 63 +++++--- .../internal/modules/auth/handler_test.go | 95 +++++++++++ .../admin/internal/modules/hostorg/handler.go | 10 +- .../admin/internal/modules/hostorg/reader.go | 58 ++++++- .../admin/internal/modules/hostorg/request.go | 2 + .../admin/internal/modules/hostorg/service.go | 10 ++ server/admin/internal/modules/resource/dto.go | 4 - .../internal/modules/resource/handler.go | 25 ++- .../internal/modules/resource/request.go | 8 +- .../internal/modules/roomtreasure/service.go | 8 +- .../modules/userleaderboard/service.go | 18 +-- .../activityapi/user_leaderboard_handler.go | 18 +-- .../deploy/mysql/initdb/001_room_service.sql | 2 +- .../internal/room/command/command.go | 2 +- .../internal/room/service/gift.go | 2 - .../internal/room/service/room_treasure.go | 19 +-- .../internal/storage/mysql/repository.go | 2 +- .../mysql/initdb/001_wallet_service.sql | 8 +- .../internal/domain/ledger/ledger.go | 35 ++-- .../internal/domain/resource/resource.go | 97 ++++++------ .../internal/service/wallet/service_test.go | 54 +++---- .../internal/storage/mysql/repository.go | 148 ++++------------- .../storage/mysql/resource_repository.go | 25 ++- .../internal/transport/grpc/resource.go | 13 +- 27 files changed, 537 insertions(+), 382 deletions(-) create mode 100644 server/admin/internal/modules/auth/handler_test.go diff --git a/api/proto/wallet/v1/wallet.pb.go b/api/proto/wallet/v1/wallet.pb.go index 1171ab46..31cefea3 100644 --- a/api/proto/wallet/v1/wallet.pb.go +++ b/api/proto/wallet/v1/wallet.pb.go @@ -174,9 +174,10 @@ type DebitGiftResponse struct { BillingReceiptId string `protobuf:"bytes,1,opt,name=billing_receipt_id,json=billingReceiptId,proto3" json:"billing_receipt_id,omitempty"` TransactionId string `protobuf:"bytes,2,opt,name=transaction_id,json=transactionId,proto3" json:"transaction_id,omitempty"` CoinSpent int64 `protobuf:"varint,3,opt,name=coin_spent,json=coinSpent,proto3" json:"coin_spent,omitempty"` - GiftPointAdded int64 `protobuf:"varint,4,opt,name=gift_point_added,json=giftPointAdded,proto3" json:"gift_point_added,omitempty"` - HeatValue int64 `protobuf:"varint,5,opt,name=heat_value,json=heatValue,proto3" json:"heat_value,omitempty"` - PriceVersion string `protobuf:"bytes,6,opt,name=price_version,json=priceVersion,proto3" json:"price_version,omitempty"` + // gift_point_added 是历史回执字段;GIFT_POINT 已下线,新送礼固定返回 0。 + GiftPointAdded int64 `protobuf:"varint,4,opt,name=gift_point_added,json=giftPointAdded,proto3" json:"gift_point_added,omitempty"` + HeatValue int64 `protobuf:"varint,5,opt,name=heat_value,json=heatValue,proto3" json:"heat_value,omitempty"` + PriceVersion string `protobuf:"bytes,6,opt,name=price_version,json=priceVersion,proto3" json:"price_version,omitempty"` // balance_after 是 sender 被扣费资产 available_amount 账后余额。 BalanceAfter int64 `protobuf:"varint,7,opt,name=balance_after,json=balanceAfter,proto3" json:"balance_after,omitempty"` ChargeAssetType string `protobuf:"bytes,8,opt,name=charge_asset_type,json=chargeAssetType,proto3" json:"charge_asset_type,omitempty"` @@ -2574,9 +2575,10 @@ type Resource struct { ManagerGrantEnabled bool `protobuf:"varint,21,opt,name=manager_grant_enabled,json=managerGrantEnabled,proto3" json:"manager_grant_enabled,omitempty"` PriceType string `protobuf:"bytes,22,opt,name=price_type,json=priceType,proto3" json:"price_type,omitempty"` CoinPrice int64 `protobuf:"varint,23,opt,name=coin_price,json=coinPrice,proto3" json:"coin_price,omitempty"` - GiftPointAmount int64 `protobuf:"varint,24,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // gift_point_amount 是历史字段;资源和礼物价格现在只维护真实 COIN 价格,新写入固定为 0。 + GiftPointAmount int64 `protobuf:"varint,24,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Resource) Reset() { @@ -3053,20 +3055,21 @@ type GiftConfig struct { PresentationJson string `protobuf:"bytes,8,opt,name=presentation_json,json=presentationJson,proto3" json:"presentation_json,omitempty"` PriceVersion string `protobuf:"bytes,9,opt,name=price_version,json=priceVersion,proto3" json:"price_version,omitempty"` CoinPrice int64 `protobuf:"varint,10,opt,name=coin_price,json=coinPrice,proto3" json:"coin_price,omitempty"` - GiftPointAmount int64 `protobuf:"varint,11,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` - HeatValue int64 `protobuf:"varint,12,opt,name=heat_value,json=heatValue,proto3" json:"heat_value,omitempty"` - CreatedByUserId int64 `protobuf:"varint,13,opt,name=created_by_user_id,json=createdByUserId,proto3" json:"created_by_user_id,omitempty"` - UpdatedByUserId int64 `protobuf:"varint,14,opt,name=updated_by_user_id,json=updatedByUserId,proto3" json:"updated_by_user_id,omitempty"` - CreatedAtMs int64 `protobuf:"varint,15,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` - UpdatedAtMs int64 `protobuf:"varint,16,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` - RegionIds []int64 `protobuf:"varint,17,rep,packed,name=region_ids,json=regionIds,proto3" json:"region_ids,omitempty"` - GiftTypeCode string `protobuf:"bytes,18,opt,name=gift_type_code,json=giftTypeCode,proto3" json:"gift_type_code,omitempty"` - ChargeAssetType string `protobuf:"bytes,19,opt,name=charge_asset_type,json=chargeAssetType,proto3" json:"charge_asset_type,omitempty"` - EffectiveFromMs int64 `protobuf:"varint,20,opt,name=effective_from_ms,json=effectiveFromMs,proto3" json:"effective_from_ms,omitempty"` - EffectiveToMs int64 `protobuf:"varint,21,opt,name=effective_to_ms,json=effectiveToMs,proto3" json:"effective_to_ms,omitempty"` - EffectTypes []string `protobuf:"bytes,22,rep,name=effect_types,json=effectTypes,proto3" json:"effect_types,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // gift_point_amount 是历史字段;送礼结算不再读取该值,新写入固定为 0。 + GiftPointAmount int64 `protobuf:"varint,11,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` + HeatValue int64 `protobuf:"varint,12,opt,name=heat_value,json=heatValue,proto3" json:"heat_value,omitempty"` + CreatedByUserId int64 `protobuf:"varint,13,opt,name=created_by_user_id,json=createdByUserId,proto3" json:"created_by_user_id,omitempty"` + UpdatedByUserId int64 `protobuf:"varint,14,opt,name=updated_by_user_id,json=updatedByUserId,proto3" json:"updated_by_user_id,omitempty"` + CreatedAtMs int64 `protobuf:"varint,15,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` + UpdatedAtMs int64 `protobuf:"varint,16,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` + RegionIds []int64 `protobuf:"varint,17,rep,packed,name=region_ids,json=regionIds,proto3" json:"region_ids,omitempty"` + GiftTypeCode string `protobuf:"bytes,18,opt,name=gift_type_code,json=giftTypeCode,proto3" json:"gift_type_code,omitempty"` + ChargeAssetType string `protobuf:"bytes,19,opt,name=charge_asset_type,json=chargeAssetType,proto3" json:"charge_asset_type,omitempty"` + EffectiveFromMs int64 `protobuf:"varint,20,opt,name=effective_from_ms,json=effectiveFromMs,proto3" json:"effective_from_ms,omitempty"` + EffectiveToMs int64 `protobuf:"varint,21,opt,name=effective_to_ms,json=effectiveToMs,proto3" json:"effective_to_ms,omitempty"` + EffectTypes []string `protobuf:"bytes,22,rep,name=effect_types,json=effectTypes,proto3" json:"effect_types,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GiftConfig) Reset() { @@ -4307,9 +4310,10 @@ type CreateResourceRequest struct { ManagerGrantEnabled *bool `protobuf:"varint,18,opt,name=manager_grant_enabled,json=managerGrantEnabled,proto3,oneof" json:"manager_grant_enabled,omitempty"` PriceType string `protobuf:"bytes,19,opt,name=price_type,json=priceType,proto3" json:"price_type,omitempty"` CoinPrice int64 `protobuf:"varint,20,opt,name=coin_price,json=coinPrice,proto3" json:"coin_price,omitempty"` - GiftPointAmount int64 `protobuf:"varint,21,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。 + GiftPointAmount int64 `protobuf:"varint,21,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateResourceRequest) Reset() { @@ -4512,9 +4516,10 @@ type UpdateResourceRequest struct { ManagerGrantEnabled *bool `protobuf:"varint,19,opt,name=manager_grant_enabled,json=managerGrantEnabled,proto3,oneof" json:"manager_grant_enabled,omitempty"` PriceType string `protobuf:"bytes,20,opt,name=price_type,json=priceType,proto3" json:"price_type,omitempty"` CoinPrice int64 `protobuf:"varint,21,opt,name=coin_price,json=coinPrice,proto3" json:"coin_price,omitempty"` - GiftPointAmount int64 `protobuf:"varint,22,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。 + GiftPointAmount int64 `protobuf:"varint,22,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdateResourceRequest) Reset() { @@ -5424,6 +5429,7 @@ type ListGiftConfigsRequest struct { ActiveOnly bool `protobuf:"varint,7,opt,name=active_only,json=activeOnly,proto3" json:"active_only,omitempty"` RegionId int64 `protobuf:"varint,8,opt,name=region_id,json=regionId,proto3" json:"region_id,omitempty"` FilterRegion bool `protobuf:"varint,9,opt,name=filter_region,json=filterRegion,proto3" json:"filter_region,omitempty"` + GiftTypeCode string `protobuf:"bytes,10,opt,name=gift_type_code,json=giftTypeCode,proto3" json:"gift_type_code,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5521,6 +5527,13 @@ func (x *ListGiftConfigsRequest) GetFilterRegion() bool { return false } +func (x *ListGiftConfigsRequest) GetGiftTypeCode() string { + if x != nil { + return x.GiftTypeCode + } + return "" +} + type ListGiftConfigsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Gifts []*GiftConfig `protobuf:"bytes,1,rep,name=gifts,proto3" json:"gifts,omitempty"` @@ -5841,18 +5854,19 @@ type CreateGiftConfigRequest struct { PresentationJson string `protobuf:"bytes,8,opt,name=presentation_json,json=presentationJson,proto3" json:"presentation_json,omitempty"` PriceVersion string `protobuf:"bytes,9,opt,name=price_version,json=priceVersion,proto3" json:"price_version,omitempty"` CoinPrice int64 `protobuf:"varint,10,opt,name=coin_price,json=coinPrice,proto3" json:"coin_price,omitempty"` - GiftPointAmount int64 `protobuf:"varint,11,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` - HeatValue int64 `protobuf:"varint,12,opt,name=heat_value,json=heatValue,proto3" json:"heat_value,omitempty"` - EffectiveAtMs int64 `protobuf:"varint,13,opt,name=effective_at_ms,json=effectiveAtMs,proto3" json:"effective_at_ms,omitempty"` - OperatorUserId int64 `protobuf:"varint,14,opt,name=operator_user_id,json=operatorUserId,proto3" json:"operator_user_id,omitempty"` - RegionIds []int64 `protobuf:"varint,15,rep,packed,name=region_ids,json=regionIds,proto3" json:"region_ids,omitempty"` - GiftTypeCode string `protobuf:"bytes,16,opt,name=gift_type_code,json=giftTypeCode,proto3" json:"gift_type_code,omitempty"` - ChargeAssetType string `protobuf:"bytes,17,opt,name=charge_asset_type,json=chargeAssetType,proto3" json:"charge_asset_type,omitempty"` - EffectiveFromMs int64 `protobuf:"varint,18,opt,name=effective_from_ms,json=effectiveFromMs,proto3" json:"effective_from_ms,omitempty"` - EffectiveToMs int64 `protobuf:"varint,19,opt,name=effective_to_ms,json=effectiveToMs,proto3" json:"effective_to_ms,omitempty"` - EffectTypes []string `protobuf:"bytes,20,rep,name=effect_types,json=effectTypes,proto3" json:"effect_types,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。 + GiftPointAmount int64 `protobuf:"varint,11,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` + HeatValue int64 `protobuf:"varint,12,opt,name=heat_value,json=heatValue,proto3" json:"heat_value,omitempty"` + EffectiveAtMs int64 `protobuf:"varint,13,opt,name=effective_at_ms,json=effectiveAtMs,proto3" json:"effective_at_ms,omitempty"` + OperatorUserId int64 `protobuf:"varint,14,opt,name=operator_user_id,json=operatorUserId,proto3" json:"operator_user_id,omitempty"` + RegionIds []int64 `protobuf:"varint,15,rep,packed,name=region_ids,json=regionIds,proto3" json:"region_ids,omitempty"` + GiftTypeCode string `protobuf:"bytes,16,opt,name=gift_type_code,json=giftTypeCode,proto3" json:"gift_type_code,omitempty"` + ChargeAssetType string `protobuf:"bytes,17,opt,name=charge_asset_type,json=chargeAssetType,proto3" json:"charge_asset_type,omitempty"` + EffectiveFromMs int64 `protobuf:"varint,18,opt,name=effective_from_ms,json=effectiveFromMs,proto3" json:"effective_from_ms,omitempty"` + EffectiveToMs int64 `protobuf:"varint,19,opt,name=effective_to_ms,json=effectiveToMs,proto3" json:"effective_to_ms,omitempty"` + EffectTypes []string `protobuf:"bytes,20,rep,name=effect_types,json=effectTypes,proto3" json:"effect_types,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateGiftConfigRequest) Reset() { @@ -6037,18 +6051,19 @@ type UpdateGiftConfigRequest struct { PresentationJson string `protobuf:"bytes,8,opt,name=presentation_json,json=presentationJson,proto3" json:"presentation_json,omitempty"` PriceVersion string `protobuf:"bytes,9,opt,name=price_version,json=priceVersion,proto3" json:"price_version,omitempty"` CoinPrice int64 `protobuf:"varint,10,opt,name=coin_price,json=coinPrice,proto3" json:"coin_price,omitempty"` - GiftPointAmount int64 `protobuf:"varint,11,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` - HeatValue int64 `protobuf:"varint,12,opt,name=heat_value,json=heatValue,proto3" json:"heat_value,omitempty"` - EffectiveAtMs int64 `protobuf:"varint,13,opt,name=effective_at_ms,json=effectiveAtMs,proto3" json:"effective_at_ms,omitempty"` - OperatorUserId int64 `protobuf:"varint,14,opt,name=operator_user_id,json=operatorUserId,proto3" json:"operator_user_id,omitempty"` - RegionIds []int64 `protobuf:"varint,15,rep,packed,name=region_ids,json=regionIds,proto3" json:"region_ids,omitempty"` - GiftTypeCode string `protobuf:"bytes,16,opt,name=gift_type_code,json=giftTypeCode,proto3" json:"gift_type_code,omitempty"` - ChargeAssetType string `protobuf:"bytes,17,opt,name=charge_asset_type,json=chargeAssetType,proto3" json:"charge_asset_type,omitempty"` - EffectiveFromMs int64 `protobuf:"varint,18,opt,name=effective_from_ms,json=effectiveFromMs,proto3" json:"effective_from_ms,omitempty"` - EffectiveToMs int64 `protobuf:"varint,19,opt,name=effective_to_ms,json=effectiveToMs,proto3" json:"effective_to_ms,omitempty"` - EffectTypes []string `protobuf:"bytes,20,rep,name=effect_types,json=effectTypes,proto3" json:"effect_types,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。 + GiftPointAmount int64 `protobuf:"varint,11,opt,name=gift_point_amount,json=giftPointAmount,proto3" json:"gift_point_amount,omitempty"` + HeatValue int64 `protobuf:"varint,12,opt,name=heat_value,json=heatValue,proto3" json:"heat_value,omitempty"` + EffectiveAtMs int64 `protobuf:"varint,13,opt,name=effective_at_ms,json=effectiveAtMs,proto3" json:"effective_at_ms,omitempty"` + OperatorUserId int64 `protobuf:"varint,14,opt,name=operator_user_id,json=operatorUserId,proto3" json:"operator_user_id,omitempty"` + RegionIds []int64 `protobuf:"varint,15,rep,packed,name=region_ids,json=regionIds,proto3" json:"region_ids,omitempty"` + GiftTypeCode string `protobuf:"bytes,16,opt,name=gift_type_code,json=giftTypeCode,proto3" json:"gift_type_code,omitempty"` + ChargeAssetType string `protobuf:"bytes,17,opt,name=charge_asset_type,json=chargeAssetType,proto3" json:"charge_asset_type,omitempty"` + EffectiveFromMs int64 `protobuf:"varint,18,opt,name=effective_from_ms,json=effectiveFromMs,proto3" json:"effective_from_ms,omitempty"` + EffectiveToMs int64 `protobuf:"varint,19,opt,name=effective_to_ms,json=effectiveToMs,proto3" json:"effective_to_ms,omitempty"` + EffectTypes []string `protobuf:"bytes,20,rep,name=effect_types,json=effectTypes,proto3" json:"effect_types,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdateGiftConfigRequest) Reset() { @@ -8589,15 +8604,16 @@ type GiftWallItem struct { TotalValue int64 `protobuf:"varint,9,opt,name=total_value,json=totalValue,proto3" json:"total_value,omitempty"` TotalCoinValue int64 `protobuf:"varint,10,opt,name=total_coin_value,json=totalCoinValue,proto3" json:"total_coin_value,omitempty"` TotalDiamondValue int64 `protobuf:"varint,11,opt,name=total_diamond_value,json=totalDiamondValue,proto3" json:"total_diamond_value,omitempty"` - TotalGiftPoint int64 `protobuf:"varint,12,opt,name=total_gift_point,json=totalGiftPoint,proto3" json:"total_gift_point,omitempty"` - TotalHeatValue int64 `protobuf:"varint,13,opt,name=total_heat_value,json=totalHeatValue,proto3" json:"total_heat_value,omitempty"` - ChargeAssetType string `protobuf:"bytes,14,opt,name=charge_asset_type,json=chargeAssetType,proto3" json:"charge_asset_type,omitempty"` - LastPriceVersion string `protobuf:"bytes,15,opt,name=last_price_version,json=lastPriceVersion,proto3" json:"last_price_version,omitempty"` - FirstReceivedAtMs int64 `protobuf:"varint,16,opt,name=first_received_at_ms,json=firstReceivedAtMs,proto3" json:"first_received_at_ms,omitempty"` - LastReceivedAtMs int64 `protobuf:"varint,17,opt,name=last_received_at_ms,json=lastReceivedAtMs,proto3" json:"last_received_at_ms,omitempty"` - SortOrder int32 `protobuf:"varint,18,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // total_gift_point 是历史礼物墙字段;新送礼不再累加。 + TotalGiftPoint int64 `protobuf:"varint,12,opt,name=total_gift_point,json=totalGiftPoint,proto3" json:"total_gift_point,omitempty"` + TotalHeatValue int64 `protobuf:"varint,13,opt,name=total_heat_value,json=totalHeatValue,proto3" json:"total_heat_value,omitempty"` + ChargeAssetType string `protobuf:"bytes,14,opt,name=charge_asset_type,json=chargeAssetType,proto3" json:"charge_asset_type,omitempty"` + LastPriceVersion string `protobuf:"bytes,15,opt,name=last_price_version,json=lastPriceVersion,proto3" json:"last_price_version,omitempty"` + FirstReceivedAtMs int64 `protobuf:"varint,16,opt,name=first_received_at_ms,json=firstReceivedAtMs,proto3" json:"first_received_at_ms,omitempty"` + LastReceivedAtMs int64 `protobuf:"varint,17,opt,name=last_received_at_ms,json=lastReceivedAtMs,proto3" json:"last_received_at_ms,omitempty"` + SortOrder int32 `protobuf:"varint,18,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GiftWallItem) Reset() { @@ -8824,10 +8840,11 @@ type GetUserGiftWallResponse struct { TotalValue int64 `protobuf:"varint,4,opt,name=total_value,json=totalValue,proto3" json:"total_value,omitempty"` TotalCoinValue int64 `protobuf:"varint,5,opt,name=total_coin_value,json=totalCoinValue,proto3" json:"total_coin_value,omitempty"` TotalDiamondValue int64 `protobuf:"varint,6,opt,name=total_diamond_value,json=totalDiamondValue,proto3" json:"total_diamond_value,omitempty"` - TotalGiftPoint int64 `protobuf:"varint,7,opt,name=total_gift_point,json=totalGiftPoint,proto3" json:"total_gift_point,omitempty"` - TotalHeatValue int64 `protobuf:"varint,8,opt,name=total_heat_value,json=totalHeatValue,proto3" json:"total_heat_value,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // total_gift_point 是历史礼物墙汇总字段;新送礼不再累加。 + TotalGiftPoint int64 `protobuf:"varint,7,opt,name=total_gift_point,json=totalGiftPoint,proto3" json:"total_gift_point,omitempty"` + TotalHeatValue int64 `protobuf:"varint,8,opt,name=total_heat_value,json=totalHeatValue,proto3" json:"total_heat_value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetUserGiftWallResponse) Reset() { @@ -14606,7 +14623,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\x06status\x18\x04 \x01(\tR\x06status\x12(\n" + "\x10operator_user_id\x18\x05 \x01(\x03R\x0eoperatorUserId\"M\n" + "\x15ResourceGroupResponse\x124\n" + - "\x05group\x18\x01 \x01(\v2\x1e.hyapp.wallet.v1.ResourceGroupR\x05group\"\x98\x02\n" + + "\x05group\x18\x01 \x01(\v2\x1e.hyapp.wallet.v1.ResourceGroupR\x05group\"\xbe\x02\n" + "\x16ListGiftConfigsRequest\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" + @@ -14618,7 +14635,9 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\vactive_only\x18\a \x01(\bR\n" + "activeOnly\x12\x1b\n" + "\tregion_id\x18\b \x01(\x03R\bregionId\x12#\n" + - "\rfilter_region\x18\t \x01(\bR\ffilterRegion\"b\n" + + "\rfilter_region\x18\t \x01(\bR\ffilterRegion\x12$\n" + + "\x0egift_type_code\x18\n" + + " \x01(\tR\fgiftTypeCode\"b\n" + "\x17ListGiftConfigsResponse\x121\n" + "\x05gifts\x18\x01 \x03(\v2\x1b.hyapp.wallet.v1.GiftConfigR\x05gifts\x12\x14\n" + "\x05total\x18\x02 \x01(\x03R\x05total\"\x8f\x01\n" + diff --git a/api/proto/wallet/v1/wallet.proto b/api/proto/wallet/v1/wallet.proto index 7bad52ac..4daa67e5 100644 --- a/api/proto/wallet/v1/wallet.proto +++ b/api/proto/wallet/v1/wallet.proto @@ -32,6 +32,7 @@ message DebitGiftResponse { string billing_receipt_id = 1; string transaction_id = 2; int64 coin_spent = 3; + // gift_point_added 是历史回执字段;GIFT_POINT 已下线,新送礼固定返回 0。 int64 gift_point_added = 4; int64 heat_value = 5; string price_version = 6; @@ -325,6 +326,7 @@ message Resource { bool manager_grant_enabled = 21; string price_type = 22; int64 coin_price = 23; + // gift_point_amount 是历史字段;资源和礼物价格现在只维护真实 COIN 价格,新写入固定为 0。 int64 gift_point_amount = 24; } @@ -369,6 +371,7 @@ message GiftConfig { string presentation_json = 8; string price_version = 9; int64 coin_price = 10; + // gift_point_amount 是历史字段;送礼结算不再读取该值,新写入固定为 0。 int64 gift_point_amount = 11; int64 heat_value = 12; int64 created_by_user_id = 13; @@ -518,6 +521,7 @@ message CreateResourceRequest { optional bool manager_grant_enabled = 18; string price_type = 19; int64 coin_price = 20; + // gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。 int64 gift_point_amount = 21; } @@ -543,6 +547,7 @@ message UpdateResourceRequest { optional bool manager_grant_enabled = 19; string price_type = 20; int64 coin_price = 21; + // gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。 int64 gift_point_amount = 22; } @@ -630,6 +635,7 @@ message ListGiftConfigsRequest { bool active_only = 7; int64 region_id = 8; bool filter_region = 9; + string gift_type_code = 10; } message ListGiftConfigsResponse { @@ -674,6 +680,7 @@ message CreateGiftConfigRequest { string presentation_json = 8; string price_version = 9; int64 coin_price = 10; + // gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。 int64 gift_point_amount = 11; int64 heat_value = 12; int64 effective_at_ms = 13; @@ -697,6 +704,7 @@ message UpdateGiftConfigRequest { string presentation_json = 8; string price_version = 9; int64 coin_price = 10; + // gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。 int64 gift_point_amount = 11; int64 heat_value = 12; int64 effective_at_ms = 13; @@ -967,6 +975,7 @@ message GiftWallItem { int64 total_value = 9; int64 total_coin_value = 10; int64 total_diamond_value = 11; + // total_gift_point 是历史礼物墙字段;新送礼不再累加。 int64 total_gift_point = 12; int64 total_heat_value = 13; string charge_asset_type = 14; @@ -989,6 +998,7 @@ message GetUserGiftWallResponse { int64 total_value = 4; int64 total_coin_value = 5; int64 total_diamond_value = 6; + // total_gift_point 是历史礼物墙汇总字段;新送礼不再累加。 int64 total_gift_point = 7; int64 total_heat_value = 8; } diff --git a/server/admin/internal/integration/userclient/client.go b/server/admin/internal/integration/userclient/client.go index 42c8d105..7c348822 100644 --- a/server/admin/internal/integration/userclient/client.go +++ b/server/admin/internal/integration/userclient/client.go @@ -386,21 +386,25 @@ type Agency struct { } type HostProfile struct { - UserID int64 `json:"userId,string"` - Status string `json:"status"` - RegionID int64 `json:"regionId"` - CurrentAgencyID int64 `json:"currentAgencyId"` - CurrentMembershipID int64 `json:"currentMembershipId"` - Source string `json:"source"` - FirstBecameHostAtMs int64 `json:"firstBecameHostAtMs"` - CreatedAtMs int64 `json:"createdAtMs"` - UpdatedAtMs int64 `json:"updatedAtMs"` - DisplayUserID string `json:"displayUserId"` - Username string `json:"username"` - Avatar string `json:"avatar"` - RegionName string `json:"regionName"` - CurrentAgencyName string `json:"currentAgencyName"` - Diamond int64 `json:"diamond"` + UserID int64 `json:"userId,string"` + Status string `json:"status"` + RegionID int64 `json:"regionId"` + CurrentAgencyID int64 `json:"currentAgencyId"` + CurrentMembershipID int64 `json:"currentMembershipId"` + Source string `json:"source"` + FirstBecameHostAtMs int64 `json:"firstBecameHostAtMs"` + CreatedAtMs int64 `json:"createdAtMs"` + UpdatedAtMs int64 `json:"updatedAtMs"` + DisplayUserID string `json:"displayUserId"` + Username string `json:"username"` + Avatar string `json:"avatar"` + RegionName string `json:"regionName"` + CurrentAgencyName string `json:"currentAgencyName"` + Diamond int64 `json:"diamond"` + CurrentAgencyOwnerUserID int64 `json:"currentAgencyOwnerUserId,string"` + CurrentAgencyOwnerDisplayUserID string `json:"currentAgencyOwnerDisplayUserId"` + CurrentAgencyOwnerUsername string `json:"currentAgencyOwnerUsername"` + CurrentAgencyOwnerAvatar string `json:"currentAgencyOwnerAvatar"` } type CoinSellerProfile struct { diff --git a/server/admin/internal/modules/auth/handler.go b/server/admin/internal/modules/auth/handler.go index b4657c31..d66a45d7 100644 --- a/server/admin/internal/modules/auth/handler.go +++ b/server/admin/internal/modules/auth/handler.go @@ -12,7 +12,11 @@ import ( authcore "hyapp-admin-server/internal/service" ) -const refreshCookieName = "hyapp_admin_refresh" +const ( + refreshCookieName = "hyapp_admin_refresh" + refreshCookiePath = "/" + legacyRefreshCookiePath = "/api/v1/auth" +) type Handler struct { service *AuthFlowService @@ -52,7 +56,7 @@ func (h *Handler) Login(c *gin.Context) { } func (h *Handler) Logout(c *gin.Context) { - if token, err := c.Cookie(refreshCookieName); err == nil && token != "" { + for _, token := range refreshCookieCandidates(c) { _ = h.service.Logout(token) } h.setRefreshCookie(c, "", -1) @@ -60,28 +64,32 @@ func (h *Handler) Logout(c *gin.Context) { } func (h *Handler) Refresh(c *gin.Context) { - token, err := c.Cookie(refreshCookieName) - if err != nil || token == "" { + tokens := refreshCookieCandidates(c) + if len(tokens) == 0 { response.Unauthorized(c, "刷新会话不存在") return } - result, err := h.service.Refresh(token, LoginInput{ - IP: c.ClientIP(), - UserAgent: c.Request.UserAgent(), - }) - if err != nil { - response.Unauthorized(c, "刷新会话已失效") - return + var result LoginResult + var err error + for _, token := range tokens { + result, err = h.service.Refresh(token, LoginInput{ + IP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + }) + if err == nil { + h.setRefreshCookie(c, result.RefreshToken, int(h.cfg.RefreshTokenTTL.Seconds())) + response.OK(c, gin.H{ + "accessToken": result.AccessToken, + "expiresAtMs": result.ExpiresAtMS, + "user": shared.UserDTO(result.User), + "permissions": result.Permissions, + }) + return + } } - h.setRefreshCookie(c, result.RefreshToken, int(h.cfg.RefreshTokenTTL.Seconds())) - response.OK(c, gin.H{ - "accessToken": result.AccessToken, - "expiresAtMs": result.ExpiresAtMS, - "user": shared.UserDTO(result.User), - "permissions": result.Permissions, - }) + response.Unauthorized(c, "刷新会话已失效") } func (h *Handler) Me(c *gin.Context) { @@ -114,7 +122,24 @@ func (h *Handler) ChangePassword(c *gin.Context) { func (h *Handler) setRefreshCookie(c *gin.Context, token string, maxAge int) { c.SetSameSite(refreshCookieSameSite(h.cfg.RefreshCookieSameSite)) - c.SetCookie(refreshCookieName, token, maxAge, "/api/v1/auth", "", h.cfg.RefreshCookieSecure, true) + c.SetCookie(refreshCookieName, token, maxAge, refreshCookiePath, "", h.cfg.RefreshCookieSecure, true) + c.SetCookie(refreshCookieName, "", -1, legacyRefreshCookiePath, "", h.cfg.RefreshCookieSecure, true) +} + +func refreshCookieCandidates(c *gin.Context) []string { + values := make([]string, 0, 2) + seen := map[string]struct{}{} + for _, cookie := range c.Request.Cookies() { + if cookie.Name != refreshCookieName || cookie.Value == "" { + continue + } + if _, ok := seen[cookie.Value]; ok { + continue + } + seen[cookie.Value] = struct{}{} + values = append(values, cookie.Value) + } + return values } func refreshCookieSameSite(value string) http.SameSite { diff --git a/server/admin/internal/modules/auth/handler_test.go b/server/admin/internal/modules/auth/handler_test.go new file mode 100644 index 00000000..27da8da1 --- /dev/null +++ b/server/admin/internal/modules/auth/handler_test.go @@ -0,0 +1,95 @@ +package auth + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gin-gonic/gin" + "hyapp-admin-server/internal/config" + authcore "hyapp-admin-server/internal/service" +) + +func TestRefreshUsesRootCookieAndClearsLegacyCookiePath(t *testing.T) { + gin.SetMode(gin.TestMode) + store := newFakeAuthStore(t) + handler := testAuthHandler(store) + login, err := handler.service.Login(LoginInput{Username: "admin", Password: "secret", IP: "127.0.0.1", UserAgent: "browser"}) + if err != nil { + t.Fatalf("login: %v", err) + } + router := gin.New() + router.POST("/api/v1/auth/refresh", handler.Refresh) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/refresh", nil) + req.AddCookie(&http.Cookie{Name: refreshCookieName, Value: login.RefreshToken}) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("refresh status = %d body=%s", rec.Code, rec.Body.String()) + } + setCookies := rec.Result().Cookies() + if !hasCookieWithPath(setCookies, refreshCookieName, refreshCookiePath) { + t.Fatalf("refresh must set root refresh cookie: %+v", setCookies) + } + if !hasExpiredCookieWithPath(setCookies, refreshCookieName, legacyRefreshCookiePath) { + t.Fatalf("refresh must clear legacy path cookie: %+v", setCookies) + } +} + +func TestRefreshTriesDuplicateRefreshCookiesUntilOneWorks(t *testing.T) { + gin.SetMode(gin.TestMode) + store := newFakeAuthStore(t) + handler := testAuthHandler(store) + first, err := handler.service.Login(LoginInput{Username: "admin", Password: "secret", IP: "127.0.0.1", UserAgent: "browser"}) + if err != nil { + t.Fatalf("login: %v", err) + } + refreshed, err := handler.service.Refresh(first.RefreshToken, LoginInput{IP: "127.0.0.1", UserAgent: "browser"}) + if err != nil { + t.Fatalf("prime refresh: %v", err) + } + router := gin.New() + router.POST("/api/v1/auth/refresh", handler.Refresh) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/refresh", nil) + req.Header.Set("Cookie", refreshCookieName+"="+first.RefreshToken+"; "+refreshCookieName+"="+refreshed.RefreshToken) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("refresh status = %d body=%s", rec.Code, rec.Body.String()) + } + if strings.Contains(rec.Body.String(), "刷新会话已失效") { + t.Fatalf("refresh should skip stale duplicate cookie: %s", rec.Body.String()) + } +} + +func testAuthHandler(store *fakeAuthStore) *Handler { + cfg := config.Config{RefreshTokenTTL: 7 * 24 * time.Hour, RefreshCookieSameSite: "lax"} + return &Handler{ + service: NewService(store, authcore.NewAuthService("test-secret", time.Hour), cfg), + cfg: cfg, + } +} + +func hasCookieWithPath(cookies []*http.Cookie, name string, path string) bool { + for _, cookie := range cookies { + if cookie.Name == name && cookie.Path == path && cookie.MaxAge >= 0 && cookie.Value != "" { + return true + } + } + return false +} + +func hasExpiredCookieWithPath(cookies []*http.Cookie, name string, path string) bool { + for _, cookie := range cookies { + if cookie.Name == name && cookie.Path == path && cookie.MaxAge < 0 { + return true + } + } + return false +} diff --git a/server/admin/internal/modules/hostorg/handler.go b/server/admin/internal/modules/hostorg/handler.go index bd336947..1558403d 100644 --- a/server/admin/internal/modules/hostorg/handler.go +++ b/server/admin/internal/modules/hostorg/handler.go @@ -318,10 +318,12 @@ func parseInt64ID(c *gin.Context, name string) (int64, bool) { func parseListQuery(c *gin.Context) (listQuery, bool) { options := shared.ListOptions(c) query := listQuery{ - Page: options.Page, - PageSize: options.PageSize, - Keyword: options.Keyword, - Status: options.Status, + Page: options.Page, + PageSize: options.PageSize, + Keyword: options.Keyword, + Status: options.Status, + SortBy: firstNonBlank(c.Query("sortBy"), c.Query("sort_by")), + SortDirection: firstNonBlank(c.Query("sortDirection"), c.Query("sort_direction"), c.Query("order")), } var ok bool if query.RegionID, ok = parseOptionalInt64Query(c, "regionId", "region_id"); !ok { diff --git a/server/admin/internal/modules/hostorg/reader.go b/server/admin/internal/modules/hostorg/reader.go index 46cccc47..b164fc0f 100644 --- a/server/admin/internal/modules/hostorg/reader.go +++ b/server/admin/internal/modules/hostorg/reader.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "sort" "strings" "time" @@ -341,6 +342,7 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user FROM host_profiles hp LEFT JOIN users u ON u.app_code = hp.app_code AND u.user_id = hp.user_id LEFT JOIN agencies a ON a.app_code = hp.app_code AND a.agency_id = hp.current_agency_id + LEFT JOIN users agency_owner ON agency_owner.app_code = a.app_code AND agency_owner.user_id = a.owner_user_id LEFT JOIN regions r ON r.app_code = hp.app_code AND r.region_id = hp.region_id WHERE hp.app_code = ?` args := []any{appCode} @@ -358,24 +360,32 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user } if keyword := strings.TrimSpace(query.Keyword); keyword != "" { like := "%" + keyword + "%" - whereSQL += " AND (CAST(hp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ? OR a.name LIKE ? OR r.name LIKE ?)" - args = append(args, like, like, like, like, like) + whereSQL += " AND (CAST(hp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ? OR a.name LIKE ? OR agency_owner.current_display_user_id LIKE ? OR agency_owner.username LIKE ? OR r.name LIKE ?)" + args = append(args, like, like, like, like, like, like, like) } total, err := countRows(ctx, r.db, whereSQL, args...) if err != nil { return nil, 0, err } + limitSQL := "LIMIT ? OFFSET ?" + queryArgs := append(args, query.PageSize, offset(query.Page, query.PageSize)) + if query.SortBy == "diamond" { + limitSQL = "" + queryArgs = args + } rows, err := r.db.QueryContext(ctx, fmt.Sprintf(` SELECT hp.user_id, hp.status, hp.region_id, COALESCE(hp.current_agency_id, 0), COALESCE(hp.current_membership_id, 0), hp.source, hp.first_became_host_at_ms, hp.created_at_ms, hp.updated_at_ms, COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''), - COALESCE(u.avatar, ''), COALESCE(r.name, ''), COALESCE(a.name, '') + COALESCE(u.avatar, ''), COALESCE(r.name, ''), COALESCE(a.name, ''), + COALESCE(a.owner_user_id, 0), COALESCE(agency_owner.current_display_user_id, ''), + COALESCE(agency_owner.username, ''), COALESCE(agency_owner.avatar, '') %s ORDER BY hp.created_at_ms DESC, hp.user_id DESC - LIMIT ? OFFSET ? - `, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...) + %s + `, whereSQL, limitSQL), queryArgs...) if err != nil { return nil, 0, err } @@ -400,6 +410,10 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user &item.Avatar, &item.RegionName, &item.CurrentAgencyName, + &item.CurrentAgencyOwnerUserID, + &item.CurrentAgencyOwnerDisplayUserID, + &item.CurrentAgencyOwnerUsername, + &item.CurrentAgencyOwnerAvatar, ); err != nil { return nil, 0, err } @@ -416,6 +430,10 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user for _, item := range items { item.Diamond = diamonds[item.UserID] } + if query.SortBy == "diamond" { + sortHostProfilesByDiamond(items, query.SortDirection) + items = paginateHostProfiles(items, query.Page, query.PageSize) + } return items, total, nil } @@ -718,6 +736,36 @@ func (r *Reader) hostPeriodDiamonds(ctx context.Context, appCode string, userIDs return result, rows.Err() } +func sortHostProfilesByDiamond(items []*userclient.HostProfile, direction string) { + sort.SliceStable(items, func(i, j int) bool { + if items[i].Diamond == items[j].Diamond { + return false + } + if direction == "asc" { + return items[i].Diamond < items[j].Diamond + } + return items[i].Diamond > items[j].Diamond + }) +} + +func paginateHostProfiles(items []*userclient.HostProfile, page int, pageSize int) []*userclient.HostProfile { + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + start := (page - 1) * pageSize + if start >= len(items) { + return []*userclient.HostProfile{} + } + end := start + pageSize + if end > len(items) { + end = len(items) + } + return items[start:end] +} + func sqlPlaceholders(count int) string { if count <= 0 { return "" diff --git a/server/admin/internal/modules/hostorg/request.go b/server/admin/internal/modules/hostorg/request.go index aa6b2ad9..b142b65c 100644 --- a/server/admin/internal/modules/hostorg/request.go +++ b/server/admin/internal/modules/hostorg/request.go @@ -9,6 +9,8 @@ type listQuery struct { AgencyID int64 ParentBDUserID int64 ParentLeaderUserID int64 + SortBy string + SortDirection string } type createBDLeaderRequest struct { diff --git a/server/admin/internal/modules/hostorg/service.go b/server/admin/internal/modules/hostorg/service.go index adeea825..10dbab77 100644 --- a/server/admin/internal/modules/hostorg/service.go +++ b/server/admin/internal/modules/hostorg/service.go @@ -472,6 +472,16 @@ func normalizeListQuery(query listQuery) listQuery { } query.Keyword = strings.TrimSpace(query.Keyword) query.Status = strings.ToLower(strings.TrimSpace(query.Status)) + query.SortBy = strings.ToLower(strings.TrimSpace(query.SortBy)) + if query.SortBy != "diamond" { + query.SortBy = "" + query.SortDirection = "" + return query + } + query.SortDirection = strings.ToLower(strings.TrimSpace(query.SortDirection)) + if query.SortDirection != "asc" { + query.SortDirection = "desc" + } return query } diff --git a/server/admin/internal/modules/resource/dto.go b/server/admin/internal/modules/resource/dto.go index f232ebd6..be9e9e36 100644 --- a/server/admin/internal/modules/resource/dto.go +++ b/server/admin/internal/modules/resource/dto.go @@ -16,7 +16,6 @@ type resourceDTO struct { WalletAssetAmount int64 `json:"walletAssetAmount"` PriceType string `json:"priceType"` CoinPrice int64 `json:"coinPrice"` - GiftPointAmount int64 `json:"giftPointAmount"` BadgeForm string `json:"badgeForm,omitempty"` BadgeKind string `json:"badgeKind,omitempty"` LevelTrack string `json:"levelTrack,omitempty"` @@ -74,7 +73,6 @@ type giftDTO struct { GiftTypeCode string `json:"giftTypeCode"` ChargeAssetType string `json:"chargeAssetType"` CoinPrice int64 `json:"coinPrice"` - GiftPointAmount int64 `json:"giftPointAmount"` EffectiveFromMS int64 `json:"effectiveFromMs"` EffectiveToMS int64 `json:"effectiveToMs"` EffectTypes []string `json:"effectTypes"` @@ -197,7 +195,6 @@ func resourceFromProto(item *walletv1.Resource) resourceDTO { WalletAssetAmount: item.GetWalletAssetAmount(), PriceType: item.GetPriceType(), CoinPrice: item.GetCoinPrice(), - GiftPointAmount: item.GetGiftPointAmount(), BadgeForm: badgeFormForResource(item.GetResourceType(), item.GetMetadataJson()), BadgeKind: badgeKindForResource(item.GetResourceType(), item.GetMetadataJson()), LevelTrack: badgeLevelTrackForResource(item.GetResourceType(), item.GetMetadataJson()), @@ -298,7 +295,6 @@ func giftFromProto(gift *walletv1.GiftConfig) giftDTO { GiftTypeCode: gift.GetGiftTypeCode(), ChargeAssetType: gift.GetChargeAssetType(), CoinPrice: gift.GetCoinPrice(), - GiftPointAmount: gift.GetGiftPointAmount(), EffectiveFromMS: gift.GetEffectiveFromMs(), EffectiveToMS: gift.GetEffectiveToMs(), EffectTypes: gift.GetEffectTypes(), diff --git a/server/admin/internal/modules/resource/handler.go b/server/admin/internal/modules/resource/handler.go index 9f4932d4..dc2a9cf1 100644 --- a/server/admin/internal/modules/resource/handler.go +++ b/server/admin/internal/modules/resource/handler.go @@ -294,14 +294,15 @@ func (h *Handler) ListGifts(c *gin.Context) { return } resp, err := h.wallet.ListGiftConfigs(c.Request.Context(), &walletv1.ListGiftConfigsRequest{ - RequestId: middleware.CurrentRequestID(c), - AppCode: appctx.FromContext(c.Request.Context()), - Status: options.Status, - Keyword: options.Keyword, - Page: int32(options.Page), - PageSize: int32(options.PageSize), - RegionId: regionID, - FilterRegion: filterRegion, + RequestId: middleware.CurrentRequestID(c), + AppCode: appctx.FromContext(c.Request.Context()), + Status: options.Status, + Keyword: options.Keyword, + Page: int32(options.Page), + PageSize: int32(options.PageSize), + RegionId: regionID, + FilterRegion: filterRegion, + GiftTypeCode: giftTypeCodeQuery(c), }) if err != nil { response.ServerError(c, "获取礼物列表失败") @@ -719,6 +720,14 @@ func optionalInt64Query(c *gin.Context, primary string, fallback string) (int64, return value, ok } +func giftTypeCodeQuery(c *gin.Context) string { + value := strings.TrimSpace(c.Query("giftTypeCode")) + if value == "" { + value = strings.TrimSpace(c.Query("gift_type_code")) + } + return value +} + func optionalInt64QueryWithPresence(c *gin.Context, primary string, fallback string) (int64, bool, bool) { raw := strings.TrimSpace(c.Query(primary)) if raw == "" { diff --git a/server/admin/internal/modules/resource/request.go b/server/admin/internal/modules/resource/request.go index ec2c2008..86ae1f8d 100644 --- a/server/admin/internal/modules/resource/request.go +++ b/server/admin/internal/modules/resource/request.go @@ -53,7 +53,6 @@ type resourceRequest struct { Amount int64 `json:"amount"` PriceType string `json:"priceType"` CoinPrice int64 `json:"coinPrice"` - GiftPointAmount int64 `json:"giftPointAmount"` BadgeForm string `json:"badgeForm"` BadgeKind string `json:"badgeKind"` LevelTrack string `json:"levelTrack"` @@ -134,7 +133,6 @@ type giftRequest struct { GiftTypeCode string `json:"giftTypeCode"` ChargeAssetType string `json:"chargeAssetType"` CoinPrice int64 `json:"coinPrice"` - GiftPointAmount int64 `json:"giftPointAmount"` HeatValue int64 `json:"heatValue"` // 兼容旧后台请求;新后台不再展示或提交热度。 EffectiveAtMS int64 `json:"effectiveAtMs"` EffectiveFromMS int64 `json:"effectiveFromMs"` @@ -370,7 +368,7 @@ func (r giftRequest) createProto(c *gin.Context) *walletv1.CreateGiftConfigReque GiftTypeCode: strings.TrimSpace(r.GiftTypeCode), ChargeAssetType: strings.TrimSpace(r.ChargeAssetType), CoinPrice: r.CoinPrice, - GiftPointAmount: r.GiftPointAmount, + GiftPointAmount: 0, HeatValue: r.CoinPrice, EffectiveAtMs: r.EffectiveAtMS, EffectiveFromMs: r.EffectiveFromMS, @@ -395,7 +393,7 @@ func (r giftRequest) updateProto(c *gin.Context, giftID string) *walletv1.Update GiftTypeCode: strings.TrimSpace(r.GiftTypeCode), ChargeAssetType: strings.TrimSpace(r.ChargeAssetType), CoinPrice: r.CoinPrice, - GiftPointAmount: r.GiftPointAmount, + GiftPointAmount: 0, HeatValue: r.CoinPrice, EffectiveAtMs: r.EffectiveAtMS, EffectiveFromMs: r.EffectiveFromMS, @@ -502,7 +500,7 @@ func resourcePricing(priceType string, coinPrice int64) (string, int64, int64) { if priceType == resourcePriceTypeFree { return resourcePriceTypeFree, 0, 0 } - return resourcePriceTypeCoin, coinPrice, coinPrice + return resourcePriceTypeCoin, coinPrice, 0 } func validateResourcePricing(req resourceRequest) error { diff --git a/server/admin/internal/modules/roomtreasure/service.go b/server/admin/internal/modules/roomtreasure/service.go index 81fb395c..93516f7e 100644 --- a/server/admin/internal/modules/roomtreasure/service.go +++ b/server/admin/internal/modules/roomtreasure/service.go @@ -13,7 +13,7 @@ import ( const ( roomTreasureLevelCount = 7 - defaultEnergySource = "gift_point_added" + defaultEnergySource = "heat_value" defaultOpenDelayMS = int64(30_000) defaultBroadcastScope = "region" defaultRewardStackPolicy = "allow_stack" @@ -161,7 +161,11 @@ func normalizeConfig(config RoomTreasureConfig) (RoomTreasureConfig, error) { return RoomTreasureConfig{}, fmt.Errorf("%w: 配置版本不能小于 0", ErrInvalidArgument) } config.EnergySource = defaultString(strings.TrimSpace(config.EnergySource), defaultEnergySource) - if !stringIn(config.EnergySource, "gift_point_added", "heat_value") { + if config.EnergySource == "gift_point_added" { + // gift_point_added 是历史配置值;GIFT_POINT 已下线,后台读取旧配置时统一切到真实礼物贡献口径。 + config.EnergySource = defaultEnergySource + } + if config.EnergySource != defaultEnergySource { return RoomTreasureConfig{}, fmt.Errorf("%w: 能量来源不支持", ErrInvalidArgument) } if config.OpenDelayMS < 0 { diff --git a/server/admin/internal/modules/userleaderboard/service.go b/server/admin/internal/modules/userleaderboard/service.go index b120b635..3d4f2b2b 100644 --- a/server/admin/internal/modules/userleaderboard/service.go +++ b/server/admin/internal/modules/userleaderboard/service.go @@ -213,6 +213,7 @@ func periodWindow(period string, now time.Time) (time.Time, time.Time) { func (s *Service) count(ctx context.Context, appCode string, boardType string, startMS int64, endMS int64) (int64, error) { subject, predicate := leaderboardDimension(boardType) + // 排行榜价值优先使用送礼时按真实扣费比例生成的 heat_value;旧交易缺字段时只退回 coin_spent/charge_amount,不再读取 GIFT_POINT。 query := fmt.Sprintf(` WITH gift_facts AS ( SELECT @@ -220,10 +221,9 @@ func (s *Service) count(ctx context.Context, appCode string, boardType string, s CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) AS target_user_id, COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.room_id')), '') AS room_id, COALESCE( - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_added')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_amount')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.heat_value')) AS SIGNED), CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.coin_spent')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), 0 ) AS gift_value, COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_count')) AS SIGNED), 0) AS gift_count, @@ -247,6 +247,7 @@ func (s *Service) count(ctx context.Context, appCode string, boardType string, s func (s *Service) listItems(ctx context.Context, appCode string, boardType string, startMS int64, endMS int64, limit int, offset int) ([]Item, error) { subject, predicate := leaderboardDimension(boardType) + // 排行榜列表和值排序统一使用 heat_value 口径,避免历史 gift_point 字段重新参与运营排行。 query := fmt.Sprintf(` WITH gift_facts AS ( SELECT @@ -254,10 +255,9 @@ func (s *Service) listItems(ctx context.Context, appCode string, boardType strin CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) AS target_user_id, COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.room_id')), '') AS room_id, COALESCE( - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_added')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_amount')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.heat_value')) AS SIGNED), CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.coin_spent')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), 0 ) AS gift_value, COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_count')) AS SIGNED), 0) AS gift_count, @@ -305,6 +305,7 @@ func (s *Service) listItems(ctx context.Context, appCode string, boardType strin func (s *Service) rankForUser(ctx context.Context, appCode string, boardType string, startMS int64, endMS int64, userID int64) (Item, bool, error) { subject, predicate := leaderboardDimension(boardType) + // 用户名次查询与列表共用真实贡献口径;只兼容读取老交易的扣费金额,不兼容读取积分字段。 query := fmt.Sprintf(` WITH gift_facts AS ( SELECT @@ -312,10 +313,9 @@ func (s *Service) rankForUser(ctx context.Context, appCode string, boardType str CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) AS target_user_id, COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.room_id')), '') AS room_id, COALESCE( - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_added')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_amount')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.heat_value')) AS SIGNED), CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.coin_spent')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), 0 ) AS gift_value, COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_count')) AS SIGNED), 0) AS gift_count, diff --git a/services/gateway-service/internal/transport/http/activityapi/user_leaderboard_handler.go b/services/gateway-service/internal/transport/http/activityapi/user_leaderboard_handler.go index 1afbe771..f1d1ad02 100644 --- a/services/gateway-service/internal/transport/http/activityapi/user_leaderboard_handler.go +++ b/services/gateway-service/internal/transport/http/activityapi/user_leaderboard_handler.go @@ -223,6 +223,7 @@ func userLeaderboardPeriodWindow(period string, now time.Time) (time.Time, time. func countUserLeaderboard(ctx context.Context, db *sql.DB, appCode string, boardType string, startMS int64, endMS int64) (int64, error) { subject, predicate := userLeaderboardDimension(boardType) + // App 排行榜价值优先使用真实送礼贡献 heat_value;旧交易只兜底扣费金额,不再读取历史 GIFT_POINT 字段。 query := fmt.Sprintf(` WITH gift_facts AS ( SELECT @@ -230,10 +231,9 @@ func countUserLeaderboard(ctx context.Context, db *sql.DB, appCode string, board CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) AS target_user_id, COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.room_id')), '') AS room_id, COALESCE( - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_added')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_amount')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.heat_value')) AS SIGNED), CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.coin_spent')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), 0 ) AS gift_value, COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_count')) AS SIGNED), 0) AS gift_count, @@ -257,6 +257,7 @@ func countUserLeaderboard(ctx context.Context, db *sql.DB, appCode string, board func listUserLeaderboardItems(ctx context.Context, db *sql.DB, appCode string, boardType string, startMS int64, endMS int64, limit int, offset int) ([]userLeaderboardItem, error) { subject, predicate := userLeaderboardDimension(boardType) + // 列表排序和值统计统一走 heat_value,保证 App 和后台排行榜不被历史积分字段污染。 query := fmt.Sprintf(` WITH gift_facts AS ( SELECT @@ -264,10 +265,9 @@ func listUserLeaderboardItems(ctx context.Context, db *sql.DB, appCode string, b CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) AS target_user_id, COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.room_id')), '') AS room_id, COALESCE( - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_added')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_amount')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.heat_value')) AS SIGNED), CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.coin_spent')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), 0 ) AS gift_value, COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_count')) AS SIGNED), 0) AS gift_count, @@ -315,6 +315,7 @@ func listUserLeaderboardItems(ctx context.Context, db *sql.DB, appCode string, b func userLeaderboardRankForUser(ctx context.Context, db *sql.DB, appCode string, boardType string, startMS int64, endMS int64, userID int64) (userLeaderboardItem, bool, error) { subject, predicate := userLeaderboardDimension(boardType) + // 当前用户名次查询只按真实贡献或扣费兜底统计,不再兼容 gift_point_added/gift_point_amount。 query := fmt.Sprintf(` WITH gift_facts AS ( SELECT @@ -322,10 +323,9 @@ func userLeaderboardRankForUser(ctx context.Context, db *sql.DB, appCode string, CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) AS target_user_id, COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.room_id')), '') AS room_id, COALESCE( - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_added')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_point_amount')) AS SIGNED), - CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.heat_value')) AS SIGNED), CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.coin_spent')) AS SIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_amount')) AS SIGNED), 0 ) AS gift_value, COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.gift_count')) AS SIGNED), 0) AS gift_count, diff --git a/services/room-service/deploy/mysql/initdb/001_room_service.sql b/services/room-service/deploy/mysql/initdb/001_room_service.sql index 26d3b27e..b267839d 100644 --- a/services/room-service/deploy/mysql/initdb/001_room_service.sql +++ b/services/room-service/deploy/mysql/initdb/001_room_service.sql @@ -254,7 +254,7 @@ INSERT IGNORE INTO room_treasure_configs ( 'lalu', 0, 1, - 'gift_point_added', + 'heat_value', 30000, 1, 'region', diff --git a/services/room-service/internal/room/command/command.go b/services/room-service/internal/room/command/command.go index bc22a9a5..c752e3d3 100644 --- a/services/room-service/internal/room/command/command.go +++ b/services/room-service/internal/room/command/command.go @@ -406,7 +406,7 @@ type SendGift struct { BillingReceiptID string `json:"billing_receipt_id,omitempty"` // CoinSpent 是 sender COIN 实际扣减值,来自 wallet-service 服务端价格。 CoinSpent int64 `json:"coin_spent,omitempty"` - // GiftPointAdded 是 target GIFT_POINT 实际入账值,来自 wallet-service。 + // GiftPointAdded 是历史命令字段;GIFT_POINT 已下线,新送礼不再赋值,恢复逻辑只依赖 HeatValue。 GiftPointAdded int64 `json:"gift_point_added,omitempty"` // HeatValue 是 Room Cell 恢复时唯一可信的热度增量,不能重新用客户端价格推导。 HeatValue int64 `json:"heat_value,omitempty"` diff --git a/services/room-service/internal/room/service/gift.go b/services/room-service/internal/room/service/gift.go index 51551591..d1a71cd0 100644 --- a/services/room-service/internal/room/service/gift.go +++ b/services/room-service/internal/room/service/gift.go @@ -123,7 +123,6 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r } settledCommand.BillingReceiptID = billing.GetBillingReceiptId() settledCommand.CoinSpent = billing.GetCoinSpent() - settledCommand.GiftPointAdded = billing.GetGiftPointAdded() settledCommand.HeatValue = billing.GetHeatValue() settledCommand.PriceVersion = billing.GetPriceVersion() settledCommand.GiftTypeCode = billing.GetGiftTypeCode() @@ -259,7 +258,6 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r "gift_count": fmt.Sprintf("%d", cmd.GiftCount), "billing_receipt_id": billing.GetBillingReceiptId(), "coin_spent": fmt.Sprintf("%d", settledCommand.CoinSpent), - "gift_point_added": fmt.Sprintf("%d", settledCommand.GiftPointAdded), "target_count": fmt.Sprintf("%d", len(cmd.TargetUserIDs)), "target_user_ids": giftTargetUserIDsAttribute(cmd.TargetUserIDs), "price_version": settledCommand.PriceVersion, diff --git a/services/room-service/internal/room/service/room_treasure.go b/services/room-service/internal/room/service/room_treasure.go index 4f80cb78..359c0de1 100644 --- a/services/room-service/internal/room/service/room_treasure.go +++ b/services/room-service/internal/room/service/room_treasure.go @@ -25,12 +25,11 @@ import ( const ( roomTreasureLevelCount = 7 - roomTreasureDefaultEnergySource = "gift_point_added" + roomTreasureDefaultEnergySource = "heat_value" roomTreasureDefaultOpenDelayMS = int64(30_000) roomTreasureDefaultBroadcastScope = "region" roomTreasureDefaultRewardStackPolicy = "allow_stack" - roomTreasureEnergyGiftPoint = "gift_point_added" roomTreasureEnergyHeatValue = "heat_value" roomTreasureBroadcastNone = "none" @@ -127,7 +126,11 @@ func normalizeRoomTreasureConfig(config RoomTreasureConfig) (RoomTreasureConfig, return RoomTreasureConfig{}, xerr.New(xerr.InvalidArgument, "room treasure config_version is invalid") } config.EnergySource = defaultRoomTreasureString(config.EnergySource, roomTreasureDefaultEnergySource) - if config.EnergySource != roomTreasureEnergyGiftPoint && config.EnergySource != roomTreasureEnergyHeatValue { + if config.EnergySource == "gift_point_added" { + // 旧配置可能还存着 gift_point_added;GIFT_POINT 已下线,运行时统一按真实送礼贡献 heat_value 计算宝箱能量。 + config.EnergySource = roomTreasureEnergyHeatValue + } + if config.EnergySource != roomTreasureEnergyHeatValue { return RoomTreasureConfig{}, xerr.New(xerr.InvalidArgument, "room treasure energy_source is invalid") } if config.OpenDelayMS < 0 || config.BroadcastDelayMS < 0 { @@ -327,7 +330,7 @@ func (s *Service) applyRoomTreasureGift(now time.Time, current *state.RoomState, treasure := treasureStateForNow(current.Treasure, cfg, now, true) result := treasureGiftApplyResult{touched: current.Treasure == nil || treasure.ResetAtMS != current.Treasure.ResetAtMS} - addedEnergy := roomTreasureEnergy(cfg, cmd.GiftID, billing.GetGiftTypeCode(), billing.GetGiftPointAdded(), billing.GetHeatValue()) + addedEnergy := roomTreasureEnergy(cfg, cmd.GiftID, billing.GetGiftTypeCode(), billing.GetHeatValue()) effectiveEnergy := int64(0) overflowEnergy := int64(0) @@ -483,11 +486,9 @@ func roomTreasureLevelByNumber(cfg RoomTreasureConfig, level int32) RoomTreasure return defaultRoomTreasureConfig(cfg.AppCode).Levels[0] } -func roomTreasureEnergy(cfg RoomTreasureConfig, giftID string, giftTypeCode string, giftPointAdded int64, heatValue int64) int64 { - base := giftPointAdded - if cfg.EnergySource == roomTreasureEnergyHeatValue { - base = heatValue - } +func roomTreasureEnergy(cfg RoomTreasureConfig, giftID string, giftTypeCode string, heatValue int64) int64 { + // 宝箱能量只读取 wallet-service 按真实扣费和贡献比例计算出的 heat_value,避免历史 GIFT_POINT 字段污染活动进度。 + base := heatValue if base < 0 { base = 0 } diff --git a/services/room-service/internal/storage/mysql/repository.go b/services/room-service/internal/storage/mysql/repository.go index 8560606d..dac7c891 100644 --- a/services/room-service/internal/storage/mysql/repository.go +++ b/services/room-service/internal/storage/mysql/repository.go @@ -291,7 +291,7 @@ func (r *Repository) Migrate(ctx context.Context) error { 'lalu', 0, 1, - 'gift_point_added', + 'heat_value', 30000, 1, 'region', diff --git a/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql b/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql index f6151479..95e27fc0 100644 --- a/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql +++ b/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql @@ -402,7 +402,7 @@ CREATE TABLE IF NOT EXISTS wallet_gift_prices ( status VARCHAR(32) NOT NULL COMMENT '业务状态', charge_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '扣费资产类型', coin_price BIGINT NOT NULL COMMENT '金币价格', - gift_point_amount BIGINT NOT NULL COMMENT '礼物积分数量', + gift_point_amount BIGINT NOT NULL COMMENT '历史字段:GIFT_POINT 已下线,新写入固定 0', heat_value BIGINT NOT NULL COMMENT '热度值', effective_at_ms BIGINT NOT NULL COMMENT '生效时间,UTC epoch ms', created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', @@ -590,7 +590,7 @@ CREATE TABLE IF NOT EXISTS resources ( wallet_asset_amount BIGINT NOT NULL DEFAULT 0 COMMENT '钱包资产数量', price_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '资源价格类型:coin/free', coin_price BIGINT NOT NULL DEFAULT 0 COMMENT '资源金币价格', - gift_point_amount BIGINT NOT NULL DEFAULT 0 COMMENT '资源礼物积分', + gift_point_amount BIGINT NOT NULL DEFAULT 0 COMMENT '历史字段:GIFT_POINT 已下线,新写入固定 0', usage_scope_json JSON NOT NULL COMMENT '使用范围 JSON 配置或快照', asset_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '资产 URL', preview_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '预览 URL', @@ -635,7 +635,7 @@ DEALLOCATE PREPARE stmt; SET @ddl := IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resources' AND COLUMN_NAME = 'gift_point_amount') = 0, - 'ALTER TABLE resources ADD COLUMN gift_point_amount BIGINT NOT NULL DEFAULT 0 COMMENT ''资源礼物积分'' AFTER coin_price', + 'ALTER TABLE resources ADD COLUMN gift_point_amount BIGINT NOT NULL DEFAULT 0 COMMENT ''历史字段:GIFT_POINT 已下线,新写入固定 0'' AFTER coin_price', 'SELECT 1' ); PREPARE stmt FROM @ddl; @@ -831,7 +831,7 @@ CREATE TABLE IF NOT EXISTS user_gift_wall ( total_value BIGINT NOT NULL DEFAULT 0 COMMENT '总值', total_coin_value BIGINT NOT NULL DEFAULT 0 COMMENT '总金币值', total_diamond_value BIGINT NOT NULL DEFAULT 0 COMMENT '总钻石值', - total_gift_point BIGINT NOT NULL DEFAULT 0 COMMENT '总礼物积分', + total_gift_point BIGINT NOT NULL DEFAULT 0 COMMENT '历史字段:GIFT_POINT 已下线,新送礼不再累加', total_heat_value BIGINT NOT NULL DEFAULT 0 COMMENT '总热度值', charge_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '扣费资产类型', last_price_version VARCHAR(64) NOT NULL DEFAULT '' COMMENT '最后价格版本', diff --git a/services/wallet-service/internal/domain/ledger/ledger.go b/services/wallet-service/internal/domain/ledger/ledger.go index fce24118..c6ebf25b 100644 --- a/services/wallet-service/internal/domain/ledger/ledger.go +++ b/services/wallet-service/internal/domain/ledger/ledger.go @@ -7,7 +7,7 @@ const ( AssetCoin = "COIN" // AssetCoinSellerCoin 是币商专用金币资产,只能通过币商转账兑换成玩家普通 COIN。 AssetCoinSellerCoin = "COIN_SELLER_COIN" - // AssetGiftPoint 是主播礼物积分,不能直接消费或提现。 + // AssetGiftPoint 是历史礼物积分资产,只保留历史账本查询兼容;新送礼不会再给它入账。 AssetGiftPoint = "GIFT_POINT" // AssetHostPeriodDiamond 是主播工资周期钻石投影,只参与工资等级结算,不进入通用钱包余额。 AssetHostPeriodDiamond = "HOST_PERIOD_DIAMOND" @@ -126,7 +126,7 @@ type DebitGiftCommand struct { type DebitGiftTargetCommand struct { // CommandID 是单个目标交易的幂等键;同一批量送礼必须为每个目标派生独立值。 CommandID string - // TargetUserID 是本次入账 GIFT_POINT 的用户。 + // TargetUserID 是本次收礼用户;GIFT_POINT 已下线,新送礼不会再给目标用户积分入账。 TargetUserID int64 // TargetIsHost 只能由 gateway 注入,批量目标之间不能共享该身份快照。 TargetIsHost bool @@ -157,11 +157,12 @@ type Receipt struct { CoinSpent int64 ChargeAssetType string ChargeAmount int64 - GiftPointAdded int64 - HeatValue int64 - GiftTypeCode string - PriceVersion string - BalanceAfter int64 + // GiftPointAdded 是历史回执字段,新送礼固定为 0;房间贡献和主播周期钻石只按真实扣费金额计算。 + GiftPointAdded int64 + HeatValue int64 + GiftTypeCode string + PriceVersion string + BalanceAfter int64 // HostPeriodDiamondAdded 是本次送礼写入主播工资周期账户的钻石数;非主播恒为 0。 HostPeriodDiamondAdded int64 // HostPeriodCycleKey 是工资周期键,当前按 UTC 月生成,后续结算按此键定位周期账户。 @@ -447,13 +448,14 @@ type GiftWallItem struct { TotalValue int64 TotalCoinValue int64 TotalDiamondValue int64 - TotalGiftPoint int64 - TotalHeatValue int64 - ChargeAssetType string - LastPriceVersion string - FirstReceivedAtMS int64 - LastReceivedAtMS int64 - SortOrder int32 + // TotalGiftPoint 是历史礼物墙字段,新送礼不再累加,保留用于旧数据展示兼容。 + TotalGiftPoint int64 + TotalHeatValue int64 + ChargeAssetType string + LastPriceVersion string + FirstReceivedAtMS int64 + LastReceivedAtMS int64 + SortOrder int32 } // UserGiftWall 汇总礼物墙首屏需要的全量统计和按礼物类型聚合列表。 @@ -464,8 +466,9 @@ type UserGiftWall struct { TotalValue int64 TotalCoinValue int64 TotalDiamondValue int64 - TotalGiftPoint int64 - TotalHeatValue int64 + // TotalGiftPoint 是历史礼物墙汇总字段,新送礼不再累加,保留用于旧数据展示兼容。 + TotalGiftPoint int64 + TotalHeatValue int64 } // RechargeProduct 是用户充值页读取的充值档位。 diff --git a/services/wallet-service/internal/domain/resource/resource.go b/services/wallet-service/internal/domain/resource/resource.go index d2472591..4304f712 100644 --- a/services/wallet-service/internal/domain/resource/resource.go +++ b/services/wallet-service/internal/domain/resource/resource.go @@ -79,18 +79,19 @@ const ( ) type Resource struct { - AppCode string - ResourceID int64 - ResourceCode string - ResourceType string - Name string - Status string - Grantable bool - GrantStrategy string - WalletAssetType string - WalletAssetAmount int64 - PriceType string - CoinPrice int64 + AppCode string + ResourceID int64 + ResourceCode string + ResourceType string + Name string + Status string + Grantable bool + GrantStrategy string + WalletAssetType string + WalletAssetAmount int64 + PriceType string + CoinPrice int64 + // GiftPointAmount 是历史字段;GIFT_POINT 已下线,新资源和礼物配置只保留真实 COIN 价格。 GiftPointAmount int64 UsageScopes []string AssetURL string @@ -146,18 +147,19 @@ type GiftConfig struct { PresentationJSON string PriceVersion string CoinPrice int64 - GiftPointAmount int64 - HeatValue int64 - GiftTypeCode string - ChargeAssetType string - EffectiveFromMS int64 - EffectiveToMS int64 - EffectTypes []string - CreatedByUserID int64 - UpdatedByUserID int64 - CreatedAtMS int64 - UpdatedAtMS int64 - RegionIDs []int64 + // GiftPointAmount 是历史字段;送礼结算不再读取,新配置固定为 0。 + GiftPointAmount int64 + HeatValue int64 + GiftTypeCode string + ChargeAssetType string + EffectiveFromMS int64 + EffectiveToMS int64 + EffectTypes []string + CreatedByUserID int64 + UpdatedByUserID int64 + CreatedAtMS int64 + UpdatedAtMS int64 + RegionIDs []int64 } type GiftTypeConfig struct { @@ -249,18 +251,19 @@ type ListResourcesQuery struct { } type ResourceCommand struct { - AppCode string - ResourceID int64 - ResourceCode string - ResourceType string - Name string - Status string - Grantable bool - GrantStrategy string - WalletAssetType string - WalletAssetAmount int64 - PriceType string - CoinPrice int64 + AppCode string + ResourceID int64 + ResourceCode string + ResourceType string + Name string + Status string + Grantable bool + GrantStrategy string + WalletAssetType string + WalletAssetAmount int64 + PriceType string + CoinPrice int64 + // GiftPointAmount 是历史字段;保存资源时固定为 0,保留字段只兼容旧表结构。 GiftPointAmount int64 UsageScopes []string AssetURL string @@ -315,6 +318,7 @@ type ListGiftConfigsQuery struct { AppCode string Status string Keyword string + GiftTypeCode string Page int32 PageSize int32 ActiveOnly bool @@ -338,16 +342,17 @@ type GiftConfigCommand struct { PresentationJSON string PriceVersion string CoinPrice int64 - GiftPointAmount int64 - HeatValue int64 - EffectiveAtMS int64 - GiftTypeCode string - ChargeAssetType string - EffectiveFromMS int64 - EffectiveToMS int64 - EffectTypes []string - OperatorUserID int64 - RegionIDs []int64 + // GiftPointAmount 是历史字段;保存礼物价格时固定为 0,业务计算只读 CoinPrice。 + GiftPointAmount int64 + HeatValue int64 + EffectiveAtMS int64 + GiftTypeCode string + ChargeAssetType string + EffectiveFromMS int64 + EffectiveToMS int64 + EffectTypes []string + OperatorUserID int64 + RegionIDs []int64 } type GiftTypeConfigCommand struct { diff --git a/services/wallet-service/internal/service/wallet/service_test.go b/services/wallet-service/internal/service/wallet/service_test.go index a32cc57a..7f7864d4 100644 --- a/services/wallet-service/internal/service/wallet/service_test.go +++ b/services/wallet-service/internal/service/wallet/service_test.go @@ -34,8 +34,8 @@ func walletOutboxMessageFromRecord(record mysqlstorage.WalletOutboxRecord) walle } } -// TestDebitGiftUsesServerPriceAndCreditsGiftPoint 验证送礼只信服务端价格并产生双边分录。 -func TestDebitGiftUsesServerPriceAndCreditsGiftPoint(t *testing.T) { +// TestDebitGiftUsesServerPriceWithoutGiftPointCredit 验证送礼只信服务端价格,且礼物积分下线后不再给收礼人入账。 +func TestDebitGiftUsesServerPriceWithoutGiftPointCredit(t *testing.T) { repository := mysqltest.NewRepository(t) repository.SetBalance(10001, 100) repository.SetGiftPrice("rose", "v9", 7, 3, 11) @@ -54,7 +54,7 @@ func TestDebitGiftUsesServerPriceAndCreditsGiftPoint(t *testing.T) { t.Fatalf("DebitGift failed: %v", err) } - if receipt.CoinSpent != 14 || receipt.GiftPointAdded != 6 || receipt.HeatValue != 14 || receipt.BalanceAfter != 86 || receipt.PriceVersion != "v9" { + if receipt.CoinSpent != 14 || receipt.GiftPointAdded != 0 || receipt.HeatValue != 14 || receipt.BalanceAfter != 86 || receipt.PriceVersion != "v9" { t.Fatalf("server price settlement mismatch: %+v", receipt) } if receipt.TransactionID == "" || receipt.BillingReceiptID == "" { @@ -72,14 +72,14 @@ func TestDebitGiftUsesServerPriceAndCreditsGiftPoint(t *testing.T) { if err != nil { t.Fatalf("GetBalances target failed: %v", err) } - if balanceAmount(balances, ledger.AssetGiftPoint) != 6 { - t.Fatalf("target GIFT_POINT balance mismatch: %+v", balances) + if balanceAmount(balances, ledger.AssetGiftPoint) != 0 { + t.Fatalf("target GIFT_POINT must stay zero after point removal: %+v", balances) } - if got := repository.CountRows("wallet_entries", "transaction_id = ?", receipt.TransactionID); got != 2 { - t.Fatalf("gift debit should write two entries, got %d", got) + if got := repository.CountRows("wallet_entries", "transaction_id = ?", receipt.TransactionID); got != 1 { + t.Fatalf("gift debit should only write sender COIN entry, got %d", got) } - if got := repository.CountRows("wallet_outbox", "transaction_id = ?", receipt.TransactionID); got != 3 { - t.Fatalf("gift debit should write balance and gift outbox events, got %d", got) + if got := repository.CountRows("wallet_outbox", "transaction_id = ?", receipt.TransactionID); got != 2 { + t.Fatalf("gift debit should write sender balance and gift outbox events, got %d", got) } if receipt.HostPeriodDiamondAdded != 0 || receipt.HostPeriodCycleKey != "" { t.Fatalf("non-host gift must not credit host period diamonds: %+v", receipt) @@ -113,7 +113,7 @@ func TestBatchDebitGiftSettlesAllTargetsAtomically(t *testing.T) { t.Fatalf("BatchDebitGift failed: %v", err) } - if receipt.Aggregate.CoinSpent != 28 || receipt.Aggregate.GiftPointAdded != 12 || receipt.Aggregate.HeatValue != 28 || receipt.Aggregate.BalanceAfter != 72 || len(receipt.Targets) != 2 { + if receipt.Aggregate.CoinSpent != 28 || receipt.Aggregate.GiftPointAdded != 0 || receipt.Aggregate.HeatValue != 28 || receipt.Aggregate.BalanceAfter != 72 || len(receipt.Targets) != 2 { t.Fatalf("batch aggregate mismatch: %+v", receipt) } for _, userID := range []int64{10002, 10003} { @@ -121,8 +121,8 @@ func TestBatchDebitGiftSettlesAllTargetsAtomically(t *testing.T) { if err != nil { t.Fatalf("GetBalances target %d failed: %v", userID, err) } - if balanceAmount(balances, ledger.AssetGiftPoint) != 6 { - t.Fatalf("target %d GIFT_POINT mismatch: %+v", userID, balances) + if balanceAmount(balances, ledger.AssetGiftPoint) != 0 { + t.Fatalf("target %d GIFT_POINT must stay zero after point removal: %+v", userID, balances) } } balances, err := svc.GetBalances(context.Background(), 10001, []string{ledger.AssetCoin}) @@ -135,8 +135,8 @@ func TestBatchDebitGiftSettlesAllTargetsAtomically(t *testing.T) { if got := repository.CountRows("wallet_transactions", "command_id IN (?, ?)", "cmd-gift-batch:target:10002", "cmd-gift-batch:target:10003"); got != 2 { t.Fatalf("batch gift should write one transaction per target, got %d", got) } - if got := repository.CountRows("wallet_entries", "transaction_id IN (?, ?)", receipt.Targets[0].Receipt.TransactionID, receipt.Targets[1].Receipt.TransactionID); got != 4 { - t.Fatalf("batch gift should write sender and target entries per target, got %d", got) + if got := repository.CountRows("wallet_entries", "transaction_id IN (?, ?)", receipt.Targets[0].Receipt.TransactionID, receipt.Targets[1].Receipt.TransactionID); got != 2 { + t.Fatalf("batch gift should only write sender COIN entries per target, got %d", got) } again, err := svc.BatchDebitGift(context.Background(), ledger.BatchDebitGiftCommand{ @@ -303,7 +303,7 @@ func TestDebitGiftAppliesGlobalDefaultGiftDiamondRatioToHeatValueByGiftType(t *t if err != nil { t.Fatalf("DebitGift %s failed: %v", tc.giftType, err) } - if receipt.HeatValue != tc.wantHeat || receipt.GiftPointAdded != 100 || receipt.CoinSpent != tc.coinPrice { + if receipt.HeatValue != tc.wantHeat || receipt.GiftPointAdded != 0 || receipt.CoinSpent != tc.coinPrice { t.Fatalf("%s room contribution ratio mismatch: %+v want heat %d", tc.giftType, receipt, tc.wantHeat) } } @@ -338,7 +338,7 @@ func TestBatchDebitGiftAppliesGlobalDefaultGiftDiamondRatioToEachTargetHeatValue t.Fatalf("batch aggregate room contribution ratio mismatch: %+v", receipt) } for _, target := range receipt.Targets { - if target.Receipt.HeatValue != 10 || target.Receipt.GiftPointAdded != 100 { + if target.Receipt.HeatValue != 10 || target.Receipt.GiftPointAdded != 0 { t.Fatalf("target room contribution ratio mismatch: %+v", target) } } @@ -1003,7 +1003,7 @@ func TestHostSalaryHalfMonthSettlementUsesPolicyMode(t *testing.T) { } } -// TestDebitGiftAllowsSelfGift 验证用户可以给自己送礼,扣 COIN 和收 GIFT_POINT 仍然分别落账。 +// TestDebitGiftAllowsSelfGift 验证用户可以给自己送礼;礼物积分下线后只扣 COIN,不再回加 GIFT_POINT。 func TestDebitGiftAllowsSelfGift(t *testing.T) { repository := mysqltest.NewRepository(t) repository.SetBalance(10001, 100) @@ -1023,18 +1023,18 @@ func TestDebitGiftAllowsSelfGift(t *testing.T) { t.Fatalf("self DebitGift failed: %v", err) } - if receipt.CoinSpent != 14 || receipt.GiftPointAdded != 6 || receipt.BalanceAfter != 86 { + if receipt.CoinSpent != 14 || receipt.GiftPointAdded != 0 || receipt.BalanceAfter != 86 { t.Fatalf("self gift settlement mismatch: %+v", receipt) } balances, err := svc.GetBalances(context.Background(), 10001, []string{ledger.AssetCoin, ledger.AssetGiftPoint}) if err != nil { t.Fatalf("GetBalances failed: %v", err) } - if balanceAmount(balances, ledger.AssetCoin) != 86 || balanceAmount(balances, ledger.AssetGiftPoint) != 6 { + if balanceAmount(balances, ledger.AssetCoin) != 86 || balanceAmount(balances, ledger.AssetGiftPoint) != 0 { t.Fatalf("self gift balances mismatch: %+v", balances) } - if got := repository.CountRows("wallet_entries", "transaction_id = ?", receipt.TransactionID); got != 2 { - t.Fatalf("self gift should still write debit and credit entries, got %d", got) + if got := repository.CountRows("wallet_entries", "transaction_id = ?", receipt.TransactionID); got != 1 { + t.Fatalf("self gift should only write debit entry, got %d", got) } } @@ -2159,7 +2159,7 @@ func TestGiftConfigMustSelectGiftResource(t *testing.T) { if err != nil { t.Fatalf("DebitGift with resource-backed gift failed: %v", err) } - if receipt.CoinSpent != 10 || receipt.GiftPointAdded != 4 || receipt.HeatValue != 10 { + if receipt.CoinSpent != 10 || receipt.GiftPointAdded != 0 || receipt.HeatValue != 10 { t.Fatalf("resource-backed gift settlement mismatch: %+v", receipt) } } @@ -2244,8 +2244,8 @@ func TestGiftConfigSyncsConfiguredResourcePrice(t *testing.T) { if err != nil { t.Fatalf("create priced gift resource failed: %v", err) } - if paidResource.GiftPointAmount != 77 { - t.Fatalf("resource gift point should sync coin price: %+v", paidResource) + if paidResource.GiftPointAmount != 0 { + t.Fatalf("resource gift point should stay disabled: %+v", paidResource) } gift, err := svc.CreateGiftConfig(ctx, resourcedomain.GiftConfigCommand{ GiftID: "price-sync", @@ -2264,7 +2264,7 @@ func TestGiftConfigSyncsConfiguredResourcePrice(t *testing.T) { if err != nil { t.Fatalf("create synced gift config failed: %v", err) } - if gift.ChargeAssetType != ledger.AssetCoin || gift.CoinPrice != 77 || gift.GiftPointAmount != 77 { + if gift.ChargeAssetType != ledger.AssetCoin || gift.CoinPrice != 77 || gift.GiftPointAmount != 0 { t.Fatalf("gift price should be overridden by resource price: %+v", gift) } updatedResource, err := svc.UpdateResource(ctx, resourcedomain.ResourceCommand{ @@ -2283,7 +2283,7 @@ func TestGiftConfigSyncsConfiguredResourcePrice(t *testing.T) { if err != nil { t.Fatalf("update priced gift resource failed: %v", err) } - if updatedResource.CoinPrice != 88 || updatedResource.GiftPointAmount != 88 { + if updatedResource.CoinPrice != 88 || updatedResource.GiftPointAmount != 0 { t.Fatalf("updated resource price mismatch: %+v", updatedResource) } repository.SetBalance(54001, 100) @@ -2298,7 +2298,7 @@ func TestGiftConfigSyncsConfiguredResourcePrice(t *testing.T) { if err != nil { t.Fatalf("debit gift after resource price update failed: %v", err) } - if updatedReceipt.CoinSpent != 88 || updatedReceipt.GiftPointAdded != 88 { + if updatedReceipt.CoinSpent != 88 || updatedReceipt.GiftPointAdded != 0 { t.Fatalf("gift price should follow updated resource price: %+v", updatedReceipt) } diff --git a/services/wallet-service/internal/storage/mysql/repository.go b/services/wallet-service/internal/storage/mysql/repository.go index 95a24148..95c7fd16 100644 --- a/services/wallet-service/internal/storage/mysql/repository.go +++ b/services/wallet-service/internal/storage/mysql/repository.go @@ -173,7 +173,7 @@ func (r *Repository) Ping(ctx context.Context) error { return r.db.PingContext(ctx) } -// DebitGift 在一个 MySQL 事务内完成 sender 扣费、target GIFT_POINT 入账、主播周期钻石入账、分录和 outbox。 +// DebitGift 在一个 MySQL 事务内完成 sender 扣费、主播周期钻石入账、分录和 outbox;历史 GIFT_POINT 回执字段保留但不再入账。 func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error) { if r == nil || r.db == nil { return ledger.Receipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") @@ -218,10 +218,7 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm if err != nil { return ledger.Receipt{}, err } - giftPointAdded, err := checkedMul(price.GiftPointAmount, int64(command.GiftCount)) - if err != nil { - return ledger.Receipt{}, err - } + // 礼物积分已经从运营配置和收礼收益里下线;回执字段只保留 0,兼容旧客户端和历史事件解析。 roomContributionRatio, roomContributionRatioRegionID, err := r.resolveGlobalGiftDiamondRatio(ctx, tx, command.AppCode, giftConfig.GiftTypeCode) if err != nil { return ledger.Receipt{}, err @@ -240,7 +237,7 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm if err != nil { return ledger.Receipt{}, err } - // 非主播不会进入该分支,因此普通用户收礼只增加 GIFT_POINT,不产生工资周期积分。 + // 非主播不会进入该分支,因此普通用户收礼不会产生工资周期钻石。 hostPeriodDiamondAdded, err = giftDiamondAmount(chargeAmount, ratio.PPM) if err != nil { return ledger.Receipt{}, err @@ -257,11 +254,6 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm if sender.AvailableAmount < chargeAmount { return ledger.Receipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance") } - target, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetGiftPoint, true, nowMs) - if err != nil { - return ledger.Receipt{}, err - } - transactionID := transactionID(command.AppCode, command.CommandID) metadata := giftMetadata{ AppCode: command.AppCode, @@ -277,7 +269,7 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm ChargeAssetType: price.ChargeAssetType, ChargeAmount: chargeAmount, CoinSpent: chargeAmount, - GiftPointAdded: giftPointAdded, + GiftPointAdded: 0, HeatValue: heatValue, BalanceAfter: sender.AvailableAmount - chargeAmount, BillingReceipt: billingReceiptID(command.AppCode, command.CommandID), @@ -296,7 +288,7 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm RoomContributionRatioPercent: roomContributionRatio.Percent, RoomContributionRatioRegionID: roomContributionRatioRegionID, CoinPrice: price.CoinPrice, - GiftPointAmount: price.GiftPointAmount, + GiftPointAmount: 0, HeatUnitValue: price.CoinPrice, } if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeGiftDebit, requestHash, fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil { @@ -320,26 +312,8 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm }); err != nil { return ledger.Receipt{}, err } - if err := r.applyAccountDelta(ctx, tx, target, giftPointAdded, 0, nowMs); err != nil { - return ledger.Receipt{}, err - } - targetAfter := target.AvailableAmount + giftPointAdded - if err := r.insertEntry(ctx, tx, walletEntry{ - TransactionID: transactionID, - UserID: command.TargetUserID, - AssetType: ledger.AssetGiftPoint, - AvailableDelta: giftPointAdded, - FrozenDelta: 0, - AvailableAfter: targetAfter, - FrozenAfter: target.FrozenAmount, - CounterpartyUserID: command.SenderUserID, - RoomID: command.RoomID, - CreatedAtMS: nowMs, - }); err != nil { - return ledger.Receipt{}, err - } if hostPeriodDiamondAdded > 0 { - // 周期钻石与送礼扣费同事务提交;如果后续任一步失败,用户扣币、主播礼物积分和工资积分会一起回滚。 + // 周期钻石与送礼扣费同事务提交;如果后续任一步失败,用户扣币和主播工资周期钻石会一起回滚。 hostPeriodDiamondAfter, hostPeriodDiamondVersion, err := r.creditHostPeriodDiamonds(ctx, tx, transactionID, command.CommandID, metadata, nowMs) if err != nil { return ledger.Receipt{}, err @@ -350,7 +324,6 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm events := []walletOutboxEvent{ balanceChangedEvent(transactionID, command.CommandID, command.SenderUserID, price.ChargeAssetType, -chargeAmount, 0, senderAfter, sender.FrozenAmount, sender.Version+1, metadata, nowMs), - balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetGiftPoint, giftPointAdded, 0, targetAfter, target.FrozenAmount, target.Version+1, metadata, nowMs), giftDebitedEvent(transactionID, command.CommandID, command.SenderUserID, price.ChargeAssetType, -chargeAmount, 0, metadata, nowMs), } if hostPeriodDiamondAdded > 0 { @@ -402,10 +375,7 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb if err != nil { return ledger.BatchGiftReceipt{}, err } - giftPointAdded, err := checkedMul(price.GiftPointAmount, int64(command.GiftCount)) - if err != nil { - return ledger.BatchGiftReceipt{}, err - } + // 礼物积分已经下线;批量回执仍保留字段,但每个目标都固定返回 0。 roomContributionRatio, roomContributionRatioRegionID, err := r.resolveGlobalGiftDiamondRatio(ctx, tx, command.AppCode, giftConfig.GiftTypeCode) if err != nil { return ledger.BatchGiftReceipt{}, err @@ -466,11 +436,6 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb return ledger.BatchGiftReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance") } - targetAccounts, err := r.lockGiftTargetAccounts(ctx, tx, command.Targets, nowMs) - if err != nil { - return ledger.BatchGiftReceipt{}, err - } - anyHostTarget := false for _, target := range command.Targets { if target.TargetIsHost { @@ -487,7 +452,7 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb } } - events := make([]walletOutboxEvent, 0, len(command.Targets)*4) + events := make([]walletOutboxEvent, 0, len(command.Targets)*3) targetReceipts := make([]ledger.BatchGiftTargetReceipt, 0, len(command.Targets)) for _, target := range command.Targets { hostPeriodDiamondAdded := int64(0) @@ -521,7 +486,7 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb ChargeAssetType: price.ChargeAssetType, ChargeAmount: chargeAmount, CoinSpent: chargeAmount, - GiftPointAdded: giftPointAdded, + GiftPointAdded: 0, HeatValue: heatValue, BalanceAfter: senderAfter, BillingReceipt: billingReceiptID(command.AppCode, target.CommandID), @@ -540,7 +505,7 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb RoomContributionRatioPercent: roomContributionRatio.Percent, RoomContributionRatioRegionID: roomContributionRatioRegionID, CoinPrice: price.CoinPrice, - GiftPointAmount: price.GiftPointAmount, + GiftPointAmount: 0, HeatUnitValue: price.CoinPrice, } if err := r.insertTransaction(ctx, tx, transactionID, target.CommandID, bizTypeGiftDebit, debitRequestHash(debitGiftCommandFromBatchTarget(command, target)), fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil { @@ -553,16 +518,6 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb sender.AvailableAmount = senderAfter sender.Version++ - targetAccount := targetAccounts[target.TargetUserID] - if err := r.applyAccountDelta(ctx, tx, targetAccount, giftPointAdded, 0, nowMs); err != nil { - return ledger.BatchGiftReceipt{}, err - } - targetAfter := targetAccount.AvailableAmount + giftPointAdded - events = append(events, balanceChangedEvent(transactionID, target.CommandID, target.TargetUserID, ledger.AssetGiftPoint, giftPointAdded, 0, targetAfter, targetAccount.FrozenAmount, targetAccount.Version+1, metadata, nowMs)) - targetAccount.AvailableAmount = targetAfter - targetAccount.Version++ - targetAccounts[target.TargetUserID] = targetAccount - if err := r.insertEntry(ctx, tx, walletEntry{ TransactionID: transactionID, UserID: command.SenderUserID, @@ -577,20 +532,6 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb }); err != nil { return ledger.BatchGiftReceipt{}, err } - if err := r.insertEntry(ctx, tx, walletEntry{ - TransactionID: transactionID, - UserID: target.TargetUserID, - AssetType: ledger.AssetGiftPoint, - AvailableDelta: giftPointAdded, - FrozenDelta: 0, - AvailableAfter: targetAfter, - FrozenAfter: targetAccount.FrozenAmount, - CounterpartyUserID: command.SenderUserID, - RoomID: command.RoomID, - CreatedAtMS: nowMs, - }); err != nil { - return ledger.BatchGiftReceipt{}, err - } if hostPeriodDiamondAdded > 0 { hostPeriodDiamondAfter, hostPeriodDiamondVersion, err := r.creditHostPeriodDiamonds(ctx, tx, transactionID, target.CommandID, metadata, nowMs) if err != nil { @@ -742,7 +683,7 @@ func (r *Repository) upsertUserGiftWall(ctx context.Context, tx *sql.Tx, metadat chargeAmount, totalCoinValue, totalDiamondValue, - metadata.GiftPointAdded, + 0, metadata.HeatValue, chargeAssetType, metadata.PriceVersion, @@ -1675,6 +1616,7 @@ type giftPrice struct { PriceVersion string ChargeAssetType string CoinPrice int64 + // GiftPointAmount 是 wallet_gift_prices 的历史列;送礼结算只按 CoinPrice 和比例计算,不再读取它做收益。 GiftPointAmount int64 HeatValue int64 } @@ -1716,7 +1658,7 @@ func (r *Repository) resolveGiftPrice(ctx context.Context, tx *sql.Tx, giftID st } return giftPrice{}, err } - if price.CoinPrice < 0 || price.GiftPointAmount < 0 || price.HeatValue < 0 { + if price.CoinPrice < 0 || price.HeatValue < 0 { return giftPrice{}, xerr.New(xerr.Internal, "gift price is invalid") } price.ChargeAssetType = ledger.NormalizeGiftChargeAssetType(price.ChargeAssetType) @@ -2537,22 +2479,24 @@ func (r *Repository) balanceAfterTransaction(ctx context.Context, tx *sql.Tx, tr } type giftMetadata struct { - AppCode string `json:"app_code"` - GiftID string `json:"gift_id"` - GiftName string `json:"gift_name"` - ResourceID int64 `json:"resource_id"` - ResourceSnapshot string `json:"resource_snapshot_json"` - GiftTypeCode string `json:"gift_type_code"` - PresentationJSON string `json:"presentation_json"` - SortOrder int32 `json:"sort_order"` - GiftCount int32 `json:"gift_count"` - PriceVersion string `json:"price_version"` - CoinPrice int64 `json:"coin_price"` - GiftPointAmount int64 `json:"gift_point_amount"` - HeatUnitValue int64 `json:"heat_unit_value"` - ChargeAssetType string `json:"charge_asset_type"` - ChargeAmount int64 `json:"charge_amount"` - CoinSpent int64 `json:"coin_spent"` + AppCode string `json:"app_code"` + GiftID string `json:"gift_id"` + GiftName string `json:"gift_name"` + ResourceID int64 `json:"resource_id"` + ResourceSnapshot string `json:"resource_snapshot_json"` + GiftTypeCode string `json:"gift_type_code"` + PresentationJSON string `json:"presentation_json"` + SortOrder int32 `json:"sort_order"` + GiftCount int32 `json:"gift_count"` + PriceVersion string `json:"price_version"` + CoinPrice int64 `json:"coin_price"` + // GiftPointAmount 是历史礼物积分单价字段,新送礼固定写 0,业务统计只读真实扣费和热度。 + GiftPointAmount int64 `json:"gift_point_amount"` + HeatUnitValue int64 `json:"heat_unit_value"` + ChargeAssetType string `json:"charge_asset_type"` + ChargeAmount int64 `json:"charge_amount"` + CoinSpent int64 `json:"coin_spent"` + // GiftPointAdded 是历史收礼积分字段,新送礼固定写 0,保留 JSON 字段只为旧事件解析兼容。 GiftPointAdded int64 `json:"gift_point_added"` HeatValue int64 `json:"heat_value"` BalanceAfter int64 `json:"balance_after"` @@ -2711,7 +2655,7 @@ func receiptFromGiftMetadata(transactionID string, metadata giftMetadata) ledger CoinSpent: metadata.CoinSpent, ChargeAssetType: chargeAssetType, ChargeAmount: chargeAmount, - GiftPointAdded: metadata.GiftPointAdded, + GiftPointAdded: 0, HeatValue: metadata.HeatValue, GiftTypeCode: metadata.GiftTypeCode, PriceVersion: metadata.PriceVersion, @@ -3207,31 +3151,6 @@ func debitGiftCommandFromBatchTarget(command ledger.BatchDebitGiftCommand, targe } } -func (r *Repository) lockGiftTargetAccounts(ctx context.Context, tx *sql.Tx, targets []ledger.DebitGiftTargetCommand, nowMs int64) (map[int64]walletAccount, error) { - ids := make([]int64, 0, len(targets)) - seen := make(map[int64]struct{}, len(targets)) - for _, target := range targets { - if _, exists := seen[target.TargetUserID]; exists { - continue - } - seen[target.TargetUserID] = struct{}{} - ids = append(ids, target.TargetUserID) - } - sort.Slice(ids, func(left, right int) bool { - return ids[left] < ids[right] - }) - accounts := make(map[int64]walletAccount, len(ids)) - for _, targetUserID := range ids { - // 固定按 user_id 升序锁目标账户,降低并发批量送礼互相等待时形成死锁的概率。 - account, err := r.lockAccount(ctx, tx, targetUserID, ledger.AssetGiftPoint, true, nowMs) - if err != nil { - return nil, err - } - accounts[targetUserID] = account - } - return accounts, nil -} - func aggregateGiftReceipts(targets []ledger.BatchGiftTargetReceipt) (ledger.Receipt, error) { aggregate := ledger.Receipt{} billingReceiptIDs := make([]string, 0, len(targets)) @@ -3251,9 +3170,6 @@ func aggregateGiftReceipts(targets []ledger.BatchGiftTargetReceipt) (ledger.Rece if aggregate.ChargeAmount, err = checkedAdd(aggregate.ChargeAmount, receipt.ChargeAmount); err != nil { return ledger.Receipt{}, err } - if aggregate.GiftPointAdded, err = checkedAdd(aggregate.GiftPointAdded, receipt.GiftPointAdded); err != nil { - return ledger.Receipt{}, err - } if aggregate.HeatValue, err = checkedAdd(aggregate.HeatValue, receipt.HeatValue); err != nil { return ledger.Receipt{}, err } diff --git a/services/wallet-service/internal/storage/mysql/resource_repository.go b/services/wallet-service/internal/storage/mysql/resource_repository.go index f23a2355..521972b1 100644 --- a/services/wallet-service/internal/storage/mysql/resource_repository.go +++ b/services/wallet-service/internal/storage/mysql/resource_repository.go @@ -292,11 +292,11 @@ func (r *Repository) syncGiftPricesForResourceTx(ctx context.Context, tx *sql.Tx } coinPrice := int64(0) - giftPointAmount := int64(0) if priceType == resourcedomain.PriceTypeCoin { coinPrice = resource.CoinPrice - giftPointAmount = resource.GiftPointAmount } + // gift_point_amount 是历史字段,资源改价同步礼物价格时固定写 0,避免旧积分口径重新进入送礼链路。 + giftPointAmount := int64(0) if _, err := tx.ExecContext(ctx, ` UPDATE wallet_gift_prices gp JOIN gift_configs gc ON gc.app_code = gp.app_code AND gc.gift_id = gp.gift_id @@ -627,7 +627,7 @@ func (r *Repository) ListGiftConfigs(ctx context.Context, query resourcedomain.L item.PriceVersion = price.PriceVersion item.ChargeAssetType = price.ChargeAssetType item.CoinPrice = price.CoinPrice - item.GiftPointAmount = price.GiftPointAmount + item.GiftPointAmount = 0 item.HeatValue = price.CoinPrice } items = append(items, item) @@ -2006,7 +2006,8 @@ func (r *Repository) getGiftConfig(ctx context.Context, giftID string) (resource gift.PriceVersion = price.PriceVersion gift.ChargeAssetType = price.ChargeAssetType gift.CoinPrice = price.CoinPrice - gift.GiftPointAmount = price.GiftPointAmount + // gift_point_amount 是历史列,后台和送礼主链路只暴露真实 COIN 价格。 + gift.GiftPointAmount = 0 // heat_value 是旧配置列,后台不再读写;对兼容调用方只回真实价格,房间贡献也按真实扣费结算。 gift.HeatValue = price.CoinPrice } @@ -2837,6 +2838,10 @@ func giftConfigWhereSQL(query resourcedomain.ListGiftConfigsQuery) (string, []an where += ` AND (gc.gift_id LIKE ? OR gc.name LIKE ? OR r.resource_code LIKE ?)` args = append(args, like, like, like) } + if query.GiftTypeCode != "" { + where += ` AND gc.gift_type_code = ?` + args = append(args, query.GiftTypeCode) + } if query.FilterRegion { where += ` AND EXISTS ( SELECT 1 @@ -2908,6 +2913,7 @@ func normalizeResourceGroupListQuery(query resourcedomain.ListResourceGroupsQuer func normalizeGiftConfigListQuery(query resourcedomain.ListGiftConfigsQuery) resourcedomain.ListGiftConfigsQuery { query.Status = strings.ToLower(strings.TrimSpace(query.Status)) query.Keyword = strings.TrimSpace(query.Keyword) + query.GiftTypeCode = resourcedomain.NormalizeGiftTypeCode(query.GiftTypeCode) query.Page, query.PageSize = normalizePage(query.Page, query.PageSize) return query } @@ -3082,7 +3088,8 @@ func normalizeResourceCommand(command resourcedomain.ResourceCommand) resourcedo command.CoinPrice = 0 command.GiftPointAmount = 0 } else if command.PriceType == resourcedomain.PriceTypeCoin { - command.GiftPointAmount = command.CoinPrice + // 资源价格只保留真实 COIN 价格;gift_point_amount 是历史字段,写入 0 兼容旧表结构。 + command.GiftPointAmount = 0 } command.AssetURL = strings.TrimSpace(command.AssetURL) command.PreviewURL = strings.TrimSpace(command.PreviewURL) @@ -3124,7 +3131,7 @@ func validateResourceCommand(command resourcedomain.ResourceCommand) error { if command.PriceType == resourcedomain.PriceTypeCoin && command.CoinPrice <= 0 { return xerr.New(xerr.InvalidArgument, "resource coin price is invalid") } - if command.CoinPrice < 0 || command.GiftPointAmount < 0 || command.GiftPointAmount != command.CoinPrice { + if command.CoinPrice < 0 { return xerr.New(xerr.InvalidArgument, "resource price is invalid") } return nil @@ -3265,7 +3272,7 @@ func applyResourcePricingToGiftCommand(command resourcedomain.GiftConfigCommand, case resourcedomain.PriceTypeCoin: command.ChargeAssetType = ledger.AssetCoin command.CoinPrice = resource.CoinPrice - command.GiftPointAmount = resource.GiftPointAmount + command.GiftPointAmount = 0 case resourcedomain.PriceTypeFree: command.ChargeAssetType = ledger.AssetCoin command.CoinPrice = 0 @@ -3289,6 +3296,8 @@ func normalizeGiftConfigCommand(command resourcedomain.GiftConfigCommand) resour command.ChargeAssetType = ledger.AssetCoin } command.EffectTypes = normalizeGiftEffectTypes(command.EffectTypes) + // gift_point_amount 是历史字段,礼物配置保存时固定归零,送礼结算只读取真实 COIN 价格。 + command.GiftPointAmount = 0 if command.EffectiveAtMS < 0 { command.EffectiveAtMS = 0 } @@ -3316,7 +3325,7 @@ func validateGiftConfigCommand(command resourcedomain.GiftConfigCommand) error { if !resourcedomain.ValidStatus(command.Status) { return xerr.New(xerr.InvalidArgument, "status is invalid") } - if command.CoinPrice < 0 || command.GiftPointAmount < 0 || command.HeatValue < 0 { + if command.CoinPrice < 0 || command.HeatValue < 0 { return xerr.New(xerr.InvalidArgument, "gift price is invalid") } if !resourcedomain.ValidGiftTypeCode(command.GiftTypeCode) { diff --git a/services/wallet-service/internal/transport/grpc/resource.go b/services/wallet-service/internal/transport/grpc/resource.go index a5c3466f..48037716 100644 --- a/services/wallet-service/internal/transport/grpc/resource.go +++ b/services/wallet-service/internal/transport/grpc/resource.go @@ -128,6 +128,7 @@ func (s *Server) ListGiftConfigs(ctx context.Context, req *walletv1.ListGiftConf AppCode: req.GetAppCode(), Status: req.GetStatus(), Keyword: req.GetKeyword(), + GiftTypeCode: req.GetGiftTypeCode(), Page: req.GetPage(), PageSize: req.GetPageSize(), ActiveOnly: req.GetActiveOnly(), @@ -401,7 +402,7 @@ func resourceCommandFromCreate(req *walletv1.CreateResourceRequest) resourcedoma WalletAssetAmount: req.GetWalletAssetAmount(), PriceType: req.GetPriceType(), CoinPrice: req.GetCoinPrice(), - GiftPointAmount: req.GetGiftPointAmount(), + GiftPointAmount: 0, UsageScopes: req.GetUsageScopes(), AssetURL: req.GetAssetUrl(), PreviewURL: req.GetPreviewUrl(), @@ -426,7 +427,7 @@ func resourceCommandFromUpdate(req *walletv1.UpdateResourceRequest) resourcedoma WalletAssetAmount: req.GetWalletAssetAmount(), PriceType: req.GetPriceType(), CoinPrice: req.GetCoinPrice(), - GiftPointAmount: req.GetGiftPointAmount(), + GiftPointAmount: 0, UsageScopes: req.GetUsageScopes(), AssetUrl: req.GetAssetUrl(), PreviewUrl: req.GetPreviewUrl(), @@ -478,7 +479,7 @@ func giftConfigCommandFromCreate(req *walletv1.CreateGiftConfigRequest) resource PresentationJSON: req.GetPresentationJson(), PriceVersion: req.GetPriceVersion(), CoinPrice: req.GetCoinPrice(), - GiftPointAmount: req.GetGiftPointAmount(), + GiftPointAmount: 0, HeatValue: req.GetHeatValue(), EffectiveAtMS: req.GetEffectiveAtMs(), GiftTypeCode: req.GetGiftTypeCode(), @@ -502,7 +503,7 @@ func giftConfigCommandFromUpdate(req *walletv1.UpdateGiftConfigRequest) resource PresentationJSON: req.GetPresentationJson(), PriceVersion: req.GetPriceVersion(), CoinPrice: req.GetCoinPrice(), - GiftPointAmount: req.GetGiftPointAmount(), + GiftPointAmount: 0, HeatValue: req.GetHeatValue(), EffectiveAtMS: req.GetEffectiveAtMs(), GiftTypeCode: req.GetGiftTypeCode(), @@ -567,7 +568,7 @@ func resourceToProto(resource resourcedomain.Resource) *walletv1.Resource { WalletAssetAmount: resource.WalletAssetAmount, PriceType: resource.PriceType, CoinPrice: resource.CoinPrice, - GiftPointAmount: resource.GiftPointAmount, + GiftPointAmount: 0, UsageScopes: resource.UsageScopes, AssetUrl: resource.AssetURL, PreviewUrl: resource.PreviewURL, @@ -669,7 +670,7 @@ func giftConfigToProto(gift resourcedomain.GiftConfig) *walletv1.GiftConfig { PriceVersion: gift.PriceVersion, ChargeAssetType: gift.ChargeAssetType, CoinPrice: gift.CoinPrice, - GiftPointAmount: gift.GiftPointAmount, + GiftPointAmount: 0, HeatValue: gift.HeatValue, GiftTypeCode: gift.GiftTypeCode, EffectiveFromMs: gift.EffectiveFromMS,