fix: 样式修复、创作者头像修复

This commit is contained in:
liulujian 2026-06-03 18:13:16 +08:00
parent feb98dd865
commit 4aa11903f4
14 changed files with 113 additions and 37 deletions

View File

@ -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` 导致用户铸造/创建失败,需紧急修复序列。

View File

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

View File

@ -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 为 falsedisplay_status 默认为 0earnings、hourlyEarnings 和 exhibitionExpireAt 为 0grade 从 asset.Grade 获取
Asset: ModelToProtoAssetDetail(asset, ownerNickname, ownerAvatar, false, 0, 0, 0, 0, getInt32Value(asset.Grade)), // 新创建的资产is_liked 为 falsedisplay_status 默认为 0earnings、hourlyEarnings 和 exhibitionExpireAt 为 0grade 从 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 != "" {

View File

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

View File

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

View File

@ -178,8 +178,8 @@ const newsItems = [
position: absolute;
top: -40rpx;
right: -24rpx;
width: 29%;
height: 29%;
width: 16%;
height: 16%;
z-index: 20;
}

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

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