Update wallet resources and admin host flows

This commit is contained in:
zhx 2026-06-05 14:58:40 +08:00
parent 6de77ee02d
commit 427d335e4a
27 changed files with 537 additions and 382 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,8 @@ type listQuery struct {
AgencyID int64
ParentBDUserID int64
ParentLeaderUserID int64
SortBy string
SortDirection string
}
type createBDLeaderRequest struct {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -254,7 +254,7 @@ INSERT IGNORE INTO room_treasure_configs (
'lalu',
0,
1,
'gift_point_added',
'heat_value',
30000,
1,
'region',

View File

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

View File

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

View File

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

View File

@ -291,7 +291,7 @@ func (r *Repository) Migrate(ctx context.Context) error {
'lalu',
0,
1,
'gift_point_added',
'heat_value',
30000,
1,
'region',

View File

@ -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 '最后价格版本',

View File

@ -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 是用户充值页读取的充值档位。

View File

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

View File

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

View File

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

View File

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

View File

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