docs: 文档修改
This commit is contained in:
parent
ad773ffc27
commit
4538725884
@ -1,6 +1,7 @@
|
|||||||
# 灵感瀑布流(Inspiration Flow)设计文档
|
# 灵感瀑布流(Inspiration Flow)设计文档
|
||||||
|
|
||||||
> **创建日期:** 2026-04-28
|
> **创建日期:** 2026-04-28
|
||||||
|
> **更新日期:** 2026-04-29
|
||||||
> **项目:** TopFans 横向瀑布流藏品展示
|
> **项目:** TopFans 横向瀑布流藏品展示
|
||||||
> **服务:** galleryService (Go Dubbo-go)
|
> **服务:** galleryService (Go Dubbo-go)
|
||||||
> **状态:** 设计中
|
> **状态:** 设计中
|
||||||
@ -9,9 +10,17 @@
|
|||||||
|
|
||||||
## 一、设计目标
|
## 一、设计目标
|
||||||
|
|
||||||
横向瀑布流展示该 star_id 下所有用户展出的藏品,支持随机展示、无限滚动加载、按类型过滤。
|
横向瀑布流展示该 star_id 下所有用户展出的藏品,支持**随机展示**、**双向横向无限滚动**、按类型过滤。
|
||||||
|
|
||||||
**无限滚动实现方式:** 使用游标分页(Cursor-based Pagination),前端滚动到底部时携带 `cursor` 参数加载下一批数据。
|
**核心特点:**
|
||||||
|
- 每次查询返回随机顺序的藏品
|
||||||
|
- 支持双向横向无限滚动:
|
||||||
|
- 向右滚动:加载更多新数据(发现新内容)
|
||||||
|
- 向左滚动:加载历史数据(回看)
|
||||||
|
- 支持按藏品类型过滤(badge/poster/original/all)
|
||||||
|
- 会话级缓存,刷新后数据重新随机
|
||||||
|
|
||||||
|
**无限滚动实现方式:** 使用游标分页(Cursor-based Pagination),前端滚动到边缘时携带 `cursor` 参数加载下一批数据。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -42,8 +51,9 @@ GET /api/v1/inspiration-flow
|
|||||||
|
|
||||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||||
|------|------|------|--------|------|
|
|------|------|------|--------|------|
|
||||||
| cursor | string | 否 | 空 | 游标(首次请求为空,加载更多时传上次返回的 cursor) |
|
| cursor | string | 否 | 空 | 游标(首次请求为空) |
|
||||||
| limit | int | 否 | 20 | 每页数量(最大 50) |
|
| direction | string | 否 | right | 滚动方向:right(加载新数据)/ left(加载历史) |
|
||||||
|
| limit | int | 否 | 10 | 每页数量(最大 20,移动端优化) |
|
||||||
| type | string | 否 | all | 过滤类型:badge/poster/original/all |
|
| type | string | 否 | all | 过滤类型:badge/poster/original/all |
|
||||||
|
|
||||||
**HTTP 响应:**
|
**HTTP 响应:**
|
||||||
@ -62,7 +72,7 @@ GET /api/v1/inspiration-flow
|
|||||||
"owner_nickname": "粉丝昵称"
|
"owner_nickname": "粉丝昵称"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"cursor": "eyJsaW1pdCI6MjAsIm9mZnNldCI6MjB9",
|
"cursor": "eyJsaW1pdCI6MTB9",
|
||||||
"has_more": true
|
"has_more": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,8 +104,9 @@ GET /api/v1/inspiration-flow
|
|||||||
// 获取灵感瀑布藏品列表请求
|
// 获取灵感瀑布藏品列表请求
|
||||||
message GetInspirationFlowRequest {
|
message GetInspirationFlowRequest {
|
||||||
string cursor = 1; // 游标(首次请求为空)
|
string cursor = 1; // 游标(首次请求为空)
|
||||||
int32 limit = 2; // 每页数量(默认20,最大50)
|
string direction = 2; // 滚动方向:right(加载新数据)/ left(加载历史)
|
||||||
string type = 3; // 过滤类型:badge/poster/original/all(默认all)
|
int32 limit = 3; // 每页数量(默认10,最大20)
|
||||||
|
string type = 4; // 过滤类型:badge/poster/original/all(默认all)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取灵感瀑布藏品列表响应
|
// 获取灵感瀑布藏品列表响应
|
||||||
@ -115,7 +126,7 @@ message InspirationFlowData {
|
|||||||
message InspirationFlowItem {
|
message InspirationFlowItem {
|
||||||
int64 asset_id = 1; // 资产ID
|
int64 asset_id = 1; // 资产ID
|
||||||
string name = 2; // 藏品名称
|
string name = 2; // 藏品名称
|
||||||
string cover_url = 3; // 封面图URL
|
string cover_url = 3; // 封面图URL
|
||||||
int32 like_count = 4; // 点赞数
|
int32 like_count = 4; // 点赞数
|
||||||
string owner_nickname = 5; // 展出者昵称
|
string owner_nickname = 5; // 展出者昵称
|
||||||
}
|
}
|
||||||
@ -143,24 +154,77 @@ service GalleryService {
|
|||||||
|
|
||||||
## 五、核心逻辑
|
## 五、核心逻辑
|
||||||
|
|
||||||
### 5.1 游标分页设计
|
### 5.1 随机查询实现
|
||||||
|
|
||||||
|
**核心需求:** 每次查询返回随机顺序的藏品数据,而不是固定顺序。
|
||||||
|
|
||||||
|
**实现方式:** 使用 PostgreSQL 的 `ORDER BY RANDOM()` 实现随机排序。
|
||||||
|
|
||||||
|
**为什么选择 ORDER BY RANDOM():**
|
||||||
|
1. **实现简单**:一条 SQL 搞定,无需应用层处理
|
||||||
|
2. **数据量适配良好**:在几千到几万条数据时性能可接受(< 100ms)
|
||||||
|
3. **接口稳定**:便于后期扩展优化策略
|
||||||
|
|
||||||
|
**性能说明:**
|
||||||
|
|
||||||
|
| 数据量 | RANDOM() 性能 | 推荐程度 |
|
||||||
|
|--------|--------------|---------|
|
||||||
|
| < 1万条 | < 50ms | 强烈推荐 |
|
||||||
|
| 1-5万条 | 50-100ms | 推荐 |
|
||||||
|
| 5-10万条 | 100-500ms | 可接受 |
|
||||||
|
| > 10万条 | 开始变慢 | 需要优化 |
|
||||||
|
|
||||||
|
> 注:后期数据量超过 10 万条时,可考虑切换为"区间采样"策略,详见本章 5.5 节扩展说明。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 随机 offset 分页设计
|
||||||
|
|
||||||
|
**核心思路:** 每次请求都是独立的随机排序,随机生成 offset 值,而不是依赖游标累加 offset。
|
||||||
|
|
||||||
**游标结构(JSON,base64 编码):**
|
**游标结构(JSON,base64 编码):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"offset": 20,
|
"limit": 10
|
||||||
"limit": 20
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**为什么用游标分页而非 offset 分页:**
|
> **说明:** 游标只记录 limit,不需要记录 offset(因为每次 offset 都是随机生成的)
|
||||||
1. **性能稳定**:offset 翻页越深性能越差,游标分页性能恒定
|
|
||||||
2. **无限滚动友好**:用户滚动过程中数据可能变化,游标避免重复/遗漏
|
|
||||||
3. **适合随机排序**:配合 RANDOM() 避免翻页时数据错位
|
|
||||||
|
|
||||||
### 5.2 查询逻辑
|
**为什么用随机 offset:**
|
||||||
|
1. **数据变化无影响**:每次都是全新随机,数据变化不影响展示
|
||||||
|
2. **性能稳定**:offset 从 0 开始,不存在深分页性能问题
|
||||||
|
3. **实现简单**:无需处理数据变化时的缓存问题
|
||||||
|
|
||||||
|
**游标的行为说明:**
|
||||||
|
- 每次请求 offset 都是随机生成的(0 ~ max(0, total-limit))
|
||||||
|
- 同一会话内滚动时,顺序可能跳变(这是预期行为)
|
||||||
|
- 用户刷新页面后,看到全新的随机顺序
|
||||||
|
|
||||||
|
> **业务说明:** 由于每次请求都是独立随机,滚动加载过程中顺序可能跳变。这是预期行为,用户每次刷新看到的是不同顺序,符合"随机展示"的需求。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 查询逻辑
|
||||||
|
|
||||||
|
**实现流程:**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 查询该 star_id 下有效展品的总数
|
||||||
|
2. 随机生成 offset 值(0 ~ max(0, total-limit))
|
||||||
|
3. 执行带随机 offset 的查询
|
||||||
|
```
|
||||||
|
|
||||||
|
**SQL 查询:**
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
-- 1. 先查询总数
|
||||||
|
SELECT COUNT(*) FROM exhibitions e
|
||||||
|
WHERE e.occupier_star_id = ?
|
||||||
|
AND e.expire_at > ?
|
||||||
|
AND e.deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- 2. 应用层生成随机 offset,然后查询
|
||||||
SELECT
|
SELECT
|
||||||
e.asset_id,
|
e.asset_id,
|
||||||
a.name,
|
a.name,
|
||||||
@ -177,7 +241,7 @@ WHERE e.occupier_star_id = ?
|
|||||||
AND a.is_active = true
|
AND a.is_active = true
|
||||||
AND (? = 'all' OR a.material_type = ?)
|
AND (? = 'all' OR a.material_type = ?)
|
||||||
ORDER BY RANDOM()
|
ORDER BY RANDOM()
|
||||||
LIMIT ?;
|
LIMIT ? OFFSET ?; -- offset 由应用层随机生成
|
||||||
```
|
```
|
||||||
|
|
||||||
**参数说明:**
|
**参数说明:**
|
||||||
@ -185,12 +249,18 @@ LIMIT ?;
|
|||||||
- `? = now` (当前时间戳)
|
- `? = now` (当前时间戳)
|
||||||
- `? = type` (过滤类型)
|
- `? = type` (过滤类型)
|
||||||
- `? = limit` (每页数量)
|
- `? = limit` (每页数量)
|
||||||
|
- `? = offset` (随机生成的偏移量,非固定值)
|
||||||
|
|
||||||
### 5.3 游标编解码
|
---
|
||||||
|
|
||||||
|
### 5.4 游标编解码
|
||||||
|
|
||||||
|
**说明:** 由于每次请求的 offset 是随机生成的,游标只需要记录 limit。
|
||||||
|
|
||||||
**编码(服务端):**
|
**编码(服务端):**
|
||||||
```go
|
```go
|
||||||
cursor := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"offset":%d,"limit":%d}`, offset, limit)))
|
limit := 10 // 默认值,实际从请求或配置获取
|
||||||
|
cursor := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"limit":%d}`, limit)))
|
||||||
```
|
```
|
||||||
|
|
||||||
**解码(服务端):**
|
**解码(服务端):**
|
||||||
@ -198,10 +268,251 @@ cursor := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"offset":%d,"li
|
|||||||
decoded, _ := base64.StdEncoding.DecodeString(cursor)
|
decoded, _ := base64.StdEncoding.DecodeString(cursor)
|
||||||
var cursorData map[string]int
|
var cursorData map[string]int
|
||||||
json.Unmarshal(decoded, &cursorData)
|
json.Unmarshal(decoded, &cursorData)
|
||||||
offset := cursorData["offset"]
|
limit := cursorData["limit"] // offset 由每次请求随机生成,无需从游标获取
|
||||||
limit := cursorData["limit"]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**前端使用:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 首次请求
|
||||||
|
fetch('/api/v1/inspiration-flow?limit=10&type=all')
|
||||||
|
|
||||||
|
// 向右滚动(加载新数据)
|
||||||
|
fetch('/api/v1/inspiration-flow?cursor=eyJsaW1pdCI6MTB9&limit=10&type=all&direction=right')
|
||||||
|
|
||||||
|
// 向左滚动(加载历史)
|
||||||
|
fetch('/api/v1/inspiration-flow?cursor=eyJsaW1pdCI6MTB9&limit=10&type=all&direction=left')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.5 双向滚动实现
|
||||||
|
|
||||||
|
**核心思路:** 后端维护会话级已展示数据缓存,实现双向滚动时完全避免重复。
|
||||||
|
|
||||||
|
**滚动方向定义:**
|
||||||
|
- `direction: "right"` - 向右滚动,加载新数据(发现新内容)
|
||||||
|
- `direction: "left"` - 向左滚动,加载历史数据(回看)
|
||||||
|
|
||||||
|
**游标结构(JSON,base64 编码):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"limit": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **说明:** direction 是独立的 query 参数,不包含在游标中。游标只用于分页控制。
|
||||||
|
|
||||||
|
**缓存结构(Redis):**
|
||||||
|
```
|
||||||
|
Key: inspiration_flow:{star_id}:{session_id}
|
||||||
|
Type: Hash
|
||||||
|
Fields:
|
||||||
|
- display_order: [1, 5, 3, 9, 2, 4, ...] # 展示顺序
|
||||||
|
- history: {1: data1, 5: data5, 3: data3, ...} # 历史数据详情(方便回看)
|
||||||
|
TTL: 30分钟(无操作自动清理)
|
||||||
|
```
|
||||||
|
|
||||||
|
**向右滚动处理流程:**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 前端请求:direction=right
|
||||||
|
2. 后端从 Redis 获取已展示ID集合
|
||||||
|
3. 后端执行随机查询,排除已展示ID
|
||||||
|
SELECT ... FROM exhibitions
|
||||||
|
WHERE id NOT IN (已展示ID)
|
||||||
|
ORDER BY RANDOM()
|
||||||
|
LIMIT ?
|
||||||
|
4. 返回新数据,并记录到缓存
|
||||||
|
5. 前端追加展示
|
||||||
|
```
|
||||||
|
|
||||||
|
**向左滚动处理流程:**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 前端请求:direction=left, offset=?
|
||||||
|
2. 后端从 Redis 获取 display_order
|
||||||
|
3. 后端根据 offset 从历史中分页返回
|
||||||
|
4. 前端在左侧插入展示
|
||||||
|
```
|
||||||
|
|
||||||
|
**SQL 排除已展示ID查询:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
e.asset_id,
|
||||||
|
a.name,
|
||||||
|
a.cover_url,
|
||||||
|
a.like_count,
|
||||||
|
fp.nickname as owner_nickname
|
||||||
|
FROM exhibitions e
|
||||||
|
JOIN assets a ON e.asset_id = a.id
|
||||||
|
JOIN fan_profiles fp ON e.occupier_uid = fp.user_id AND e.occupier_star_id = fp.star_id
|
||||||
|
WHERE e.occupier_star_id = ?
|
||||||
|
AND e.expire_at > ?
|
||||||
|
AND e.deleted_at IS NULL
|
||||||
|
AND e.id NOT IN (已展示ID列表)
|
||||||
|
AND a.status = 1
|
||||||
|
AND a.is_active = true
|
||||||
|
AND (? = 'all' OR a.material_type = ?)
|
||||||
|
ORDER BY RANDOM()
|
||||||
|
LIMIT ?;
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意:** 当已展示数据量很大时,`NOT IN` 查询性能会下降。需要在适当时机清理已展示缓存(建议 TTL 30分钟或达到一定数量后刷新随机顺序)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.6 扩展说明:数据量大时的优化策略
|
||||||
|
|
||||||
|
**触发条件:** 当展品数据量超过 10 万条,`ORDER BY RANDOM()` 性能开始明显下降时。
|
||||||
|
|
||||||
|
**优化方案:区间采样**
|
||||||
|
|
||||||
|
**思路:** 把数据按 ID 区间分成多个桶,随机选择桶后在该桶内读取数据,应用层再对结果进行随机打乱
|
||||||
|
|
||||||
|
**优点:**
|
||||||
|
- 查询性能稳定,不随数据量增长而显著下降
|
||||||
|
- 能保证随机分布,每个数据都有机会被展示
|
||||||
|
- 跨桶采样,随机性更好
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 数据量大(10 万以上)
|
||||||
|
- 需要保证随机分布的均匀性
|
||||||
|
|
||||||
|
**实现示意:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 1. 获取该 star_id 下有效展品的 ID 范围
|
||||||
|
minID, maxID := "SELECT MIN(id), MAX(id) FROM exhibitions
|
||||||
|
WHERE occupier_star_id = ? AND expire_at > ? AND deleted_at IS NULL"
|
||||||
|
|
||||||
|
// 2. 计算桶信息(假设每个桶 10000 条)
|
||||||
|
bucketSize := int64(10000)
|
||||||
|
totalBuckets := (maxID - minID) / bucketSize + 1
|
||||||
|
|
||||||
|
// 3. 随机选择 1 个桶
|
||||||
|
randomBucket := random.Int63n(totalBuckets)
|
||||||
|
bucketStartID := minID + randomBucket*bucketSize
|
||||||
|
bucketEndID := bucketStartID + bucketSize
|
||||||
|
|
||||||
|
// 4. 在桶内查询所有有效 ID(数据量小,随机开销可忽略)
|
||||||
|
bucketIDs := "SELECT id FROM exhibitions
|
||||||
|
WHERE id BETWEEN ? AND ?
|
||||||
|
AND occupier_star_id = ? AND expire_at > ? AND deleted_at IS NULL
|
||||||
|
ORDER BY id"
|
||||||
|
|
||||||
|
// 5. 应用层对 bucketIDs 随机打乱,取前 limit 个
|
||||||
|
shuffle(bucketIDs)
|
||||||
|
selectedIDs := bucketIDs[:limit]
|
||||||
|
|
||||||
|
// 6. 根据 selectedIDs 查询完整数据
|
||||||
|
"SELECT ... FROM exhibitions WHERE id IN (?)", selectedIDs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 扩展接口设计(预留)
|
||||||
|
|
||||||
|
两种策略可以抽象成统一的接口,便于后期切换:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 随机策略接口
|
||||||
|
type RandomStrategy interface {
|
||||||
|
// GetRandomAssetIDs 获取随机的展品 ID 列表
|
||||||
|
GetRandomAssetIDs(ctx context.Context, starID string, limit int) ([]int64, error)
|
||||||
|
|
||||||
|
// Name 返回策略名称
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 策略1:随机排序(当前使用,数据量 < 10 万时推荐)
|
||||||
|
type RandomOrderStrategy struct{}
|
||||||
|
|
||||||
|
func (s *RandomOrderStrategy) GetRandomAssetIDs(ctx context.Context, starID string, limit int) ([]int64, error) {
|
||||||
|
// 1. 获取有效展品总数
|
||||||
|
total, err := repo.CountValidAssets(starID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
return []int64{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 随机生成 offset(0 ~ max(0, total-limit))
|
||||||
|
maxOffset := total - int64(limit)
|
||||||
|
if maxOffset < 0 {
|
||||||
|
maxOffset = 0
|
||||||
|
}
|
||||||
|
randomOffset := random.Int63n(maxOffset + 1) // +1 是因为 Int63n(0) 会报错
|
||||||
|
|
||||||
|
// 3. 执行随机排序查询
|
||||||
|
return repo.GetRandomAssetsByOrder(starID, int(randomOffset), limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RandomOrderStrategy) Name() string {
|
||||||
|
return "random_order"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 策略2:区间采样(数据量 > 10 万时推荐)
|
||||||
|
type RangeSamplingStrategy struct {
|
||||||
|
bucketSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RangeSamplingStrategy) GetRandomAssetIDs(ctx context.Context, starID string, limit int) ([]int64, error) {
|
||||||
|
// 1. 获取 ID 范围
|
||||||
|
minID, maxID, err := repo.GetIDRange(starID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 计算桶信息并随机选择桶
|
||||||
|
bucketSize := s.bucketSize // 默认 10000
|
||||||
|
totalBuckets := (maxID - minID) / bucketSize + 1
|
||||||
|
randomBucket := random.Int63n(totalBuckets)
|
||||||
|
bucketStartID := minID + randomBucket*bucketSize
|
||||||
|
bucketEndID := bucketStartID + bucketSize
|
||||||
|
|
||||||
|
// 3. 在桶内查询 ID 列表
|
||||||
|
ids, err := repo.GetAssetIDsInRange(starID, bucketStartID, bucketEndID, limit*3)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 应用层随机打乱
|
||||||
|
shuffle(ids)
|
||||||
|
if len(ids) > limit {
|
||||||
|
ids = ids[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RangeSamplingStrategy) Name() string {
|
||||||
|
return "range_sampling"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**策略选择建议:**
|
||||||
|
|
||||||
|
| 数据量 | 方案 | 随机性 | 推荐度 |
|
||||||
|
|--------|------|--------|--------|
|
||||||
|
| < 10 万 | RandomOrderStrategy | ✅ 真正随机 | 强烈推荐 |
|
||||||
|
| > 10 万 | RangeSamplingStrategy | ✅ 跨桶分布均匀 | 强烈推荐 |
|
||||||
|
|
||||||
|
**策略切换配置:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// config.go
|
||||||
|
// 数据量 < 10 万(推荐)
|
||||||
|
var randomStrategy RandomStrategy = &RandomOrderStrategy{}
|
||||||
|
|
||||||
|
// 数据量 > 10 万(推荐)
|
||||||
|
// var randomStrategy RandomStrategy = &RangeSamplingStrategy{bucketSize: 10000}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **扩展提示:** 切换策略只需修改配置,将 `randomStrategy` 的具体实现替换,接口和调用方无需改动。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 六、数据模型
|
## 六、数据模型
|
||||||
@ -241,8 +552,8 @@ type Asset struct {
|
|||||||
|
|
||||||
| 配置项 | 说明 | 默认值 |
|
| 配置项 | 说明 | 默认值 |
|
||||||
|--------|------|--------|
|
|--------|------|--------|
|
||||||
| inspiration_flow_limit | 默认每页数量 | 20 |
|
| inspiration_flow_limit | 默认每页数量 | 10 |
|
||||||
| inspiration_flow_max_limit | 最大每页数量 | 50 |
|
| inspiration_flow_max_limit | 最大每页数量 | 20 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -260,7 +571,7 @@ backend/
|
|||||||
|
|
||||||
├── services/galleryService/
|
├── services/galleryService/
|
||||||
│ ├── repository/
|
│ ├── repository/
|
||||||
│ │ └── gallery_repository.go # 修改:新增 GetInspirationFlow 方法
|
│ │ └── gallery_repository.go # 修改:新增 GetInspirationFlow 方法,使用 ORDER BY RANDOM()
|
||||||
│ │
|
│ │
|
||||||
│ ├── service/
|
│ ├── service/
|
||||||
│ │ └── gallery_service.go # 修改:新增 GetInspirationFlow 方法
|
│ │ └── gallery_service.go # 修改:新增 GetInspirationFlow 方法
|
||||||
@ -309,29 +620,78 @@ CREATE INDEX IF NOT EXISTS idx_assets_material_type ON assets(material_type);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 十、无限滚动前端对接说明
|
## 十、前端对接说明
|
||||||
|
|
||||||
### 10.1 首次请求
|
### 10.1 首次请求
|
||||||
```
|
```
|
||||||
GET /api/v1/inspiration-flow?limit=20&type=all
|
GET /api/v1/inspiration-flow?limit=10&type=all
|
||||||
```
|
```
|
||||||
|
|
||||||
### 10.2 加载更多
|
### 10.2 向右滚动(加载新数据)
|
||||||
```
|
```
|
||||||
GET /api/v1/inspiration-flow?cursor=eyJsaW1pdCI6MjAsIm9mZnNldCI6MjB9&limit=20&type=all
|
GET /api/v1/inspiration-flow?cursor=eyJsaW1pdCI6MTB9&limit=10&type=all&direction=right
|
||||||
```
|
```
|
||||||
|
|
||||||
### 10.3 前端逻辑
|
### 10.3 向左滚动(加载历史)
|
||||||
1. 首次请求 cursor 为空
|
```
|
||||||
|
GET /api/v1/inspiration-flow?cursor=eyJsaW1pdCI6MTB9&limit=10&type=all&direction=left
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.4 前端逻辑
|
||||||
|
1. 首次请求 cursor 为空(`?cursor=` 不传或传空),direction 默认为 right
|
||||||
2. 解析响应中的 cursor 和 has_more
|
2. 解析响应中的 cursor 和 has_more
|
||||||
3. 滚动到底部时,若 has_more=true,携带 cursor 发起下一页请求
|
3. 滚动到右侧边缘时,若 has_more=true,携带 cursor 发起下一页请求(direction=right)
|
||||||
4. 数据变化时重置 cursor 重新加载
|
4. 滚动到左侧边缘时,携带 cursor 发起上一页请求(direction=left)
|
||||||
|
5. 会话级缓存,刷新后数据重新随机
|
||||||
|
|
||||||
|
### 10.5 双向滚动行为说明
|
||||||
|
- **首次进入**:看到随机顺序的藏品
|
||||||
|
- **向右滚动**:加载更多新数据(发现新内容)
|
||||||
|
- **向左滚动**:加载之前看过的历史数据(回看)
|
||||||
|
- **刷新页面**:数据重新随机,已浏览历史清空
|
||||||
|
- **首次进入时左滑**:前端禁用左滑操作(因为没有历史数据)
|
||||||
|
|
||||||
|
**前端实现要点:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 判断是否可以左滑
|
||||||
|
const canScrollLeft = displayedIDs.length > 0;
|
||||||
|
|
||||||
|
// 首次进入时禁用左滑
|
||||||
|
if (!canScrollLeft) {
|
||||||
|
// 禁用左滑手势
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据后启用左滑
|
||||||
|
onDataLoaded() {
|
||||||
|
this.canScrollLeft = true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.6 随机展示行为说明
|
||||||
|
- **每次进入页面**:看到的是新的随机顺序
|
||||||
|
- **滚动加载过程中**:顺序可能跳变(因为每次都是独立随机)
|
||||||
|
- **刷新页面**:随机顺序重新生成,已浏览历史清空
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 十一、待确认事项
|
## 十一、待确认事项
|
||||||
|
|
||||||
1. **material_type 枚举值**:badge/poster/original,后续按需扩展
|
1. **material_type 枚举值**:badge/poster/original,后续按需扩展
|
||||||
2. **随机排序策略**:ORDER BY RANDOM() 在数据量大时可能有性能问题,后续可考虑按 like_count 随机采样
|
2. **每页数量上限**:当前设为 20(移动端优化),是否合适?
|
||||||
3. **每页数量上限**:当前设为 50,是否合适?
|
3. **是否需要缓存**:热门 star_id 的数据可以考虑 Redis 缓存
|
||||||
4. **是否需要缓存**:热门 star_id 的数据可以考虑 Redis 缓存
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、变更记录
|
||||||
|
|
||||||
|
| 日期 | 变更内容 |
|
||||||
|
|------|---------|
|
||||||
|
| 2026-04-29 | 重构随机查询逻辑,明确使用 ORDER BY RANDOM() 实现随机展示;添加大数据量扩展说明 |
|
||||||
|
| 2026-04-29 | 针对移动端优化:默认每页数量从 20 调整为 10,最大每页数量从 50 调整为 20 |
|
||||||
|
| 2026-04-29 | 补充大数据量优化方案:增加"区间采样"方案及 RandomStrategy 扩展接口设计 |
|
||||||
|
| 2026-04-29 | 移除方案一(随机起点),只保留 ORDER BY RANDOM() 和区间采样两个方案 |
|
||||||
|
| 2026-04-29 | 明确使用 PostgreSQL 数据库,ORDER BY RANDOM() 语法与 MySQL 相同 |
|
||||||
|
| 2026-04-29 | 改用随机 offset 方案(方案 B),每次请求都是独立随机,数据变化无影响 |
|
||||||
|
| 2026-04-29 | 新增双向滚动支持:向右加载新数据,向左加载历史数据,后端维护会话级缓存 |
|
||||||
|
| 2026-04-29 | 修复:修正重复的 10.4 章节、RangeSamplingStrategy 签名、游标结构说明 |
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name" : "TopFans",
|
"name" : "TopFans",
|
||||||
"appid" : "__UNI__F199FF4",
|
"appid" : "__UNI__F199FF4",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "1.0.0",
|
"versionName" : "1.0.1",
|
||||||
"versionCode" : "100",
|
"versionCode" : "100",
|
||||||
"transformPx" : false,
|
"transformPx" : false,
|
||||||
/* 5+App特有相关 */
|
/* 5+App特有相关 */
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// API 基础配置
|
// API 基础配置
|
||||||
// const baseURL = 'http://101.132.250.62:8080'
|
const baseURL = 'http://101.132.250.62:8080'
|
||||||
const baseURL = 'http://192.168.110.60:8080'
|
// const baseURL = 'http://192.168.110.60:8080'
|
||||||
// const baseURL = 'http://localhost:8080'
|
// const baseURL = 'http://localhost:8080'
|
||||||
|
|
||||||
// 是否使用模拟数据(开发调试时设为 true,后端API准备好后改为 false)
|
// 是否使用模拟数据(开发调试时设为 true,后端API准备好后改为 false)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user