feat: 经济系统重构 - 任务服务与画廊服务解耦

- galleryService 独立启动,配置 task-service-url
- 新增 taskService 到 galleryService 的 RPC 调用
- 更新 proto 文件并重新生成代码
- 新增展览唯一约束迁移脚本
- 修复多个 service 的配置和依赖关系

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
zerosaturation 2026-05-15 23:06:19 +08:00
parent 2c2e707971
commit c5bf9df955
27 changed files with 780 additions and 214 deletions

View File

@ -339,7 +339,12 @@ echo -e "${YELLOW}🚀 启动所有服务...${NC}"
start_service "userService" "services/userService/userService" 20000 1
start_service "assetService" "services/assetService/assetService" 20003 1
start_service "socialService" "services/socialService/socialService" 20002 1
start_service "galleryService" "services/galleryService/galleryService" 20004 1
# galleryService 需要连接 taskService (20006),单独处理
echo -e "${GREEN}🚀 启动 galleryService...${NC}"
"$SCRIPT_DIR/services/galleryService/galleryService" -port=20004 -task-service-url="tri://localhost:20006" "${DB_ARGS[@]}" > "/tmp/galleryService.log" 2>&1 &
echo $! > "/tmp/dev_sh_galleryService.pid"
sleep 2
echo -e "${GREEN}✅ galleryService 已启动 (PID: $(cat /tmp/dev_sh_galleryService.pid), 端口: 20004)${NC}"
start_service "activityService" "services/activityService/activityService" 20005 1
start_service "taskService" "services/taskService/taskService" 20006 1
start_service "starbookService" "services/starbookService/starbookService" 20007 1
@ -352,7 +357,6 @@ start_watcher "gateway" "gateway" "gateway/gateway" 8
start_watcher "userService" "services/userService" "services/userService/userService" 20000 1
start_watcher "assetService" "services/assetService" "services/assetService/assetService" 20003 1
start_watcher "socialService" "services/socialService" "services/socialService/socialService" 20002 1
start_watcher "galleryService" "services/galleryService" "services/galleryService/galleryService" 20004 1
start_watcher "activityService" "services/activityService" "services/activityService/activityService" 20005 1
start_watcher "taskService" "services/taskService" "services/taskService/taskService" 20006 1
start_watcher "starbookService" "services/starbookService" "services/starbookService/starbookService" 20007 1

View File

@ -602,7 +602,9 @@ func (ctrl *TaskController) ClaimExhibitionRevenue(c *gin.Context) {
}
response.Success(c, map[string]interface{}{
"success": resp.Success,
"success": resp.Success,
"crystal_amount": resp.CrystalAmount,
"total_balance": resp.TotalBalance,
})
}

View File

@ -71,20 +71,22 @@ func ConvertMintOrder(pbOrder *pbAsset.MintOrder) MintOrderDTO {
// 注意:预签名 URL 需要在 Controller 层生成,因为需要访问 OSS 配置
func ConvertAsset(pbAsset *pbAsset.Asset) AssetDTO {
dto := AssetDTO{
AssetID: pbAsset.AssetId,
OwnerUID: pbAsset.OwnerUid,
OwnerNickname: pbAsset.OwnerNickname,
StarID: pbAsset.StarId,
Name: pbAsset.Name,
CoverURL: pbAsset.CoverUrl,
Visibility: pbAsset.Visibility,
Status: pbAsset.Status,
LikeCount: pbAsset.LikeCount,
CreatedAt: pbAsset.CreatedAt,
UpdatedAt: pbAsset.UpdatedAt,
IsLiked: pbAsset.IsLiked,
Info: pbAsset.Info,
DisplayStatus: pbAsset.DisplayStatus,
AssetID: pbAsset.AssetId,
OwnerUID: pbAsset.OwnerUid,
OwnerNickname: pbAsset.OwnerNickname,
StarID: pbAsset.StarId,
Name: pbAsset.Name,
CoverURL: pbAsset.CoverUrl,
Visibility: pbAsset.Visibility,
Status: pbAsset.Status,
LikeCount: pbAsset.LikeCount,
CreatedAt: pbAsset.CreatedAt,
UpdatedAt: pbAsset.UpdatedAt,
IsLiked: pbAsset.IsLiked,
Info: pbAsset.Info,
DisplayStatus: pbAsset.DisplayStatus,
Earnings: pbAsset.Earnings,
ExhibitionExpireAt: pbAsset.ExhibitionExpireAt,
}
// 可选字段

View File

@ -80,7 +80,9 @@ type AssetDTO struct {
Owner *OwnerInfoDTO `json:"owner,omitempty"` // 持有者信息(可选,保留用于兼容性)
IsLiked bool `json:"is_liked"` // 当前用户是否已点赞
Info string `json:"info"` // 藏品信息
DisplayStatus int32 `json:"display_status"` // 展示状态0=待展示, 1=已展示
DisplayStatus int32 `json:"display_status"` // 展示状态0=待展示, 1=已展示
Earnings int64 `json:"earnings"` // 当前展出收益(实时计算,仅展出中时有值)
ExhibitionExpireAt int64 `json:"exhibition_expire_at"` // 展出过期时间毫秒时间戳仅展出中时有值0=未展出)
}
// OwnerInfoDTO 持有者信息

View File

@ -23,7 +23,7 @@ func (BoothSlot) TableName() string {
// Exhibition 展品展示表
type Exhibition struct {
ID int64 `gorm:"primaryKey;column:id;autoIncrement"`
AssetID int64 `gorm:"column:asset_id;not null;uniqueIndex:uk_asset"`
AssetID int64 `gorm:"column:asset_id;not null;index:idx_asset"`
SlotID int64 `gorm:"column:slot_id;not null;index:idx_slot"`
HostProfileID int64 `gorm:"column:host_profile_id;not null;index:idx_host"`
OccupierUID int64 `gorm:"column:occupier_uid;not null;index:idx_occupier"`

View File

@ -2,13 +2,13 @@ package models
// SystemConfig 系统配置表模型
type SystemConfig struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
ConfigKey string `gorm:"type:varchar(100);uniqueIndex;not null;column:config_key"`
ConfigValue int `gorm:"not null;column:config_value"`
Description string `gorm:"type:varchar(500);column:description"`
IsActive bool `gorm:"default:true;column:is_active"`
CreatedAt int64 `gorm:"column:created_at"`
UpdatedAt int64 `gorm:"column:updated_at"`
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
ConfigKey string `gorm:"type:varchar(100);uniqueIndex;not null;column:config_key"`
ConfigValue float64 `gorm:"type:numeric(10,2);not null;column:config_value"` // 使用 float64 支持小数
Description string `gorm:"type:varchar(500);column:description"`
IsActive bool `gorm:"default:true;column:is_active"`
CreatedAt int64 `gorm:"column:created_at"`
UpdatedAt int64 `gorm:"column:updated_at"`
}
// TableName 指定表名

View File

@ -44,13 +44,15 @@ type Asset struct {
UpdatedAt int64 `protobuf:"varint,16,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` // 更新时间(毫秒时间戳)
MintedAt int64 `protobuf:"varint,17,opt,name=minted_at,json=mintedAt,proto3" json:"minted_at,omitempty"` // 上链成功时间(毫秒时间戳,可选)
// 扩展字段:持有者信息(用于详情展示)
Owner *OwnerInfo `protobuf:"bytes,18,opt,name=owner,proto3" json:"owner,omitempty"` // 持有者信息(保留用于兼容性)
OwnerNickname string `protobuf:"bytes,19,opt,name=owner_nickname,json=ownerNickname,proto3" json:"owner_nickname,omitempty"` // 持有者昵称在该star下的昵称
IsLiked bool `protobuf:"varint,20,opt,name=is_liked,json=isLiked,proto3" json:"is_liked,omitempty"` // 当前用户是否已点赞
Info string `protobuf:"bytes,21,opt,name=info,proto3" json:"info,omitempty"` // 藏品信息
DisplayStatus int32 `protobuf:"varint,22,opt,name=display_status,json=displayStatus,proto3" json:"display_status,omitempty"` // 展示状态0=待展示, 1=已展示
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
Owner *OwnerInfo `protobuf:"bytes,18,opt,name=owner,proto3" json:"owner,omitempty"` // 持有者信息(保留用于兼容性)
OwnerNickname string `protobuf:"bytes,19,opt,name=owner_nickname,json=ownerNickname,proto3" json:"owner_nickname,omitempty"` // 持有者昵称在该star下的昵称
IsLiked bool `protobuf:"varint,20,opt,name=is_liked,json=isLiked,proto3" json:"is_liked,omitempty"` // 当前用户是否已点赞
Info string `protobuf:"bytes,21,opt,name=info,proto3" json:"info,omitempty"` // 藏品信息
DisplayStatus int32 `protobuf:"varint,22,opt,name=display_status,json=displayStatus,proto3" json:"display_status,omitempty"` // 展示状态0=待展示, 1=已展示
Earnings int64 `protobuf:"varint,23,opt,name=earnings,proto3" json:"earnings,omitempty"` // 当前展出收益(实时计算,仅展出中时有值)
ExhibitionExpireAt int64 `protobuf:"varint,24,opt,name=exhibition_expire_at,json=exhibitionExpireAt,proto3" json:"exhibition_expire_at,omitempty"` // 展出过期时间毫秒时间戳仅展出中时有值0=未展出)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Asset) Reset() {
@ -237,6 +239,20 @@ func (x *Asset) GetDisplayStatus() int32 {
return 0
}
func (x *Asset) GetEarnings() int64 {
if x != nil {
return x.Earnings
}
return 0
}
func (x *Asset) GetExhibitionExpireAt() int64 {
if x != nil {
return x.ExhibitionExpireAt
}
return 0
}
// 持有者信息
type OwnerInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -2618,7 +2634,7 @@ var File_asset_proto protoreflect.FileDescriptor
const file_asset_proto_rawDesc = "" +
"\n" +
"\vasset.proto\x12\rtopfans.asset\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\x93\x05\n" +
"\vasset.proto\x12\rtopfans.asset\x1a\x12proto/common.proto\x1a\x1cgoogle/api/annotations.proto\"\xe1\x05\n" +
"\x05Asset\x12\x19\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\x12\x1b\n" +
"\towner_uid\x18\x02 \x01(\x03R\bownerUid\x12\x17\n" +
@ -2647,7 +2663,9 @@ const file_asset_proto_rawDesc = "" +
"\x0eowner_nickname\x18\x13 \x01(\tR\rownerNickname\x12\x19\n" +
"\bis_liked\x18\x14 \x01(\bR\aisLiked\x12\x12\n" +
"\x04info\x18\x15 \x01(\tR\x04info\x12%\n" +
"\x0edisplay_status\x18\x16 \x01(\x05R\rdisplayStatus\"X\n" +
"\x0edisplay_status\x18\x16 \x01(\x05R\rdisplayStatus\x12\x1a\n" +
"\bearnings\x18\x17 \x01(\x03R\bearnings\x120\n" +
"\x14exhibition_expire_at\x18\x18 \x01(\x03R\x12exhibitionExpireAt\"X\n" +
"\tOwnerInfo\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x1a\n" +
"\bnickname\x18\x02 \x01(\tR\bnickname\x12\x16\n" +

View File

@ -939,6 +939,96 @@ func (x *RemoveFromSlotResponse) GetBase() *common.BaseResponse {
return nil
}
// 根据资产ID移除展品请求
type RemoveExhibitionByAssetRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
AssetId int64 `protobuf:"varint,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` // 资产ID必填
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RemoveExhibitionByAssetRequest) Reset() {
*x = RemoveExhibitionByAssetRequest{}
mi := &file_gallery_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RemoveExhibitionByAssetRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemoveExhibitionByAssetRequest) ProtoMessage() {}
func (x *RemoveExhibitionByAssetRequest) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RemoveExhibitionByAssetRequest.ProtoReflect.Descriptor instead.
func (*RemoveExhibitionByAssetRequest) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{16}
}
func (x *RemoveExhibitionByAssetRequest) GetAssetId() int64 {
if x != nil {
return x.AssetId
}
return 0
}
// 根据资产ID移除展品响应
type RemoveExhibitionByAssetResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RemoveExhibitionByAssetResponse) Reset() {
*x = RemoveExhibitionByAssetResponse{}
mi := &file_gallery_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RemoveExhibitionByAssetResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemoveExhibitionByAssetResponse) ProtoMessage() {}
func (x *RemoveExhibitionByAssetResponse) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RemoveExhibitionByAssetResponse.ProtoReflect.Descriptor instead.
func (*RemoveExhibitionByAssetResponse) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{17}
}
func (x *RemoveExhibitionByAssetResponse) GetBase() *common.BaseResponse {
if x != nil {
return x.Base
}
return nil
}
// 获取我展出的作品列表请求
type GetMyExhibitedAssetsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -950,7 +1040,7 @@ type GetMyExhibitedAssetsRequest struct {
func (x *GetMyExhibitedAssetsRequest) Reset() {
*x = GetMyExhibitedAssetsRequest{}
mi := &file_gallery_proto_msgTypes[16]
mi := &file_gallery_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -962,7 +1052,7 @@ func (x *GetMyExhibitedAssetsRequest) String() string {
func (*GetMyExhibitedAssetsRequest) ProtoMessage() {}
func (x *GetMyExhibitedAssetsRequest) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[16]
mi := &file_gallery_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -975,7 +1065,7 @@ func (x *GetMyExhibitedAssetsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetMyExhibitedAssetsRequest.ProtoReflect.Descriptor instead.
func (*GetMyExhibitedAssetsRequest) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{16}
return file_gallery_proto_rawDescGZIP(), []int{18}
}
func (x *GetMyExhibitedAssetsRequest) GetPage() int32 {
@ -1003,7 +1093,7 @@ type GetMyExhibitedAssetsResponse struct {
func (x *GetMyExhibitedAssetsResponse) Reset() {
*x = GetMyExhibitedAssetsResponse{}
mi := &file_gallery_proto_msgTypes[17]
mi := &file_gallery_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1015,7 +1105,7 @@ func (x *GetMyExhibitedAssetsResponse) String() string {
func (*GetMyExhibitedAssetsResponse) ProtoMessage() {}
func (x *GetMyExhibitedAssetsResponse) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[17]
mi := &file_gallery_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1028,7 +1118,7 @@ func (x *GetMyExhibitedAssetsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetMyExhibitedAssetsResponse.ProtoReflect.Descriptor instead.
func (*GetMyExhibitedAssetsResponse) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{17}
return file_gallery_proto_rawDescGZIP(), []int{19}
}
func (x *GetMyExhibitedAssetsResponse) GetBase() *common.BaseResponse {
@ -1059,7 +1149,7 @@ type ExhibitedAssetsData struct {
func (x *ExhibitedAssetsData) Reset() {
*x = ExhibitedAssetsData{}
mi := &file_gallery_proto_msgTypes[18]
mi := &file_gallery_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1071,7 +1161,7 @@ func (x *ExhibitedAssetsData) String() string {
func (*ExhibitedAssetsData) ProtoMessage() {}
func (x *ExhibitedAssetsData) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[18]
mi := &file_gallery_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1084,7 +1174,7 @@ func (x *ExhibitedAssetsData) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExhibitedAssetsData.ProtoReflect.Descriptor instead.
func (*ExhibitedAssetsData) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{18}
return file_gallery_proto_rawDescGZIP(), []int{20}
}
func (x *ExhibitedAssetsData) GetItems() []*ExhibitedAssetItem {
@ -1138,7 +1228,7 @@ type ExhibitedAssetItem struct {
func (x *ExhibitedAssetItem) Reset() {
*x = ExhibitedAssetItem{}
mi := &file_gallery_proto_msgTypes[19]
mi := &file_gallery_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1150,7 +1240,7 @@ func (x *ExhibitedAssetItem) String() string {
func (*ExhibitedAssetItem) ProtoMessage() {}
func (x *ExhibitedAssetItem) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[19]
mi := &file_gallery_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1163,7 +1253,7 @@ func (x *ExhibitedAssetItem) ProtoReflect() protoreflect.Message {
// Deprecated: Use ExhibitedAssetItem.ProtoReflect.Descriptor instead.
func (*ExhibitedAssetItem) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{19}
return file_gallery_proto_rawDescGZIP(), []int{21}
}
func (x *ExhibitedAssetItem) GetAssetId() int64 {
@ -1229,7 +1319,7 @@ type GetInspirationFlowRequest struct {
func (x *GetInspirationFlowRequest) Reset() {
*x = GetInspirationFlowRequest{}
mi := &file_gallery_proto_msgTypes[20]
mi := &file_gallery_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1241,7 +1331,7 @@ func (x *GetInspirationFlowRequest) String() string {
func (*GetInspirationFlowRequest) ProtoMessage() {}
func (x *GetInspirationFlowRequest) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[20]
mi := &file_gallery_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1254,7 +1344,7 @@ func (x *GetInspirationFlowRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetInspirationFlowRequest.ProtoReflect.Descriptor instead.
func (*GetInspirationFlowRequest) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{20}
return file_gallery_proto_rawDescGZIP(), []int{22}
}
func (x *GetInspirationFlowRequest) GetCursor() string {
@ -1303,7 +1393,7 @@ type GetInspirationFlowResponse struct {
func (x *GetInspirationFlowResponse) Reset() {
*x = GetInspirationFlowResponse{}
mi := &file_gallery_proto_msgTypes[21]
mi := &file_gallery_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1315,7 +1405,7 @@ func (x *GetInspirationFlowResponse) String() string {
func (*GetInspirationFlowResponse) ProtoMessage() {}
func (x *GetInspirationFlowResponse) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[21]
mi := &file_gallery_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1328,7 +1418,7 @@ func (x *GetInspirationFlowResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetInspirationFlowResponse.ProtoReflect.Descriptor instead.
func (*GetInspirationFlowResponse) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{21}
return file_gallery_proto_rawDescGZIP(), []int{23}
}
func (x *GetInspirationFlowResponse) GetBase() *common.BaseResponse {
@ -1358,7 +1448,7 @@ type InspirationFlowData struct {
func (x *InspirationFlowData) Reset() {
*x = InspirationFlowData{}
mi := &file_gallery_proto_msgTypes[22]
mi := &file_gallery_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1370,7 +1460,7 @@ func (x *InspirationFlowData) String() string {
func (*InspirationFlowData) ProtoMessage() {}
func (x *InspirationFlowData) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[22]
mi := &file_gallery_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1383,7 +1473,7 @@ func (x *InspirationFlowData) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspirationFlowData.ProtoReflect.Descriptor instead.
func (*InspirationFlowData) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{22}
return file_gallery_proto_rawDescGZIP(), []int{24}
}
func (x *InspirationFlowData) GetItems() []*InspirationFlowItem {
@ -1430,7 +1520,7 @@ type InspirationFlowItem struct {
func (x *InspirationFlowItem) Reset() {
*x = InspirationFlowItem{}
mi := &file_gallery_proto_msgTypes[23]
mi := &file_gallery_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1442,7 +1532,7 @@ func (x *InspirationFlowItem) String() string {
func (*InspirationFlowItem) ProtoMessage() {}
func (x *InspirationFlowItem) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[23]
mi := &file_gallery_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1455,7 +1545,7 @@ func (x *InspirationFlowItem) ProtoReflect() protoreflect.Message {
// Deprecated: Use InspirationFlowItem.ProtoReflect.Descriptor instead.
func (*InspirationFlowItem) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{23}
return file_gallery_proto_rawDescGZIP(), []int{25}
}
func (x *InspirationFlowItem) GetAssetId() int64 {
@ -1519,7 +1609,7 @@ type GetUserExhibitedAssetsRequest struct {
func (x *GetUserExhibitedAssetsRequest) Reset() {
*x = GetUserExhibitedAssetsRequest{}
mi := &file_gallery_proto_msgTypes[24]
mi := &file_gallery_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1531,7 +1621,7 @@ func (x *GetUserExhibitedAssetsRequest) String() string {
func (*GetUserExhibitedAssetsRequest) ProtoMessage() {}
func (x *GetUserExhibitedAssetsRequest) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[24]
mi := &file_gallery_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1544,7 +1634,7 @@ func (x *GetUserExhibitedAssetsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetUserExhibitedAssetsRequest.ProtoReflect.Descriptor instead.
func (*GetUserExhibitedAssetsRequest) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{24}
return file_gallery_proto_rawDescGZIP(), []int{26}
}
func (x *GetUserExhibitedAssetsRequest) GetUserId() int64 {
@ -1579,7 +1669,7 @@ type GetUserExhibitedAssetsResponse struct {
func (x *GetUserExhibitedAssetsResponse) Reset() {
*x = GetUserExhibitedAssetsResponse{}
mi := &file_gallery_proto_msgTypes[25]
mi := &file_gallery_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1591,7 +1681,7 @@ func (x *GetUserExhibitedAssetsResponse) String() string {
func (*GetUserExhibitedAssetsResponse) ProtoMessage() {}
func (x *GetUserExhibitedAssetsResponse) ProtoReflect() protoreflect.Message {
mi := &file_gallery_proto_msgTypes[25]
mi := &file_gallery_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1604,7 +1694,7 @@ func (x *GetUserExhibitedAssetsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetUserExhibitedAssetsResponse.ProtoReflect.Descriptor instead.
func (*GetUserExhibitedAssetsResponse) Descriptor() ([]byte, []int) {
return file_gallery_proto_rawDescGZIP(), []int{25}
return file_gallery_proto_rawDescGZIP(), []int{27}
}
func (x *GetUserExhibitedAssetsResponse) GetBase() *common.BaseResponse {
@ -1695,6 +1785,10 @@ const file_gallery_proto_rawDesc = "" +
"\x15RemoveFromSlotRequest\x12\x17\n" +
"\aslot_id\x18\x01 \x01(\x03R\x06slotId\"J\n" +
"\x16RemoveFromSlotResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\";\n" +
"\x1eRemoveExhibitionByAssetRequest\x12\x19\n" +
"\basset_id\x18\x01 \x01(\x03R\aassetId\"S\n" +
"\x1fRemoveExhibitionByAssetResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\"N\n" +
"\x1bGetMyExhibitedAssetsRequest\x12\x12\n" +
"\x04page\x18\x01 \x01(\x05R\x04page\x12\x1b\n" +
@ -1748,7 +1842,7 @@ const file_gallery_proto_rawDesc = "" +
"\tpage_size\x18\x03 \x01(\x05R\bpageSize\"\x8c\x01\n" +
"\x1eGetUserExhibitedAssetsResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x128\n" +
"\x04data\x18\x02 \x01(\v2$.topfans.gallery.ExhibitedAssetsDataR\x04data2\xf4\b\n" +
"\x04data\x18\x02 \x01(\v2$.topfans.gallery.ExhibitedAssetsDataR\x04data2\xf2\t\n" +
"\x0eGalleryService\x12u\n" +
"\fGetMyGallery\x12$.topfans.gallery.GetMyGalleryRequest\x1a%.topfans.gallery.GetMyGalleryResponse\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/api/mygalleries\x12\x86\x01\n" +
"\x0eGetUserGallery\x12&.topfans.gallery.GetUserGalleryRequest\x1a'.topfans.gallery.GetUserGalleryResponse\"#\x82\xd3\xe4\x93\x02\x1d\x12\x1b/api/galleries/{target_uid}\x12v\n" +
@ -1756,7 +1850,8 @@ const file_gallery_proto_rawDesc = "" +
"PlaceAsset\x12\".topfans.gallery.PlaceAssetRequest\x1a#.topfans.gallery.PlaceAssetResponse\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/galleries/place\x12}\n" +
"\n" +
"UnlockSlot\x12\".topfans.gallery.UnlockSlotRequest\x1a#.topfans.gallery.UnlockSlotResponse\"&\x82\xd3\xe4\x93\x02 :\x01*\"\x1b/api/galleries/slots_unlock\x12\x8f\x01\n" +
"\x0eRemoveFromSlot\x12&.topfans.gallery.RemoveFromSlotRequest\x1a'.topfans.gallery.RemoveFromSlotResponse\",\x82\xd3\xe4\x93\x02&*$/api/galleries/slots/{slot_id}/asset\x12\x98\x01\n" +
"\x0eRemoveFromSlot\x12&.topfans.gallery.RemoveFromSlotRequest\x1a'.topfans.gallery.RemoveFromSlotResponse\",\x82\xd3\xe4\x93\x02&*$/api/galleries/slots/{slot_id}/asset\x12|\n" +
"\x17RemoveExhibitionByAsset\x12/.topfans.gallery.RemoveExhibitionByAssetRequest\x1a0.topfans.gallery.RemoveExhibitionByAssetResponse\x12\x98\x01\n" +
"\x14GetMyExhibitedAssets\x12,.topfans.gallery.GetMyExhibitedAssetsRequest\x1a-.topfans.gallery.GetMyExhibitedAssetsResponse\"#\x82\xd3\xe4\x93\x02\x1d\x12\x1b/api/v1/me/exhibited-assets\x12\x8f\x01\n" +
"\x12GetInspirationFlow\x12*.topfans.gallery.GetInspirationFlowRequest\x1a+.topfans.gallery.GetInspirationFlowResponse\" \x82\xd3\xe4\x93\x02\x1a\x12\x18/api/v1/inspiration-flow\x12\xab\x01\n" +
"\x16GetUserExhibitedAssets\x12..topfans.gallery.GetUserExhibitedAssetsRequest\x1a/.topfans.gallery.GetUserExhibitedAssetsResponse\"0\x82\xd3\xe4\x93\x02*\x12(/api/v1/users/{user_id}/exhibited-assetsB6Z4github.com/topfans/backend/pkg/proto/gallery;galleryb\x06proto3"
@ -1773,78 +1868,83 @@ func file_gallery_proto_rawDescGZIP() []byte {
return file_gallery_proto_rawDescData
}
var file_gallery_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
var file_gallery_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
var file_gallery_proto_goTypes = []any{
(*GetMyGalleryRequest)(nil), // 0: topfans.gallery.GetMyGalleryRequest
(*GetMyGalleryResponse)(nil), // 1: topfans.gallery.GetMyGalleryResponse
(*GetUserGalleryRequest)(nil), // 2: topfans.gallery.GetUserGalleryRequest
(*GetUserGalleryResponse)(nil), // 3: topfans.gallery.GetUserGalleryResponse
(*PlaceAssetRequest)(nil), // 4: topfans.gallery.PlaceAssetRequest
(*PlaceAssetResponse)(nil), // 5: topfans.gallery.PlaceAssetResponse
(*UnlockSlotRequest)(nil), // 6: topfans.gallery.UnlockSlotRequest
(*UnlockSlotResponse)(nil), // 7: topfans.gallery.UnlockSlotResponse
(*GalleryData)(nil), // 8: topfans.gallery.GalleryData
(*SlotInfo)(nil), // 9: topfans.gallery.SlotInfo
(*AssetInfo)(nil), // 10: topfans.gallery.AssetInfo
(*UnlockCondition)(nil), // 11: topfans.gallery.UnlockCondition
(*PlaceAssetData)(nil), // 12: topfans.gallery.PlaceAssetData
(*UnlockSlotData)(nil), // 13: topfans.gallery.UnlockSlotData
(*RemoveFromSlotRequest)(nil), // 14: topfans.gallery.RemoveFromSlotRequest
(*RemoveFromSlotResponse)(nil), // 15: topfans.gallery.RemoveFromSlotResponse
(*GetMyExhibitedAssetsRequest)(nil), // 16: topfans.gallery.GetMyExhibitedAssetsRequest
(*GetMyExhibitedAssetsResponse)(nil), // 17: topfans.gallery.GetMyExhibitedAssetsResponse
(*ExhibitedAssetsData)(nil), // 18: topfans.gallery.ExhibitedAssetsData
(*ExhibitedAssetItem)(nil), // 19: topfans.gallery.ExhibitedAssetItem
(*GetInspirationFlowRequest)(nil), // 20: topfans.gallery.GetInspirationFlowRequest
(*GetInspirationFlowResponse)(nil), // 21: topfans.gallery.GetInspirationFlowResponse
(*InspirationFlowData)(nil), // 22: topfans.gallery.InspirationFlowData
(*InspirationFlowItem)(nil), // 23: topfans.gallery.InspirationFlowItem
(*GetUserExhibitedAssetsRequest)(nil), // 24: topfans.gallery.GetUserExhibitedAssetsRequest
(*GetUserExhibitedAssetsResponse)(nil), // 25: topfans.gallery.GetUserExhibitedAssetsResponse
(*common.BaseResponse)(nil), // 26: topfans.common.BaseResponse
(*GetMyGalleryRequest)(nil), // 0: topfans.gallery.GetMyGalleryRequest
(*GetMyGalleryResponse)(nil), // 1: topfans.gallery.GetMyGalleryResponse
(*GetUserGalleryRequest)(nil), // 2: topfans.gallery.GetUserGalleryRequest
(*GetUserGalleryResponse)(nil), // 3: topfans.gallery.GetUserGalleryResponse
(*PlaceAssetRequest)(nil), // 4: topfans.gallery.PlaceAssetRequest
(*PlaceAssetResponse)(nil), // 5: topfans.gallery.PlaceAssetResponse
(*UnlockSlotRequest)(nil), // 6: topfans.gallery.UnlockSlotRequest
(*UnlockSlotResponse)(nil), // 7: topfans.gallery.UnlockSlotResponse
(*GalleryData)(nil), // 8: topfans.gallery.GalleryData
(*SlotInfo)(nil), // 9: topfans.gallery.SlotInfo
(*AssetInfo)(nil), // 10: topfans.gallery.AssetInfo
(*UnlockCondition)(nil), // 11: topfans.gallery.UnlockCondition
(*PlaceAssetData)(nil), // 12: topfans.gallery.PlaceAssetData
(*UnlockSlotData)(nil), // 13: topfans.gallery.UnlockSlotData
(*RemoveFromSlotRequest)(nil), // 14: topfans.gallery.RemoveFromSlotRequest
(*RemoveFromSlotResponse)(nil), // 15: topfans.gallery.RemoveFromSlotResponse
(*RemoveExhibitionByAssetRequest)(nil), // 16: topfans.gallery.RemoveExhibitionByAssetRequest
(*RemoveExhibitionByAssetResponse)(nil), // 17: topfans.gallery.RemoveExhibitionByAssetResponse
(*GetMyExhibitedAssetsRequest)(nil), // 18: topfans.gallery.GetMyExhibitedAssetsRequest
(*GetMyExhibitedAssetsResponse)(nil), // 19: topfans.gallery.GetMyExhibitedAssetsResponse
(*ExhibitedAssetsData)(nil), // 20: topfans.gallery.ExhibitedAssetsData
(*ExhibitedAssetItem)(nil), // 21: topfans.gallery.ExhibitedAssetItem
(*GetInspirationFlowRequest)(nil), // 22: topfans.gallery.GetInspirationFlowRequest
(*GetInspirationFlowResponse)(nil), // 23: topfans.gallery.GetInspirationFlowResponse
(*InspirationFlowData)(nil), // 24: topfans.gallery.InspirationFlowData
(*InspirationFlowItem)(nil), // 25: topfans.gallery.InspirationFlowItem
(*GetUserExhibitedAssetsRequest)(nil), // 26: topfans.gallery.GetUserExhibitedAssetsRequest
(*GetUserExhibitedAssetsResponse)(nil), // 27: topfans.gallery.GetUserExhibitedAssetsResponse
(*common.BaseResponse)(nil), // 28: topfans.common.BaseResponse
}
var file_gallery_proto_depIdxs = []int32{
26, // 0: topfans.gallery.GetMyGalleryResponse.base:type_name -> topfans.common.BaseResponse
28, // 0: topfans.gallery.GetMyGalleryResponse.base:type_name -> topfans.common.BaseResponse
8, // 1: topfans.gallery.GetMyGalleryResponse.data:type_name -> topfans.gallery.GalleryData
26, // 2: topfans.gallery.GetUserGalleryResponse.base:type_name -> topfans.common.BaseResponse
28, // 2: topfans.gallery.GetUserGalleryResponse.base:type_name -> topfans.common.BaseResponse
8, // 3: topfans.gallery.GetUserGalleryResponse.data:type_name -> topfans.gallery.GalleryData
26, // 4: topfans.gallery.PlaceAssetResponse.base:type_name -> topfans.common.BaseResponse
28, // 4: topfans.gallery.PlaceAssetResponse.base:type_name -> topfans.common.BaseResponse
12, // 5: topfans.gallery.PlaceAssetResponse.data:type_name -> topfans.gallery.PlaceAssetData
26, // 6: topfans.gallery.UnlockSlotResponse.base:type_name -> topfans.common.BaseResponse
28, // 6: topfans.gallery.UnlockSlotResponse.base:type_name -> topfans.common.BaseResponse
13, // 7: topfans.gallery.UnlockSlotResponse.data:type_name -> topfans.gallery.UnlockSlotData
9, // 8: topfans.gallery.GalleryData.slots:type_name -> topfans.gallery.SlotInfo
10, // 9: topfans.gallery.SlotInfo.asset:type_name -> topfans.gallery.AssetInfo
11, // 10: topfans.gallery.SlotInfo.unlock_condition:type_name -> topfans.gallery.UnlockCondition
26, // 11: topfans.gallery.RemoveFromSlotResponse.base:type_name -> topfans.common.BaseResponse
26, // 12: topfans.gallery.GetMyExhibitedAssetsResponse.base:type_name -> topfans.common.BaseResponse
18, // 13: topfans.gallery.GetMyExhibitedAssetsResponse.data:type_name -> topfans.gallery.ExhibitedAssetsData
19, // 14: topfans.gallery.ExhibitedAssetsData.items:type_name -> topfans.gallery.ExhibitedAssetItem
26, // 15: topfans.gallery.GetInspirationFlowResponse.base:type_name -> topfans.common.BaseResponse
22, // 16: topfans.gallery.GetInspirationFlowResponse.data:type_name -> topfans.gallery.InspirationFlowData
23, // 17: topfans.gallery.InspirationFlowData.items:type_name -> topfans.gallery.InspirationFlowItem
26, // 18: topfans.gallery.GetUserExhibitedAssetsResponse.base:type_name -> topfans.common.BaseResponse
18, // 19: topfans.gallery.GetUserExhibitedAssetsResponse.data:type_name -> topfans.gallery.ExhibitedAssetsData
0, // 20: topfans.gallery.GalleryService.GetMyGallery:input_type -> topfans.gallery.GetMyGalleryRequest
2, // 21: topfans.gallery.GalleryService.GetUserGallery:input_type -> topfans.gallery.GetUserGalleryRequest
4, // 22: topfans.gallery.GalleryService.PlaceAsset:input_type -> topfans.gallery.PlaceAssetRequest
6, // 23: topfans.gallery.GalleryService.UnlockSlot:input_type -> topfans.gallery.UnlockSlotRequest
14, // 24: topfans.gallery.GalleryService.RemoveFromSlot:input_type -> topfans.gallery.RemoveFromSlotRequest
16, // 25: topfans.gallery.GalleryService.GetMyExhibitedAssets:input_type -> topfans.gallery.GetMyExhibitedAssetsRequest
20, // 26: topfans.gallery.GalleryService.GetInspirationFlow:input_type -> topfans.gallery.GetInspirationFlowRequest
24, // 27: topfans.gallery.GalleryService.GetUserExhibitedAssets:input_type -> topfans.gallery.GetUserExhibitedAssetsRequest
1, // 28: topfans.gallery.GalleryService.GetMyGallery:output_type -> topfans.gallery.GetMyGalleryResponse
3, // 29: topfans.gallery.GalleryService.GetUserGallery:output_type -> topfans.gallery.GetUserGalleryResponse
5, // 30: topfans.gallery.GalleryService.PlaceAsset:output_type -> topfans.gallery.PlaceAssetResponse
7, // 31: topfans.gallery.GalleryService.UnlockSlot:output_type -> topfans.gallery.UnlockSlotResponse
15, // 32: topfans.gallery.GalleryService.RemoveFromSlot:output_type -> topfans.gallery.RemoveFromSlotResponse
17, // 33: topfans.gallery.GalleryService.GetMyExhibitedAssets:output_type -> topfans.gallery.GetMyExhibitedAssetsResponse
21, // 34: topfans.gallery.GalleryService.GetInspirationFlow:output_type -> topfans.gallery.GetInspirationFlowResponse
25, // 35: topfans.gallery.GalleryService.GetUserExhibitedAssets:output_type -> topfans.gallery.GetUserExhibitedAssetsResponse
28, // [28:36] is the sub-list for method output_type
20, // [20:28] is the sub-list for method input_type
20, // [20:20] is the sub-list for extension type_name
20, // [20:20] is the sub-list for extension extendee
0, // [0:20] is the sub-list for field type_name
28, // 11: topfans.gallery.RemoveFromSlotResponse.base:type_name -> topfans.common.BaseResponse
28, // 12: topfans.gallery.RemoveExhibitionByAssetResponse.base:type_name -> topfans.common.BaseResponse
28, // 13: topfans.gallery.GetMyExhibitedAssetsResponse.base:type_name -> topfans.common.BaseResponse
20, // 14: topfans.gallery.GetMyExhibitedAssetsResponse.data:type_name -> topfans.gallery.ExhibitedAssetsData
21, // 15: topfans.gallery.ExhibitedAssetsData.items:type_name -> topfans.gallery.ExhibitedAssetItem
28, // 16: topfans.gallery.GetInspirationFlowResponse.base:type_name -> topfans.common.BaseResponse
24, // 17: topfans.gallery.GetInspirationFlowResponse.data:type_name -> topfans.gallery.InspirationFlowData
25, // 18: topfans.gallery.InspirationFlowData.items:type_name -> topfans.gallery.InspirationFlowItem
28, // 19: topfans.gallery.GetUserExhibitedAssetsResponse.base:type_name -> topfans.common.BaseResponse
20, // 20: topfans.gallery.GetUserExhibitedAssetsResponse.data:type_name -> topfans.gallery.ExhibitedAssetsData
0, // 21: topfans.gallery.GalleryService.GetMyGallery:input_type -> topfans.gallery.GetMyGalleryRequest
2, // 22: topfans.gallery.GalleryService.GetUserGallery:input_type -> topfans.gallery.GetUserGalleryRequest
4, // 23: topfans.gallery.GalleryService.PlaceAsset:input_type -> topfans.gallery.PlaceAssetRequest
6, // 24: topfans.gallery.GalleryService.UnlockSlot:input_type -> topfans.gallery.UnlockSlotRequest
14, // 25: topfans.gallery.GalleryService.RemoveFromSlot:input_type -> topfans.gallery.RemoveFromSlotRequest
16, // 26: topfans.gallery.GalleryService.RemoveExhibitionByAsset:input_type -> topfans.gallery.RemoveExhibitionByAssetRequest
18, // 27: topfans.gallery.GalleryService.GetMyExhibitedAssets:input_type -> topfans.gallery.GetMyExhibitedAssetsRequest
22, // 28: topfans.gallery.GalleryService.GetInspirationFlow:input_type -> topfans.gallery.GetInspirationFlowRequest
26, // 29: topfans.gallery.GalleryService.GetUserExhibitedAssets:input_type -> topfans.gallery.GetUserExhibitedAssetsRequest
1, // 30: topfans.gallery.GalleryService.GetMyGallery:output_type -> topfans.gallery.GetMyGalleryResponse
3, // 31: topfans.gallery.GalleryService.GetUserGallery:output_type -> topfans.gallery.GetUserGalleryResponse
5, // 32: topfans.gallery.GalleryService.PlaceAsset:output_type -> topfans.gallery.PlaceAssetResponse
7, // 33: topfans.gallery.GalleryService.UnlockSlot:output_type -> topfans.gallery.UnlockSlotResponse
15, // 34: topfans.gallery.GalleryService.RemoveFromSlot:output_type -> topfans.gallery.RemoveFromSlotResponse
17, // 35: topfans.gallery.GalleryService.RemoveExhibitionByAsset:output_type -> topfans.gallery.RemoveExhibitionByAssetResponse
19, // 36: topfans.gallery.GalleryService.GetMyExhibitedAssets:output_type -> topfans.gallery.GetMyExhibitedAssetsResponse
23, // 37: topfans.gallery.GalleryService.GetInspirationFlow:output_type -> topfans.gallery.GetInspirationFlowResponse
27, // 38: topfans.gallery.GalleryService.GetUserExhibitedAssets:output_type -> topfans.gallery.GetUserExhibitedAssetsResponse
30, // [30:39] is the sub-list for method output_type
21, // [21:30] is the sub-list for method input_type
21, // [21:21] is the sub-list for extension type_name
21, // [21:21] is the sub-list for extension extendee
0, // [0:21] is the sub-list for field type_name
}
func init() { file_gallery_proto_init() }
@ -1858,7 +1958,7 @@ func file_gallery_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_gallery_proto_rawDesc), len(file_gallery_proto_rawDesc)),
NumEnums: 0,
NumMessages: 26,
NumMessages: 28,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -46,6 +46,8 @@ const (
GalleryServiceUnlockSlotProcedure = "/topfans.gallery.GalleryService/UnlockSlot"
// GalleryServiceRemoveFromSlotProcedure is the fully-qualified name of the GalleryService's RemoveFromSlot RPC.
GalleryServiceRemoveFromSlotProcedure = "/topfans.gallery.GalleryService/RemoveFromSlot"
// GalleryServiceRemoveExhibitionByAssetProcedure is the fully-qualified name of the GalleryService's RemoveExhibitionByAsset RPC.
GalleryServiceRemoveExhibitionByAssetProcedure = "/topfans.gallery.GalleryService/RemoveExhibitionByAsset"
// GalleryServiceGetMyExhibitedAssetsProcedure is the fully-qualified name of the GalleryService's GetMyExhibitedAssets RPC.
GalleryServiceGetMyExhibitedAssetsProcedure = "/topfans.gallery.GalleryService/GetMyExhibitedAssets"
// GalleryServiceGetInspirationFlowProcedure is the fully-qualified name of the GalleryService's GetInspirationFlow RPC.
@ -65,6 +67,7 @@ type GalleryService interface {
PlaceAsset(ctx context.Context, req *PlaceAssetRequest, opts ...client.CallOption) (*PlaceAssetResponse, error)
UnlockSlot(ctx context.Context, req *UnlockSlotRequest, opts ...client.CallOption) (*UnlockSlotResponse, error)
RemoveFromSlot(ctx context.Context, req *RemoveFromSlotRequest, opts ...client.CallOption) (*RemoveFromSlotResponse, error)
RemoveExhibitionByAsset(ctx context.Context, req *RemoveExhibitionByAssetRequest, opts ...client.CallOption) (*RemoveExhibitionByAssetResponse, error)
GetMyExhibitedAssets(ctx context.Context, req *GetMyExhibitedAssetsRequest, opts ...client.CallOption) (*GetMyExhibitedAssetsResponse, error)
GetInspirationFlow(ctx context.Context, req *GetInspirationFlowRequest, opts ...client.CallOption) (*GetInspirationFlowResponse, error)
GetUserExhibitedAssets(ctx context.Context, req *GetUserExhibitedAssetsRequest, opts ...client.CallOption) (*GetUserExhibitedAssetsResponse, error)
@ -130,6 +133,14 @@ func (c *GalleryServiceImpl) RemoveFromSlot(ctx context.Context, req *RemoveFrom
return resp, nil
}
func (c *GalleryServiceImpl) RemoveExhibitionByAsset(ctx context.Context, req *RemoveExhibitionByAssetRequest, opts ...client.CallOption) (*RemoveExhibitionByAssetResponse, error) {
resp := new(RemoveExhibitionByAssetResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "RemoveExhibitionByAsset", opts...); err != nil {
return nil, err
}
return resp, nil
}
func (c *GalleryServiceImpl) GetMyExhibitedAssets(ctx context.Context, req *GetMyExhibitedAssetsRequest, opts ...client.CallOption) (*GetMyExhibitedAssetsResponse, error) {
resp := new(GetMyExhibitedAssetsResponse)
if err := c.conn.CallUnary(ctx, []interface{}{req}, resp, "GetMyExhibitedAssets", opts...); err != nil {
@ -156,7 +167,7 @@ func (c *GalleryServiceImpl) GetUserExhibitedAssets(ctx context.Context, req *Ge
var GalleryService_ClientInfo = client.ClientInfo{
InterfaceName: "topfans.gallery.GalleryService",
MethodNames: []string{"GetMyGallery", "GetUserGallery", "PlaceAsset", "UnlockSlot", "RemoveFromSlot", "GetMyExhibitedAssets", "GetInspirationFlow", "GetUserExhibitedAssets"},
MethodNames: []string{"GetMyGallery", "GetUserGallery", "PlaceAsset", "UnlockSlot", "RemoveFromSlot", "RemoveExhibitionByAsset", "GetMyExhibitedAssets", "GetInspirationFlow", "GetUserExhibitedAssets"},
ConnectionInjectFunc: func(dubboCliRaw interface{}, conn *client.Connection) {
dubboCli := dubboCliRaw.(*GalleryServiceImpl)
dubboCli.conn = conn
@ -170,6 +181,7 @@ type GalleryServiceHandler interface {
PlaceAsset(context.Context, *PlaceAssetRequest) (*PlaceAssetResponse, error)
UnlockSlot(context.Context, *UnlockSlotRequest) (*UnlockSlotResponse, error)
RemoveFromSlot(context.Context, *RemoveFromSlotRequest) (*RemoveFromSlotResponse, error)
RemoveExhibitionByAsset(context.Context, *RemoveExhibitionByAssetRequest) (*RemoveExhibitionByAssetResponse, error)
GetMyExhibitedAssets(context.Context, *GetMyExhibitedAssetsRequest) (*GetMyExhibitedAssetsResponse, error)
GetInspirationFlow(context.Context, *GetInspirationFlowRequest) (*GetInspirationFlowResponse, error)
GetUserExhibitedAssets(context.Context, *GetUserExhibitedAssetsRequest) (*GetUserExhibitedAssetsResponse, error)
@ -262,6 +274,21 @@ var GalleryService_ServiceInfo = server.ServiceInfo{
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "RemoveExhibitionByAsset",
Type: constant.CallUnary,
ReqInitFunc: func() interface{} {
return new(RemoveExhibitionByAssetRequest)
},
MethodFunc: func(ctx context.Context, args []interface{}, handler interface{}) (interface{}, error) {
req := args[0].(*RemoveExhibitionByAssetRequest)
res, err := handler.(GalleryServiceHandler).RemoveExhibitionByAsset(ctx, req)
if err != nil {
return nil, err
}
return triple_protocol.NewResponse(res), nil
},
},
{
Name: "GetMyExhibitedAssets",
Type: constant.CallUnary,

View File

@ -1451,6 +1451,8 @@ type ClaimExhibitionRevenueResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Base *common.BaseResponse `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"`
CrystalAmount int64 `protobuf:"varint,3,opt,name=crystal_amount,json=crystalAmount,proto3" json:"crystal_amount,omitempty"` // 当前领取的收益金额
TotalBalance int64 `protobuf:"varint,4,opt,name=total_balance,json=totalBalance,proto3" json:"total_balance,omitempty"` // 领取后的当前用户全部余额
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1499,6 +1501,20 @@ func (x *ClaimExhibitionRevenueResponse) GetSuccess() bool {
return false
}
func (x *ClaimExhibitionRevenueResponse) GetCrystalAmount() int64 {
if x != nil {
return x.CrystalAmount
}
return 0
}
func (x *ClaimExhibitionRevenueResponse) GetTotalBalance() int64 {
if x != nil {
return x.TotalBalance
}
return 0
}
type ClaimAllExhibitionRevenueRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
StarId int64 `protobuf:"varint,1,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"`
@ -1969,10 +1985,12 @@ const file_task_proto_rawDesc = "" +
"\x1dClaimExhibitionRevenueRequest\x12\x1d\n" +
"\n" +
"revenue_id\x18\x01 \x01(\x03R\trevenueId\x12\x17\n" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\"l\n" +
"\astar_id\x18\x02 \x01(\x03R\x06starId\"\xb8\x01\n" +
"\x1eClaimExhibitionRevenueResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12\x18\n" +
"\asuccess\x18\x02 \x01(\bR\asuccess\";\n" +
"\asuccess\x18\x02 \x01(\bR\asuccess\x12%\n" +
"\x0ecrystal_amount\x18\x03 \x01(\x03R\rcrystalAmount\x12#\n" +
"\rtotal_balance\x18\x04 \x01(\x03R\ftotalBalance\";\n" +
" ClaimAllExhibitionRevenueRequest\x12\x17\n" +
"\astar_id\x18\x01 \x01(\x03R\x06starId\"z\n" +
"!ClaimAllExhibitionRevenueResponse\x120\n" +
@ -1997,7 +2015,7 @@ const file_task_proto_rawDesc = "" +
"\texpire_at\x18\t \x01(\x03R\bexpireAt\"}\n" +
"\x1dOnExhibitionCompletedResponse\x120\n" +
"\x04base\x18\x01 \x01(\v2\x1c.topfans.common.BaseResponseR\x04base\x12*\n" +
"\x11revenue_record_id\x18\x02 \x01(\x03R\x0frevenueRecordId2\xbd\f\n" +
"\x11revenue_record_id\x18\x02 \x01(\x03R\x0frevenueRecordId2\xc6\f\n" +
"\x11TaskMobileService\x12r\n" +
"\rGetDailyTasks\x12\".topfans.task.GetDailyTasksRequest\x1a#.topfans.task.GetDailyTasksResponse\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/api/tasks/daily\x12v\n" +
"\vReportEvent\x12 .topfans.task.ReportEventRequest\x1a!.topfans.task.ReportEventResponse\"\"\x82\xd3\xe4\x93\x02\x1c:\x01*\"\x17/api/tasks/report-event\x12~\n" +
@ -2006,10 +2024,10 @@ const file_task_proto_rawDesc = "" +
"\rCompleteGuide\x12\".topfans.task.CompleteGuideRequest\x1a#.topfans.task.CompleteGuideResponse\"$\x82\xd3\xe4\x93\x02\x1e:\x01*\"\x19/api/tasks/guide/complete\x12\x90\x01\n" +
"\x13GetOnboardingStatus\x12(.topfans.task.GetOnboardingStatusRequest\x1a).topfans.task.GetOnboardingStatusResponse\"$\x82\xd3\xe4\x93\x02\x1e\x12\x1c/api/tasks/onboarding/status\x12\x85\x01\n" +
"\fAdvanceStage\x12!.topfans.task.AdvanceStageRequest\x1a\".topfans.task.AdvanceStageResponse\".\x82\xd3\xe4\x93\x02(:\x01*\"#/api/tasks/onboarding/advance-stage\x12\x9f\x01\n" +
"\x15ClaimOnboardingReward\x12*.topfans.task.ClaimOnboardingRewardRequest\x1a+.topfans.task.ClaimOnboardingRewardResponse\"-\x82\xd3\xe4\x93\x02':\x01*\"\"/api/tasks/onboarding/claim-reward\x12\x94\x01\n" +
"\x14GetExhibitionRevenue\x12).topfans.task.GetExhibitionRevenueRequest\x1a*.topfans.task.GetExhibitionRevenueResponse\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/api/tasks/exhibition-revenue\x12\xa3\x01\n" +
"\x16ClaimExhibitionRevenue\x12+.topfans.task.ClaimExhibitionRevenueRequest\x1a,.topfans.task.ClaimExhibitionRevenueResponse\".\x82\xd3\xe4\x93\x02(:\x01*\"#/api/tasks/exhibition-revenue/claim\x12\xb0\x01\n" +
"\x19ClaimAllExhibitionRevenue\x12..topfans.task.ClaimAllExhibitionRevenueRequest\x1a/.topfans.task.ClaimAllExhibitionRevenueResponse\"2\x82\xd3\xe4\x93\x02,:\x01*\"'/api/tasks/exhibition-revenue/claim-all2\xe1\x01\n" +
"\x15ClaimOnboardingReward\x12*.topfans.task.ClaimOnboardingRewardRequest\x1a+.topfans.task.ClaimOnboardingRewardResponse\"-\x82\xd3\xe4\x93\x02':\x01*\"\"/api/tasks/onboarding/claim-reward\x12\x97\x01\n" +
"\x14GetExhibitionRevenue\x12).topfans.task.GetExhibitionRevenueRequest\x1a*.topfans.task.GetExhibitionRevenueResponse\"(\x82\xd3\xe4\x93\x02\"\x12 /api/v1/tasks/exhibition-revenue\x12\xa6\x01\n" +
"\x16ClaimExhibitionRevenue\x12+.topfans.task.ClaimExhibitionRevenueRequest\x1a,.topfans.task.ClaimExhibitionRevenueResponse\"1\x82\xd3\xe4\x93\x02+:\x01*\"&/api/v1/tasks/exhibition-revenue/claim\x12\xb3\x01\n" +
"\x19ClaimAllExhibitionRevenue\x12..topfans.task.ClaimAllExhibitionRevenueRequest\x1a/.topfans.task.ClaimAllExhibitionRevenueResponse\"5\x82\xd3\xe4\x93\x02/:\x01*\"*/api/v1/tasks/exhibition-revenue/claim-all2\xe1\x01\n" +
"\x13TaskInternalService\x12X\n" +
"\rInitUserTasks\x12\".topfans.task.InitUserTasksRequest\x1a#.topfans.task.InitUserTasksResponse\x12p\n" +
"\x15OnExhibitionCompleted\x12*.topfans.task.OnExhibitionCompletedRequest\x1a+.topfans.task.OnExhibitionCompletedResponseB0Z.github.com/topfans/backend/pkg/proto/task;taskb\x06proto3"

View File

@ -35,6 +35,8 @@ message Asset {
bool is_liked = 20; //
string info = 21; //
int32 display_status = 22; // 0=, 1=
int64 earnings = 23; //
int64 exhibition_expire_at = 24; // 0=
}
//

View File

@ -46,6 +46,9 @@ service GalleryService {
};
}
// RPCID移除展品
rpc RemoveExhibitionByAsset(RemoveExhibitionByAssetRequest) returns (RemoveExhibitionByAssetResponse);
// ========== ==========
//
@ -162,6 +165,16 @@ message RemoveFromSlotResponse {
topfans.common.BaseResponse base = 1;
}
// ID移除展品请求
message RemoveExhibitionByAssetRequest {
int64 asset_id = 1; // ID
}
// ID移除展品响应
message RemoveExhibitionByAssetResponse {
topfans.common.BaseResponse base = 1;
}
// ==================== ====================
//

View File

@ -160,6 +160,8 @@ message ClaimExhibitionRevenueRequest {
message ClaimExhibitionRevenueResponse {
topfans.common.BaseResponse base = 1;
bool success = 2;
int64 crystal_amount = 3; //
int64 total_balance = 4; //
}
message ClaimAllExhibitionRevenueRequest {
@ -228,13 +230,13 @@ service TaskMobileService {
option (google.api.http) = { post: "/api/tasks/onboarding/claim-reward"; body: "*"; };
}
rpc GetExhibitionRevenue(GetExhibitionRevenueRequest) returns (GetExhibitionRevenueResponse) {
option (google.api.http) = { get: "/api/tasks/exhibition-revenue"; };
option (google.api.http) = { get: "/api/v1/tasks/exhibition-revenue"; };
}
rpc ClaimExhibitionRevenue(ClaimExhibitionRevenueRequest) returns (ClaimExhibitionRevenueResponse) {
option (google.api.http) = { post: "/api/tasks/exhibition-revenue/claim"; body: "*"; };
option (google.api.http) = { post: "/api/v1/tasks/exhibition-revenue/claim"; body: "*"; };
}
rpc ClaimAllExhibitionRevenue(ClaimAllExhibitionRevenueRequest) returns (ClaimAllExhibitionRevenueResponse) {
option (google.api.http) = { post: "/api/tasks/exhibition-revenue/claim-all"; body: "*"; };
option (google.api.http) = { post: "/api/v1/tasks/exhibition-revenue/claim-all"; body: "*"; };
}
}

View File

@ -0,0 +1,38 @@
-- ============================================
-- 展览表索引和外键修改
-- 移除 asset_id 的唯一约束,改为普通索引
-- 执行方式: psql -h <host> -U <user> -d <db> -f backend/scripts/migrate_exhibition_remove_unique_constraint.sql
-- 创建日期: 2026-05-15
-- ============================================
-- 1. 移除 asset_id 的唯一约束
ALTER TABLE exhibitions DROP CONSTRAINT IF EXISTS uk_asset;
-- 2. 添加普通索引(如果不存在)
CREATE INDEX IF NOT EXISTS idx_asset ON exhibitions(asset_id);
-- 3. 确认修改结果
DO $$
BEGIN
-- 检查唯一约束是否已移除
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'uk_asset'
) THEN
RAISE NOTICE '唯一约束 uk_asset 已成功移除';
ELSE
RAISE WARNING '唯一约束 uk_asset 仍然存在';
END IF;
-- 检查索引是否已创建
IF EXISTS (
SELECT 1 FROM pg_indexes WHERE indexname = 'idx_asset'
) THEN
RAISE NOTICE '索引 idx_asset 已成功创建';
ELSE
RAISE WARNING '索引 idx_asset 创建失败';
END IF;
END $$;
-- ============================================
-- 完成
-- ============================================

View File

@ -50,6 +50,12 @@ type AssetRepository interface {
// IsExhibiting 检查资产是否正在展出中
IsExhibiting(assetID int64) (bool, error)
// GetExhibitionStartTime 获取资产展出开始时间(如果正在展出)
GetExhibitionStartTime(assetID int64) (int64, error)
// GetExhibitionExpireTime 获取资产展出过期时间(如果正在展出)
GetExhibitionExpireTime(assetID int64) (int64, error)
}
// assetRepository 资产Repository实现
@ -336,3 +342,43 @@ func (r *assetRepository) IsExhibiting(assetID int64) (bool, error) {
return count > 0, nil
}
// GetExhibitionStartTime 获取资产展出开始时间(如果正在展出)
func (r *assetRepository) GetExhibitionStartTime(assetID int64) (int64, error) {
if assetID <= 0 {
return 0, errors.New("asset_id must be greater than 0")
}
var exhibition models.Exhibition
err := r.db.Where("asset_id = ? AND expire_at > ?", assetID, time.Now().UnixMilli()).
First(&exhibition).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil // 未展出
}
return 0, err
}
return exhibition.StartTime, nil
}
// GetExhibitionExpireTime 获取资产展出过期时间(如果正在展出)
func (r *assetRepository) GetExhibitionExpireTime(assetID int64) (int64, error) {
if assetID <= 0 {
return 0, errors.New("asset_id must be greater than 0")
}
var exhibition models.Exhibition
err := r.db.Where("asset_id = ? AND expire_at > ?", assetID, time.Now().UnixMilli()).
First(&exhibition).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil // 未展出
}
return 0, err
}
return exhibition.ExpireAt, nil
}

View File

@ -134,6 +134,6 @@ func GetExhibitionDuration() int64 {
zap.Error(err))
return 14400
}
// 数据库存储的是小时,转换为秒
return int64(config.ConfigValue) * 3600
// 数据库存储的是小时,转换为秒(先乘后转换,避免 int64(0.10)=0 的截断问题)
return int64(config.ConfigValue * 3600)
}

View File

@ -99,9 +99,28 @@ func main() {
// 创建 Repository 层实例
db := database.GetDB()
// 自动迁移展馆相关表booth_slots / exhibitions
if err := db.AutoMigrate(&models.BoothSlot{}, &models.Exhibition{}); err != nil {
logger.Logger.Fatal(fmt.Sprintf("Failed to migrate gallery tables: %v", err))
// 安全迁移:只在表不存在时才创建表,不自动重建
// 这样可以避免 Gorm AutoMigrate 在检测到索引不匹配时重建表导致数据丢失
if !db.Migrator().HasTable(&models.Exhibition{}) {
logger.Logger.Info("Exhibitions table does not exist, creating...")
if err := db.AutoMigrate(&models.Exhibition{}); err != nil {
logger.Logger.Fatal(fmt.Sprintf("Failed to migrate Exhibition table: %v", err))
}
logger.Logger.Info("Exhibition table created successfully")
} else {
logger.Logger.Info("Exhibitions table already exists, skipping AutoMigrate to preserve data")
}
// 同样处理 booth_slots 表
if !db.Migrator().HasTable(&models.BoothSlot{}) {
logger.Logger.Info("BoothSlots table does not exist, creating...")
if err := db.AutoMigrate(&models.BoothSlot{}); err != nil {
logger.Logger.Fatal(fmt.Sprintf("Failed to migrate BoothSlot table: %v", err))
}
logger.Logger.Info("BoothSlot table created successfully")
} else {
logger.Logger.Info("BoothSlots table already exists, skipping AutoMigrate to preserve data")
}
galleryRepo := repository.NewGalleryRepository(db)

View File

@ -421,6 +421,41 @@ func (p *GalleryProvider) GetUserExhibitedAssets(ctx context.Context, req *pb.Ge
return p.exhibitionService.GetUserExhibitedAssets(ctx, userID, starID, req)
}
// RemoveExhibitionByAsset 根据资产ID移除展品内部RPC用于领取收益后下架
func (p *GalleryProvider) RemoveExhibitionByAsset(ctx context.Context, req *pb.RemoveExhibitionByAssetRequest) (*pb.RemoveExhibitionByAssetResponse, error) {
logger.Logger.Info("Received RemoveExhibitionByAsset request",
zap.Int64("asset_id", req.AssetId),
)
// 调用Service层
err := p.exhibitionService.RemoveExhibitionByAsset(ctx, req.AssetId)
if err != nil {
logger.Logger.Error("RemoveExhibitionByAsset failed",
zap.Int64("asset_id", req.AssetId),
zap.Error(err),
)
return &pb.RemoveExhibitionByAssetResponse{
Base: &pbCommon.BaseResponse{
Code: appErrors.ToStatusCode(err),
Message: err.Error(),
Timestamp: 0,
},
}, err
}
logger.Logger.Info("RemoveExhibitionByAsset successful",
zap.Int64("asset_id", req.AssetId),
)
return &pb.RemoveExhibitionByAssetResponse{
Base: &pbCommon.BaseResponse{
Code: pbCommon.StatusCode_STATUS_OK,
Message: "success",
Timestamp: 0,
},
}, nil
}
// ==================== 辅助函数 ====================
// extractUserInfoFromDubboAttachments 从Dubbo attachments提取用户信息

View File

@ -23,6 +23,7 @@ type GalleryRepository interface {
// 展品相关
GetExhibitionByAsset(assetID int64) (*models.Exhibition, error)
GetExhibitionBySlot(slotID int64) (*models.Exhibition, error)
GetActiveExhibitionBySlot(slotID, now int64) (*models.Exhibition, error)
GetExhibitionsByUser(userID, starID int64) ([]*models.Exhibition, error)
CreateExhibition(exhibition *models.Exhibition) error
DeleteExhibition(exhibitionID int64) error
@ -37,6 +38,8 @@ type GalleryRepository interface {
PlaceExhibitionTx(exhibition *models.Exhibition, displayStatus int32) error
// 事务性操作:删除展品并更新展示状态(原子操作)
RemoveExhibitionTx(exhibitionID int64, assetID int64) error
// 更新展览过期时间(用于清理后防止重复处理)
UpdateExhibitionExpireAt(exhibitionID int64, expireAt int64) error
// ========== 我的作品相关 ==========
@ -227,6 +230,19 @@ func (r *galleryRepository) GetExhibitionBySlot(slotID int64) (*models.Exhibitio
return &exhibition, nil
}
// GetActiveExhibitionBySlot 根据展位ID获取活跃展品展示记录未删除且未过期
func (r *galleryRepository) GetActiveExhibitionBySlot(slotID, now int64) (*models.Exhibition, error) {
var exhibition models.Exhibition
err := r.db.Where("slot_id = ? AND deleted_at IS NULL AND expire_at > ?", slotID, now).First(&exhibition).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &exhibition, nil
}
// GetExhibitionsByUser 获取用户的所有展品展示记录(不含已删除)
func (r *galleryRepository) GetExhibitionsByUser(userID, starID int64) ([]*models.Exhibition, error) {
var exhibitions []*models.Exhibition
@ -235,24 +251,27 @@ func (r *galleryRepository) GetExhibitionsByUser(userID, starID int64) ([]*model
return exhibitions, err
}
// CreateExhibition 创建展品展示记录(支持软删除后重新展出)
// asset_id 唯一索引冲突时:若旧记录已软删除则物理删除后重新插入;否则报错
// CreateExhibition 创建展品展示记录
// 允许多个 asset_id 记录,但同一 asset_id 只能有一个未删除且未过期的展出记录
func (r *galleryRepository) CreateExhibition(exhibition *models.Exhibition) error {
now := time.Now().UnixMilli()
// 检查是否有未删除且未过期的同一 asset_id 记录
var count int64
err := r.db.Model(&models.Exhibition{}).
Where("asset_id = ? AND deleted_at IS NULL AND expire_at > ?", exhibition.AssetID, now).
Count(&count).Error
if err != nil {
return err
}
if count > 0 {
return errors.New("该藏品正在展出中,无法重复放置")
}
exhibition.CreatedAt = now
exhibition.UpdatedAt = now
exhibition.DeletedAt = nil
// 在事务中:先物理删除已软删除的旧记录,再插入新记录
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Exec(`
DELETE FROM exhibitions
WHERE asset_id = ? AND deleted_at IS NOT NULL
`, exhibition.AssetID).Error; err != nil {
return err
}
return tx.Create(exhibition).Error
})
return r.db.Create(exhibition).Error
}
// DeleteExhibition 软删除展品展示记录根据ID
@ -286,13 +305,14 @@ func (r *galleryRepository) GetExpiredExhibitions(beforeTime int64) ([]*models.E
// GetAssetsWithInvalidDisplayStatus 获取 display_status=1 但没有有效 exhibition 的资产ID列表
// 用于清理手动软删除导致的 display_status 不同步问题
// 注意:这里只要 exhibition 未删除deleted_at IS NULL就认为有效包含已过期未领取的
func (r *galleryRepository) GetAssetsWithInvalidDisplayStatus() ([]int64, error) {
var assetIDs []int64
// 子查询:查找有有效 exhibition 记录的 asset_id
// 子查询:查找有有效 exhibition 记录的 asset_id(不管是否过期,只要未删除)
subQuery := r.db.Model(&models.Exhibition{}).
Select("asset_id").
Where("deleted_at IS NULL AND expire_at > ?", time.Now().UnixMilli())
Where("deleted_at IS NULL")
// 查找 display_status=1 但没有有效 exhibition 记录的资产
err := r.db.Model(&models.AssetRegistry{}).
@ -313,23 +333,29 @@ func (r *galleryRepository) UpdateAssetRegistryDisplayStatus(assetID int64, disp
// PlaceExhibitionTx 事务性创建展品并更新展示状态(原子操作)
func (r *galleryRepository) PlaceExhibitionTx(exhibition *models.Exhibition, displayStatus int32) error {
now := time.Now().UnixMilli()
// 检查是否有未删除且未过期的同一 asset_id 记录
var count int64
err := r.db.Model(&models.Exhibition{}).
Where("asset_id = ? AND deleted_at IS NULL AND expire_at > ?", exhibition.AssetID, now).
Count(&count).Error
if err != nil {
return err
}
if count > 0 {
return errors.New("该藏品正在展出中,无法重复放置")
}
exhibition.CreatedAt = now
exhibition.UpdatedAt = now
exhibition.DeletedAt = nil
return r.db.Transaction(func(tx *gorm.DB) error {
// 1. 物理删除已软删除的旧记录
if err := tx.Exec(`
DELETE FROM exhibitions
WHERE asset_id = ? AND deleted_at IS NOT NULL
`, exhibition.AssetID).Error; err != nil {
return err
}
// 2. 插入新记录
// 1. 插入新记录(允许多个 asset_id 记录)
if err := tx.Create(exhibition).Error; err != nil {
return err
}
// 3. 更新展示状态(与展出操作在同一事务中)
// 2. 更新展示状态(与展出操作在同一事务中)
if err := tx.Model(&models.AssetRegistry{}).
Where("asset_id = ?", exhibition.AssetID).
Update("display_status", displayStatus).Error; err != nil {
@ -363,24 +389,30 @@ func (r *galleryRepository) RemoveExhibitionTx(exhibitionID int64, assetID int64
})
}
// UpdateExhibitionExpireAt 更新展览过期时间(用于清理后防止重复处理)
func (r *galleryRepository) UpdateExhibitionExpireAt(exhibitionID int64, expireAt int64) error {
return r.db.Model(&models.Exhibition{}).
Where("id = ?", exhibitionID).
Update("expire_at", expireAt).Error
}
// ========== 我的作品相关实现 ==========
// GetMyExhibitedAssets 获取我展出的作品列表(只返回展出中且未过期的,含收益)
// GetMyExhibitedAssets 获取我展出的作品列表(返回展出中且未下架的,含收益)
// 包含未过期和已过期但未领取的展品,用户领取收益时才下架
func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pageSize int) ([]*ExhibitedAssetInfo, int64, error) {
var items []*ExhibitedAssetInfo
var total int64
now := time.Now().UnixMilli()
// 计数查询
// 计数查询只要未下架deleted_at IS NULL都显示包含已过期待领取的
err := r.db.Model(&models.Exhibition{}).
Where("occupier_uid = ? AND occupier_star_id = ? AND deleted_at IS NULL AND expire_at > ?", userID, starID, now).
Where("occupier_uid = ? AND occupier_star_id = ? AND deleted_at IS NULL", userID, starID).
Count(&total).Error
if err != nil {
return nil, 0, err
}
// 数据查询
// 数据查询:只过滤已下架的,不过滤过期(用户领取收益时才下架)
offset := (page - 1) * pageSize
err = r.db.Model(&models.Exhibition{}).
Raw(`
@ -390,10 +422,10 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag
JOIN assets a ON a.id = exhibitions.asset_id
JOIN booth_slots bs ON bs.slot_id = exhibitions.slot_id
WHERE exhibitions.occupier_uid = ? AND exhibitions.occupier_star_id = ?
AND exhibitions.deleted_at IS NULL AND exhibitions.expire_at > ?
ORDER BY bs.slot_index ASC
AND exhibitions.deleted_at IS NULL
ORDER BY exhibitions.expire_at DESC, bs.slot_index ASC
LIMIT ? OFFSET ?
`, userID, starID, now, pageSize, offset).Scan(&items).Error
`, userID, starID, pageSize, offset).Scan(&items).Error
if err != nil {
return nil, 0, err
@ -401,6 +433,7 @@ func (r *galleryRepository) GetMyExhibitedAssets(userID, starID int64, page, pag
// 实时计算展示收益R1 = R0 × T × [100% + Buff(n)]
// R0 = 5 水晶/小时
now := time.Now().UnixMilli()
for _, item := range items {
item.Earnings = calculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now)
}
@ -423,20 +456,17 @@ func (r *galleryRepository) GetUserExhibitedAssets(userID, starID int64, page, p
return nil, 0, err
}
// 数据查询
// 数据查询(实时计算收益)
offset := (page - 1) * pageSize
err = r.db.Model(&models.Exhibition{}).
Raw(`
SELECT exhibitions.asset_id, a.name, a.cover_url, a.like_count,
exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index,
COALESCE(CAST(SUM(err.crystal_amount) / 10 AS bigint), 0) as earnings
exhibitions.start_time as exhibited_at, exhibitions.expire_at, bs.slot_index
FROM exhibitions
JOIN assets a ON a.id = exhibitions.asset_id
JOIN booth_slots bs ON bs.slot_id = exhibitions.slot_id
LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable'
WHERE exhibitions.occupier_uid = ? AND exhibitions.occupier_star_id = ?
AND exhibitions.deleted_at IS NULL AND exhibitions.expire_at > ?
GROUP BY exhibitions.asset_id, a.name, a.cover_url, a.like_count, exhibitions.start_time, exhibitions.expire_at, bs.slot_index
ORDER BY bs.slot_index ASC
LIMIT ? OFFSET ?
`, userID, starID, now, pageSize, offset).Scan(&items).Error
@ -445,6 +475,11 @@ func (r *galleryRepository) GetUserExhibitedAssets(userID, starID int64, page, p
return nil, 0, err
}
// 实时计算每个资产的收益
for _, item := range items {
item.Earnings = calculateRealtimeEarnings(item.LikeCount, item.ExhibitedAt, now)
}
return items, total, nil
}

View File

@ -169,5 +169,5 @@ func GetMaxFriends() int {
zap.Error(err))
return 50
}
return config.ConfigValue
return int(config.ConfigValue)
}

View File

@ -578,33 +578,29 @@ func (r *socialRepositoryImpl) GetMyLikedAssets(userID, starID int64, page, page
var items []*LikedAssetInfo
var total int64
// 子查询:查找当前时间未过期的展出记录
now := time.Now().UnixMilli()
// 计数查询(使用 DISTINCT 因为一个资产可能在多个展位展出)
// 只要资产未删除且未下架就显示,包含已过期的(用户可继续查看点赞记录)
countQuery := r.db.Model(&models.AssetLike{}).
Joins("JOIN assets a ON a.id = asset_likes.asset_id").
Joins("JOIN exhibitions e ON e.asset_id = a.id").
Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID).
Where("a.deleted_at IS NULL AND a.is_active = ?", true).
Where("e.deleted_at IS NULL AND e.expire_at > ?", now)
Where("e.deleted_at IS NULL")
if err := countQuery.Distinct("asset_likes.asset_id").Count(&total).Error; err != nil {
return nil, 0, err
}
// 数据查询
// 数据查询:只过滤已下架的,不过滤过期
offset := (page - 1) * pageSize
err := r.db.Model(&models.AssetLike{}).
Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count,
asset_likes.created_at as liked_at,
COALESCE(SUM(err.crystal_amount), 0) as earnings`).
asset_likes.created_at as liked_at`).
Joins("JOIN assets a ON a.id = asset_likes.asset_id").
Joins("JOIN exhibitions e ON e.asset_id = a.id").
Joins("LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable'").
Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID).
Where("a.deleted_at IS NULL AND a.is_active = ?", true).
Where("e.deleted_at IS NULL AND e.expire_at > ?", now).
Where("e.deleted_at IS NULL").
Group("asset_likes.asset_id, a.name, a.cover_url, a.like_count, asset_likes.created_at").
Order("asset_likes.created_at DESC").
Limit(pageSize).
@ -615,9 +611,54 @@ func (r *socialRepositoryImpl) GetMyLikedAssets(userID, starID int64, page, page
return nil, 0, err
}
// 实时计算每个资产的收益
now := time.Now().UnixMilli()
for _, item := range items {
// 获取该资产的展出开始时间(如果还在展出中就计算收益)
var exhibition models.Exhibition
if err := r.db.Where("asset_id = ? AND deleted_at IS NULL", item.AssetID).
First(&exhibition).Error; err == nil {
item.Earnings = calculateRealtimeEarnings(item.LikeCount, exhibition.StartTime, now)
}
}
return items, total, nil
}
// calculateRealtimeEarnings 实时计算展示收益
// 公式R1 = R0 × T × [100% + Buff(n)]
// R0 = 5 水晶/小时T = 上架时长小时Buff(n) 根据点赞数计算
func calculateRealtimeEarnings(likeCount int32, startTime, now int64) int64 {
R0 := int64(5) // 水晶/小时
// 计算上架时长(毫秒转小时)
T := (now - startTime) / 3600000
if T <= 0 {
T = 1 // 最少1小时
}
// 计算Buff
var buff int
switch {
case likeCount >= 30:
buff = 30
case likeCount >= 10:
buff = 20
case likeCount >= 5:
buff = 10
default:
buff = 0
}
// 基础收益
baseRevenue := R0 * T
// 应用Buff加成R1 = R0 × T × (100% + Buff)
buffedRevenue := baseRevenue * (100 + int64(buff)) / 100
return buffedRevenue
}
// GetMyTodayLikedAssets 获取我今日点赞的作品列表(只返回展出中且未过期的)
func (r *socialRepositoryImpl) GetMyTodayLikedAssets(userID, starID int64, page, pageSize int) ([]*LikedAssetInfo, int64, error) {
now := time.Now()
@ -641,11 +682,9 @@ func (r *socialRepositoryImpl) GetMyTodayLikedAssets(userID, starID int64, page,
offset := (page - 1) * pageSize
err := r.db.Model(&models.AssetLike{}).
Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count,
asset_likes.created_at as liked_at,
COALESCE(SUM(err.crystal_amount), 0) as earnings`).
asset_likes.created_at as liked_at`).
Joins("JOIN assets a ON a.id = asset_likes.asset_id").
Joins("JOIN exhibitions e ON e.asset_id = a.id").
Joins("LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable'").
Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID).
Where("asset_likes.created_at >= ?", startOfDay).
Where("a.deleted_at IS NULL AND a.is_active = ?", true).
@ -660,6 +699,15 @@ func (r *socialRepositoryImpl) GetMyTodayLikedAssets(userID, starID int64, page,
return nil, 0, err
}
// 实时计算每个资产的收益
for _, item := range items {
var exhibition models.Exhibition
if err := r.db.Where("asset_id = ? AND deleted_at IS NULL AND expire_at > ?", item.AssetID, now.UnixMilli()).
First(&exhibition).Error; err == nil {
item.Earnings = calculateRealtimeEarnings(item.LikeCount, exhibition.StartTime, now.UnixMilli())
}
}
return items, total, nil
}
@ -693,11 +741,9 @@ func (r *socialRepositoryImpl) GetMyWeekLikedAssets(userID, starID int64, page,
offset := (page - 1) * pageSize
err := r.db.Model(&models.AssetLike{}).
Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count,
asset_likes.created_at as liked_at,
COALESCE(SUM(err.crystal_amount), 0) as earnings`).
asset_likes.created_at as liked_at`).
Joins("JOIN assets a ON a.id = asset_likes.asset_id").
Joins("JOIN exhibitions e ON e.asset_id = a.id").
Joins("LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable'").
Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID).
Where("asset_likes.created_at >= ?", startOfWeekMillis).
Where("a.deleted_at IS NULL AND a.is_active = ?", true).
@ -712,6 +758,16 @@ func (r *socialRepositoryImpl) GetMyWeekLikedAssets(userID, starID int64, page,
return nil, 0, err
}
// 实时计算每个资产的收益
nowMillis := now.UnixMilli()
for _, item := range items {
var exhibition models.Exhibition
if err := r.db.Where("asset_id = ? AND deleted_at IS NULL AND expire_at > ?", item.AssetID, nowMillis).
First(&exhibition).Error; err == nil {
item.Earnings = calculateRealtimeEarnings(item.LikeCount, exhibition.StartTime, nowMillis)
}
}
return items, total, nil
}
@ -736,11 +792,9 @@ func (r *socialRepositoryImpl) GetUserLikedAssets(userID, starID int64, page, pa
offset := (page - 1) * pageSize
err := r.db.Model(&models.AssetLike{}).
Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count,
asset_likes.created_at as liked_at,
COALESCE(SUM(err.crystal_amount), 0) as earnings`).
asset_likes.created_at as liked_at`).
Joins("JOIN assets a ON a.id = asset_likes.asset_id").
Joins("JOIN exhibitions e ON e.asset_id = a.id").
Joins("LEFT JOIN exhibition_revenue_records err ON err.asset_id = a.id AND err.status = 'claimable'").
Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID).
Where("a.deleted_at IS NULL AND a.is_active = ?", true).
Where("e.deleted_at IS NULL AND e.expire_at > ?", now).
@ -754,5 +808,14 @@ func (r *socialRepositoryImpl) GetUserLikedAssets(userID, starID int64, page, pa
return nil, 0, err
}
// 实时计算每个资产的收益
for _, item := range items {
var exhibition models.Exhibition
if err := r.db.Where("asset_id = ? AND deleted_at IS NULL AND expire_at > ?", item.AssetID, now).
First(&exhibition).Error; err == nil {
item.Earnings = calculateRealtimeEarnings(item.LikeCount, exhibition.StartTime, now)
}
}
return items, total, nil
}

View File

@ -0,0 +1,40 @@
package client
import (
"context"
"fmt"
"github.com/topfans/backend/pkg/logger"
pbCommon "github.com/topfans/backend/pkg/proto/common"
pbGallery "github.com/topfans/backend/pkg/proto/gallery"
"go.uber.org/zap"
)
type GalleryServiceClient interface {
RemoveExhibitionByAsset(ctx context.Context, assetID int64) error
}
type galleryServiceClient struct {
client pbGallery.GalleryService
}
func NewGalleryServiceClient(client pbGallery.GalleryService) GalleryServiceClient {
return &galleryServiceClient{client: client}
}
func (c *galleryServiceClient) RemoveExhibitionByAsset(ctx context.Context, assetID int64) error {
logger.Logger.Debug("Calling GalleryService.RemoveExhibitionByAsset",
zap.Int64("asset_id", assetID))
resp, err := c.client.RemoveExhibitionByAsset(ctx, &pbGallery.RemoveExhibitionByAssetRequest{
AssetId: assetID,
})
if err != nil {
logger.Logger.Error("GalleryService.RemoveExhibitionByAsset failed", zap.Error(err))
return err
}
if resp.Base.Code != pbCommon.StatusCode_STATUS_OK {
logger.Logger.Warn("RemoveExhibitionByAsset non-zero code", zap.Int32("code", int32(resp.Base.Code)), zap.String("message", resp.Base.Message))
return fmt.Errorf("RemoveExhibitionByAsset failed with code: %d, message: %s", resp.Base.Code, resp.Base.Message)
}
return nil
}

View File

@ -14,7 +14,8 @@ type DatabaseConfig struct {
}
type ServiceURLs struct {
UserService string
UserService string
GalleryService string
}
type WorkerConfig struct {
@ -25,7 +26,7 @@ type WorkerConfig struct {
var (
DBConfig = &DatabaseConfig{}
ServiceURLsConfig = &ServiceURLs{UserService: "tri://localhost:20000"}
ServiceURLsConfig = &ServiceURLs{UserService: "tri://localhost:20000", GalleryService: "tri://localhost:20004"}
WorkerConfigData = &WorkerConfig{
ResetHour: 5, ResetMinute: 0,
RevenueBatchSize: 100, RevenueMaxRetries: 3,
@ -52,6 +53,7 @@ func InitConfig() {
flag.StringVar(&DBConfig.DBName, "db-name", getEnv("DB_NAME", "topfans"), "数据库名称")
flag.StringVar(&DBConfig.SSLMode, "db-sslmode", "disable", "数据库 SSL 模式")
flag.StringVar(&ServiceURLsConfig.UserService, "user-service-url", getEnv("USER_SERVICE_URL", "tri://localhost:20000"), "User Service RPC 地址")
flag.StringVar(&ServiceURLsConfig.GalleryService, "gallery-service-url", getEnv("GALLERY_SERVICE_URL", "tri://localhost:20004"), "Gallery Service RPC 地址")
flag.IntVar(&WorkerConfigData.ResetHour, "reset-hour", getEnvInt("RESET_HOUR", 5), "每日重置小时Asia/Shanghai")
flag.IntVar(&WorkerConfigData.ResetMinute, "reset-minute", getEnvInt("RESET_MINUTE", 0), "每日重置分钟")
flag.IntVar(&WorkerConfigData.RevenueBatchSize, "revenue-batch-size", getEnvInt("REVENUE_BATCH_SIZE", 100), "收益自动发放批次大小")
@ -60,5 +62,6 @@ func InitConfig() {
log.Println("taskService 配置初始化完成")
log.Printf(" 数据库: %s:%d/%s", DBConfig.Host, DBConfig.Port, DBConfig.DBName)
log.Printf(" User Service: %s", ServiceURLsConfig.UserService)
log.Printf(" Gallery Service: %s", ServiceURLsConfig.GalleryService)
log.Printf(" 重置时间: %02d:%02d Asia/Shanghai", WorkerConfigData.ResetHour, WorkerConfigData.ResetMinute)
}

View File

@ -15,6 +15,7 @@ import (
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/logger"
pb "github.com/topfans/backend/pkg/proto/task"
pbGallery "github.com/topfans/backend/pkg/proto/gallery"
pbUser "github.com/topfans/backend/pkg/proto/user"
"github.com/topfans/backend/services/taskService/client"
"github.com/topfans/backend/services/taskService/config"
@ -89,10 +90,24 @@ func main() {
userRPCClient := client.NewUserServiceClient(userServiceClient)
logger.Logger.Info("User RPC client initialized")
// 5.1 Init galleryService Dubbo client + RPC client
galleryCli, err := dubboclient.NewClient(
dubboclient.WithClientURL(config.ServiceURLsConfig.GalleryService),
)
if err != nil {
logger.Logger.Fatal(fmt.Sprintf("Failed to create galleryService client: %v", err))
}
galleryServiceClient, err := pbGallery.NewGalleryService(galleryCli)
if err != nil {
logger.Logger.Fatal(fmt.Sprintf("Failed to get galleryService: %v", err))
}
galleryRPCClient := client.NewGalleryServiceClient(galleryServiceClient)
logger.Logger.Info("Gallery RPC client initialized")
// 6. Init services
dailySvc := service.NewDailyTaskService(dailyRepo, userRPCClient)
onboardingSvc := service.NewOnboardingService(onboardingRepo, dailyRepo, userRPCClient)
revenueSvc := service.NewRevenueService(revenueRepo, userRPCClient)
revenueSvc := service.NewRevenueService(revenueRepo, userRPCClient, galleryRPCClient)
logger.Logger.Info("Services initialized")
// 7. Init workergoroutine 中启动)

View File

@ -25,10 +25,10 @@ type FanProfileRepository interface {
// Create 创建粉丝档案
Create(profile *models.FanProfile) error
// GetByUserAndStar 根据user_id + star_id查询
// GetByUserAndStar 根据user_id + star_id查询(等级为实时计算,基于累计时长+当前展出中时长)
GetByUserAndStar(userID, starID int64) (*models.FanProfile, error)
// GetByUserID 查询用户的所有粉丝身份
// GetByUserID 查询用户的所有粉丝身份(等级为实时计算)
GetByUserID(userID int64) ([]*models.FanProfile, error)
// ExistsByNickname 检查昵称是否已存在
@ -117,7 +117,7 @@ func (r *fanProfileRepository) Create(profile *models.FanProfile) error {
return nil
}
// GetByUserAndStar 根据user_id + star_id查询
// GetByUserAndStar 根据user_id + star_id查询(等级为实时计算,基于累计时长+当前展出中时长)
func (r *fanProfileRepository) GetByUserAndStar(userID, starID int64) (*models.FanProfile, error) {
if userID <= 0 {
return nil, errors.New("user_id must be greater than 0")
@ -136,10 +136,49 @@ func (r *fanProfileRepository) GetByUserAndStar(userID, starID int64) (*models.F
return nil, err
}
// 实时计算等级:累计时长 + 当前展出中时长
profile.Level = r.calculateRealtimeLevel(userID, starID)
return &profile, nil
}
// GetByUserID 查询用户的所有粉丝身份
// calculateRealtimeLevel 实时计算用户等级(基于累计时长 + 当前展出中的展品时长)
func (r *fanProfileRepository) calculateRealtimeLevel(userID, starID int64) int32 {
// 1. 获取累计时长
var exhibitionHours models.UserExhibitionHours
err := r.db.Where("user_id = ? AND star_id = ?", userID, starID).First(&exhibitionHours).Error
if err != nil {
return 1
}
totalHours := exhibitionHours.TotalExhibitionHours
// 2. 获取当前展出中的展品累计时长(从开始到现在)
now := time.Now().UnixMilli()
var activeHours int64
var exhibitions []models.Exhibition
if err := r.db.Where("occupier_uid = ? AND occupier_star_id = ? AND deleted_at IS NULL AND expire_at > ?", userID, starID, now).
Find(&exhibitions).Error; err == nil {
for _, e := range exhibitions {
activeHours += (now - e.StartTime) / 3600000
}
}
// 3. 取较大值:累计值 vs 当前展出中的实际值(防止负数)
realtimeHours := totalHours
if activeHours > realtimeHours {
realtimeHours = activeHours
}
// 4. 计算等级
maxLevel := GetLevelCap()
newLevel := CalculateLevelFromExhibitionHours(realtimeHours)
if newLevel > maxLevel {
newLevel = maxLevel
}
return newLevel
}
// GetByUserID 查询用户的所有粉丝身份(等级为实时计算)
func (r *fanProfileRepository) GetByUserID(userID int64) ([]*models.FanProfile, error) {
if userID <= 0 {
return nil, errors.New("user_id must be greater than 0")
@ -152,6 +191,11 @@ func (r *fanProfileRepository) GetByUserID(userID int64) ([]*models.FanProfile,
return nil, err
}
// 实时计算每个身份的等级
for _, profile := range profiles {
profile.Level = r.calculateRealtimeLevel(userID, profile.StarID)
}
return profiles, nil
}

View File

@ -181,7 +181,7 @@ const handleNavClick = (index) => {
.monster-icon {
width: 100%;
height: 100%;
filter: drop-shadow(0 8rpx 24rpx rgba(255, 107, 157, 0.5)) drop-shadow(0 0 40rpx rgba(255, 140, 180, 0.3));
/* filter: drop-shadow(0 8rpx 24rpx rgba(255, 107, 157, 0.5)) drop-shadow(0 0 40rpx rgba(255, 140, 180, 0.3)); */
transition: filter 0.3s ease, transform 0.3s ease;
animation: breathe 2s ease-in-out infinite;
}
@ -235,7 +235,7 @@ const handleNavClick = (index) => {
height: 100rpx;
margin-bottom: 8rpx;
transition: transform 0.3s ease, filter 0.3s ease;
filter: drop-shadow(0 6rpx 16rpx rgba(0, 0, 0, 0.4)) drop-shadow(0 2rpx 8rpx rgba(0, 0, 0, 0.2));
/* filter: drop-shadow(0 6rpx 16rpx rgba(0, 0, 0, 0.4)) dr8op-shadow(0 2rpx 8rpx rgba(0, 0, 0, 0.2)); */
position: relative;
}

View File

@ -169,6 +169,7 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { getMyExhibitedAssetsApi, getMyLikedAssetsApi, getMyTodayLikedAssetsApi, getMyWeekLikedAssetsApi, getMyGalleriesApi, placeAssetToGalleryApi } from '@/utils/api.js';
import { getExhibitionRevenue, claimExhibitionRevenue } from '@/utils/task-api.js';
import AssetSelector from '../components/AssetSelector.vue';
import { onShow } from '@dcloudio/uni-app';
import { doubleTapLike } from '@/utils/likeHelper.js';
@ -276,9 +277,46 @@ const goToAssetDetail = (id) => {
const cardTapTimers = {};
//
const handleClaimReward = (item, _index) => {
const handleClaimReward = async (item, _index) => {
console.log('领取收益:', item);
uni.showToast({ title: '收益已领取', icon: 'success' });
uni.showLoading({ title: '领取中...' });
try {
// star_id
const starId = uni.getStorageSync('star_id') || 1;
//
const res = await getExhibitionRevenue(starId, 'claimable', 1, 100);
const records = res.data?.items || [];
// ID
const revenueRecord = records.find(r => r.asset_id === item.id);
if (!revenueRecord) {
uni.showToast({ title: '暂无可领取收益', icon: 'none' });
return;
}
//
const claimRes = await claimExhibitionRevenue(revenueRecord.id, starId);
uni.showToast({ title: '收益已领取', icon: 'success' });
//
if (claimRes?.data?.total_balance !== undefined) {
uni.$emit('balanceUpdated', { crystal_balance: claimRes.data.total_balance });
}
//
await loadExhibitedAssets();
} catch (err) {
console.error('领取收益失败:', err);
uni.showToast({ title: err.message || '领取失败', icon: 'none' });
} finally {
uni.hideLoading();
}
};
const handleExhibitionCardTap = (item, index) => {