diff --git a/CLAUDE.md b/CLAUDE.md index d3dc9b4..920fa7c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,3 +36,62 @@ Fall back to Grep/Glob/Read **only** when the graph doesn't cover what you need. 2. Use `detect_changes` for code review. 3. Use `get_affected_flows` to understand impact. 4. Use `query_graph` pattern="tests_for" to check coverage. + +--- + +## 数据库操作规范 + +### PostgreSQL 序列同步规则(强制) + +**问题背景**:项目使用 `BIGSERIAL` / `autoIncrement` 自增主键。当通过 SQL 手动 `INSERT` 指定 `id` 值时,PostgreSQL 序列不会自动跟进,导致后续 GORM 插入报 `duplicate key value violates unique constraint`。 + +**触发场景**: +- 测试数据脚本(如 `create_gallery_test_users.go`)硬编码 ID +- 数据迁移脚本手动插入记录 +- DBeaver / psql 手动补数据 + +**规范要求**: + +1. **任何手动指定 ID 的 INSERT 语句,末尾必须同步重置序列**: + ```sql + -- 错误示例(会导致序列不同步) + INSERT INTO assets (id, name, ...) VALUES (1000, 'xxx', ...); + + -- 正确示例 + INSERT INTO assets (id, name, ...) VALUES (1000, 'xxx', ...); + SELECT setval('assets_id_seq', (SELECT MAX(id) FROM assets)); + ``` + +2. **脚本文件规范**:所有输出 SQL 的 Go 脚本(如 `backend/scripts/*.go`),必须在生成的 SQL 末尾包含序列重置语句: + ```go + fmt.Printf(`SELECT setval('%s_id_seq', (SELECT MAX(id) FROM %s));\n`, tableName, tableName) + ``` + +3. **新表创建时**:预留足够的序列起始值给测试数据: + ```sql + CREATE SEQUENCE assets_id_seq START WITH 10000; + ``` + +4. **定期检查**:环境部署后执行以下 SQL 确认序列健康: + ```sql + SELECT + schemaname, sequencename, last_value, + (SELECT MAX(id) FROM assets) AS table_max_id, + last_value >= (SELECT MAX(id) FROM assets) AS is_healthy + FROM pg_sequences + WHERE sequencename = 'assets_id_seq'; + ``` + +**受影响表**(使用 autoIncrement 主键): +- `assets` +- `asset_registry` +- `users` +- `stars` +- `activity_assets` +- `collection_assets` +- `materials` +- `exhibitions` +- `galleries` +- 以及其他所有 `id BIGSERIAL PRIMARY KEY` 的表 + +**违规后果**:生产环境报 `duplicate key` 导致用户铸造/创建失败,需紧急修复序列。 diff --git a/backend/services/assetService/service/asset_service.go b/backend/services/assetService/service/asset_service.go index 646a803..b60aa47 100644 --- a/backend/services/assetService/service/asset_service.go +++ b/backend/services/assetService/service/asset_service.go @@ -437,8 +437,9 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) ( } } - // 3. 获取所有者的昵称(在该star下的nickname) + // 3. 获取所有者的昵称和头像(在该star下的nickname) var ownerNickname string + var ownerAvatar string profile, err := s.userClient.GetFanProfile(context.Background(), asset.OwnerUID, asset.StarID) if err != nil { logger.Logger.Warn("Failed to get owner fan profile, using fallback nickname", @@ -450,6 +451,7 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) ( ownerNickname = fmt.Sprintf("User%d", asset.OwnerUID) } else { ownerNickname = profile.Nickname + ownerAvatar = profile.AvatarUrl } // 4. 检查当前用户是否已点赞(需要获取当前展出中的 exhibition_id) @@ -513,7 +515,7 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) ( Message: "", Timestamp: time.Now().UnixMilli(), }, - Asset: ModelToProtoAssetDetail(asset, ownerNickname, isLiked, displayStatus, earnings, hourlyEarnings, exhibitionExpireAt, grade), + Asset: ModelToProtoAssetDetail(asset, ownerNickname, ownerAvatar, isLiked, displayStatus, earnings, hourlyEarnings, exhibitionExpireAt, grade), } logger.Logger.Debug("Get asset successful", @@ -662,11 +664,21 @@ func ModelToProtoAsset(asset *models.Asset) *pb.AssetListItem { } // ModelToProtoAssetDetail 将数据库模型转换为Proto格式(Asset详情) -func ModelToProtoAssetDetail(asset *models.Asset, ownerNickname string, isLiked bool, displayStatus int32, earnings int64, hourlyEarnings float64, exhibitionExpireAt int64, grade int32) *pb.Asset { +func ModelToProtoAssetDetail(asset *models.Asset, ownerNickname string, ownerAvatar string, isLiked bool, displayStatus int32, earnings int64, hourlyEarnings float64, exhibitionExpireAt int64, grade int32) *pb.Asset { if asset == nil { return nil } + // 构建持有者信息 + var ownerInfo *pb.OwnerInfo + if ownerAvatar != "" || ownerNickname != "" { + ownerInfo = &pb.OwnerInfo{ + UserId: asset.OwnerUID, + Nickname: ownerNickname, + Avatar: ownerAvatar, + } + } + return &pb.Asset{ AssetId: asset.ID, OwnerUid: asset.OwnerUID, @@ -685,14 +697,14 @@ func ModelToProtoAssetDetail(asset *models.Asset, ownerNickname string, isLiked CreatedAt: asset.CreatedAt, UpdatedAt: asset.UpdatedAt, MintedAt: getInt64Value(asset.MintedAt), - Owner: nil, // 保留用于兼容性 - OwnerNickname: ownerNickname, // 持有者昵称 - IsLiked: isLiked, // 当前用户是否已点赞 + Owner: ownerInfo, + OwnerNickname: ownerNickname, + IsLiked: isLiked, Info: asset.Info, - DisplayStatus: displayStatus, // 展示状态:0=待展示, 1=已展示 - Earnings: earnings, // 当前展出收益(实时计算) - HourlyEarnings: hourlyEarnings, // 每小时收益(实时计算) - ExhibitionExpireAt: exhibitionExpireAt, // 展出过期时间 + DisplayStatus: displayStatus, + Earnings: earnings, + HourlyEarnings: hourlyEarnings, + ExhibitionExpireAt: exhibitionExpireAt, } } diff --git a/backend/services/assetService/service/mint_service.go b/backend/services/assetService/service/mint_service.go index 22a868a..b273a76 100644 --- a/backend/services/assetService/service/mint_service.go +++ b/backend/services/assetService/service/mint_service.go @@ -453,8 +453,9 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st // 5. 无需异步 AI 处理,cover_url 已在步骤 3.2 中直接设置 - // 5. 获取所有者的昵称(创建时所有者就是当前用户) + // 5. 获取所有者的昵称和头像(创建时所有者就是当前用户) var ownerNickname string + var ownerAvatar string profile, err := s.userClient.GetFanProfile(context.Background(), userID, starID) if err != nil { logger.Logger.Warn("Failed to get owner fan profile, will return without nickname", @@ -465,6 +466,7 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st ownerNickname = "" } else { ownerNickname = profile.Nickname + ownerAvatar = profile.AvatarUrl } // 6. 构建响应 @@ -476,7 +478,7 @@ func (s *mintService) CreateMintOrder(req *pb.CreateMintOrderRequest, userID, st Timestamp: time.Now().UnixMilli(), }, Order: ModelToProtoMintOrder(mintOrder), - Asset: ModelToProtoAssetDetail(asset, ownerNickname, false, 0, 0, 0, 0, getInt32Value(asset.Grade)), // 新创建的资产,is_liked 为 false,display_status 默认为 0,earnings、hourlyEarnings 和 exhibitionExpireAt 为 0,grade 从 asset.Grade 获取 + Asset: ModelToProtoAssetDetail(asset, ownerNickname, ownerAvatar, false, 0, 0, 0, 0, getInt32Value(asset.Grade)), // 新创建的资产,is_liked 为 false,display_status 默认为 0,earnings、hourlyEarnings 和 exhibitionExpireAt 为 0,grade 从 asset.Grade 获取 CostCrystal: capturedCostCrystal, BalanceAfter: newBalance, } @@ -570,18 +572,20 @@ func (s *mintService) GetMintOrder(orderID string, userID, starID int64) (*pb.Ge if order.AssetID != nil { asset, err := s.assetRepo.GetByID(*order.AssetID) if err == nil && asset != nil { - // 获取所有者昵称 + // 获取所有者昵称和头像 var ownerNickname string + var ownerAvatar string profile, err := s.userClient.GetFanProfile(context.Background(), asset.OwnerUID, asset.StarID) if err == nil && profile != nil { ownerNickname = profile.Nickname + ownerAvatar = profile.AvatarUrl } // 转换为 Proto(这里需要调用 ModelToProtoAssetDetail,但需要 is_liked 参数) // 由于是查询自己的订单,is_liked 设为 false(简化处理) // 获取 display_status displayStatus, _ := s.assetRepo.GetDisplayStatusByAssetID(asset.ID) - assetProto = ModelToProtoAssetDetail(asset, ownerNickname, false, displayStatus, 0, 0, 0, getInt32Value(asset.Grade)) // 新创建的资产,earnings、hourlyEarnings 和 exhibitionExpireAt 为 0 + assetProto = ModelToProtoAssetDetail(asset, ownerNickname, ownerAvatar, false, displayStatus, 0, 0, 0, getInt32Value(asset.Grade)) // 新创建的资产,earnings、hourlyEarnings 和 exhibitionExpireAt 为 0 // 如果 cover_url 存在,生成预签名 URL if assetProto.CoverUrl != "" { diff --git a/frontend/pages/asset-detail/asset-detail.vue b/frontend/pages/asset-detail/asset-detail.vue index 0020ba1..6fd7116 100644 --- a/frontend/pages/asset-detail/asset-detail.vue +++ b/frontend/pages/asset-detail/asset-detail.vue @@ -97,7 +97,7 @@ 创作者 - + {{ assetData.owner_nickname || '未知' }} diff --git a/frontend/pages/components/CastloveContent.vue b/frontend/pages/components/CastloveContent.vue index 9e4cb37..66cc5f3 100644 --- a/frontend/pages/components/CastloveContent.vue +++ b/frontend/pages/components/CastloveContent.vue @@ -56,7 +56,7 @@ - #{{ item.certificate_id }} + 链上编号: #{{ item.certificate_id }} diff --git a/frontend/pages/components/JianmianContent.vue b/frontend/pages/components/JianmianContent.vue index 66626d0..a738b6c 100644 --- a/frontend/pages/components/JianmianContent.vue +++ b/frontend/pages/components/JianmianContent.vue @@ -178,8 +178,8 @@ const newsItems = [ position: absolute; top: -40rpx; right: -24rpx; - width: 29%; - height: 29%; + width: 16%; + height: 16%; z-index: 20; } diff --git a/frontend/pages/components/StarCityContent.vue b/frontend/pages/components/StarCityContent.vue index 7296c50..6d3e9f5 100644 --- a/frontend/pages/components/StarCityContent.vue +++ b/frontend/pages/components/StarCityContent.vue @@ -31,19 +31,19 @@ - - - - + - + - + + + + @@ -58,10 +58,10 @@ import DressupContent from './DressupContent.vue'; const activeSubTab = ref(0); const subTabs = [ - { name: '周边' }, { name: '同款' }, { name: '见面' }, - { name: '装扮' } + { name: '道具' }, + { name: '福利' }, ]; const switchSubTab = (index) => { diff --git a/frontend/pages/components/TongkuanContent.vue b/frontend/pages/components/TongkuanContent.vue index a49db68..701c2f1 100644 --- a/frontend/pages/components/TongkuanContent.vue +++ b/frontend/pages/components/TongkuanContent.vue @@ -56,6 +56,7 @@ import { ref } from 'vue'; const drawerOpen = ref(false); const toggleDrawer = () => { + return; drawerOpen.value = !drawerOpen.value; }; @@ -159,8 +160,8 @@ const drawerProducts = [ position: absolute; top: -10rpx; right: -10rpx; - width: 29%; - height: 29%; + width: 16%; + height: 16%; z-index: 20; } diff --git a/frontend/pages/components/ZhoubianContent.vue b/frontend/pages/components/ZhoubianContent.vue index 6b8cfc2..c685dd5 100644 --- a/frontend/pages/components/ZhoubianContent.vue +++ b/frontend/pages/components/ZhoubianContent.vue @@ -77,24 +77,24 @@ const bigCards = [ { label: '限时上架', - name: '肖战TOPFANS\n2026联名款', - image: '/static/starcity/zhoubian/xiaoka-card.png', + name: 'TOPFANS\n2026联名款', + image: '/static/starcity/zhoubian/xiaoka-card-2.jpg', backImage: '/static/starcity/zhoubian/xiaoka-card-back2.png', price: 288 }, { label: '限时上架', - name: '肖战2025巡回\n演唱会现场版', - image: '/static/starcity/zhoubian/xiaoka-card2.png', + name: '2025巡回\n演唱会现场版', + image: '/static/starcity/zhoubian/xiaoka-card-1.jpg', backImage: '/static/starcity/zhoubian/xiaoka-card-back.png', price: 188 } ]; const smallCards = [ - { name: '肖战jun', image: '/static/starcity/zhoubian/merchandise1.png', price: 299, isNew: true }, + { name: '卡通手办', image: '/static/starcity/zhoubian/merchandise1.png', price: 299, isNew: true }, { name: '签名项链', image: '/static/starcity/zhoubian/merchandise2.png', price: 179, isNew: false }, - { name: '肖战漂流2022黑胶', image: '/static/starcity/zhoubian/merchandise3.png', price: 399, isNew: false }, + { name: '漂流2022黑胶', image: '/static/starcity/zhoubian/merchandise3.png', price: 399, isNew: false }, { name: '手串', image: '/static/starcity/zhoubian/merchandise4.png', price: 259, isNew: false } ]; @@ -144,8 +144,8 @@ const smallCards = [ position: absolute; top: -40rpx; right: -24rpx; - width: 29%; - height: 29%; + width: 16%; + height: 16%; z-index: 20; } diff --git a/frontend/pages/square/components/CreationGrid.vue b/frontend/pages/square/components/CreationGrid.vue index 0a15a7a..df234f8 100644 --- a/frontend/pages/square/components/CreationGrid.vue +++ b/frontend/pages/square/components/CreationGrid.vue @@ -20,7 +20,7 @@ - #{{ item.certificate_id }} + 链上编号: #{{ item.certificate_id }} diff --git a/frontend/static/starcity/zhoubian/xiaoka-card-1.jpg b/frontend/static/starcity/zhoubian/xiaoka-card-1.jpg new file mode 100644 index 0000000..ab5e493 Binary files /dev/null and b/frontend/static/starcity/zhoubian/xiaoka-card-1.jpg differ diff --git a/frontend/static/starcity/zhoubian/xiaoka-card-2.jpg b/frontend/static/starcity/zhoubian/xiaoka-card-2.jpg new file mode 100644 index 0000000..3164117 Binary files /dev/null and b/frontend/static/starcity/zhoubian/xiaoka-card-2.jpg differ diff --git a/frontend/static/starcity/zhoubian/xiaoka-card.png b/frontend/static/starcity/zhoubian/xiaoka-card.png deleted file mode 100644 index 2619bba..0000000 Binary files a/frontend/static/starcity/zhoubian/xiaoka-card.png and /dev/null differ diff --git a/frontend/static/starcity/zhoubian/xiaoka-card2.png b/frontend/static/starcity/zhoubian/xiaoka-card2.png deleted file mode 100644 index 0043c61..0000000 Binary files a/frontend/static/starcity/zhoubian/xiaoka-card2.png and /dev/null differ