fix: 样式修复、创作者头像修复
This commit is contained in:
parent
feb98dd865
commit
4aa11903f4
59
CLAUDE.md
59
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` 导致用户铸造/创建失败,需紧急修复序列。
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 != "" {
|
||||
|
||||
@ -97,7 +97,7 @@
|
||||
<view class="info-item">
|
||||
<text class="info-label">创作者</text>
|
||||
<view class="info-nickname">
|
||||
<image v-if="userAvatarUrl" class="info-avatar" :src="userAvatarUrl" mode="aspectFill">
|
||||
<image v-if="assetData.owner?.avatar" class="info-avatar" :src="assetData.owner.avatar" mode="aspectFill">
|
||||
</image>
|
||||
<text class="info-value">{{ assetData.owner_nickname || '未知' }}</text>
|
||||
</view>
|
||||
|
||||
@ -56,7 +56,7 @@
|
||||
<image class="creation-image" :src="item.cover_image" mode="aspectFill"></image>
|
||||
<view class="creation-info">
|
||||
<view class="creation-id">
|
||||
<text class="id-text">#{{ item.certificate_id }}</text>
|
||||
<text class="id-text">链上编号: #{{ item.certificate_id }}</text>
|
||||
</view>
|
||||
<view class="creation-meta">
|
||||
<view class="creator-info">
|
||||
|
||||
@ -178,8 +178,8 @@ const newsItems = [
|
||||
position: absolute;
|
||||
top: -40rpx;
|
||||
right: -24rpx;
|
||||
width: 29%;
|
||||
height: 29%;
|
||||
width: 16%;
|
||||
height: 16%;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
|
||||
@ -31,19 +31,19 @@
|
||||
|
||||
<!-- 子标签内容区 -->
|
||||
<view class="sub-content-area">
|
||||
<!-- 周边 -->
|
||||
<ZhoubianContent v-show="activeSubTab === 0" class="sub-content-fill" />
|
||||
|
||||
<!-- 同款 -->
|
||||
<TongkuanContent v-show="activeSubTab === 1" class="sub-content-fill" />
|
||||
<TongkuanContent v-show="activeSubTab === 0" class="sub-content-fill" />
|
||||
|
||||
<!-- 见面 -->
|
||||
<JianmianContent v-show="activeSubTab === 2" class="sub-content-fill" />
|
||||
<JianmianContent v-show="activeSubTab === 1" class="sub-content-fill" />
|
||||
|
||||
<!-- 装扮 -->
|
||||
<view v-show="activeSubTab === 3" class="sub-content-fill dressup-wrap">
|
||||
<view v-show="activeSubTab === 2" class="sub-content-fill dressup-wrap">
|
||||
<DressupContent />
|
||||
</view>
|
||||
|
||||
<!-- 周边 -->
|
||||
<ZhoubianContent v-show="activeSubTab === 3" class="sub-content-fill" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@ -58,10 +58,10 @@ import DressupContent from './DressupContent.vue';
|
||||
const activeSubTab = ref(0);
|
||||
|
||||
const subTabs = [
|
||||
{ name: '周边' },
|
||||
{ name: '同款' },
|
||||
{ name: '见面' },
|
||||
{ name: '装扮' }
|
||||
{ name: '道具' },
|
||||
{ name: '福利' },
|
||||
];
|
||||
|
||||
const switchSubTab = (index) => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 }
|
||||
];
|
||||
</script>
|
||||
@ -144,8 +144,8 @@ const smallCards = [
|
||||
position: absolute;
|
||||
top: -40rpx;
|
||||
right: -24rpx;
|
||||
width: 29%;
|
||||
height: 29%;
|
||||
width: 16%;
|
||||
height: 16%;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<view class="wf-like-wave wf-like-wave-inner" :class="{ 'wf-like-wave-active': likingMap[item.id] }" />
|
||||
<view class="creation-info">
|
||||
<view class="creation-id">
|
||||
<text class="id-text">#{{ item.certificate_id }}</text>
|
||||
<text class="id-text">链上编号: #{{ item.certificate_id }}</text>
|
||||
</view>
|
||||
<view class="creation-meta">
|
||||
<view class="creator-info">
|
||||
|
||||
BIN
frontend/static/starcity/zhoubian/xiaoka-card-1.jpg
Normal file
BIN
frontend/static/starcity/zhoubian/xiaoka-card-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
BIN
frontend/static/starcity/zhoubian/xiaoka-card-2.jpg
Normal file
BIN
frontend/static/starcity/zhoubian/xiaoka-card-2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 251 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 501 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 481 KiB |
Loading…
Reference in New Issue
Block a user