16 KiB
16 KiB
材质收藏
**本文引用的文件** - [texture_service.go](file://internal/service/texture_service.go) - [texture_repository.go](file://internal/repository/texture_repository.go) - [texture_handler.go](file://internal/handler/texture_handler.go) - [routes.go](file://internal/handler/routes.go) - [texture.go](file://internal/model/texture.go) - [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql) - [texture_service_test.go](file://internal/service/texture_service_test.go)目录
简介
本文件围绕“材质收藏”能力进行系统化文档化,重点覆盖以下方面:
- 收藏/取消收藏的切换逻辑与幂等性设计
- 用户收藏列表的查询机制与分页实现
- API 的请求参数、响应格式与错误处理
- 基于测试用例的逻辑验证与正确性保障
- 数据模型与数据库结构支撑
项目结构
与“材质收藏”直接相关的代码分布在如下层次:
- 路由层:定义收藏相关接口路径
- 处理器层:解析请求、调用服务层、封装响应
- 服务层:业务逻辑(收藏切换、收藏列表查询)
- 仓储层:数据库访问(收藏状态判断、收藏增删、收藏计数增减、收藏列表查询)
- 模型层:数据结构(材质、收藏关系、下载日志)
- 数据库脚本:表结构与索引定义
graph TB
Routes["路由层<br/>routes.go"] --> Handler["处理器层<br/>texture_handler.go"]
Handler --> Service["服务层<br/>texture_service.go"]
Service --> Repo["仓储层<br/>texture_repository.go"]
Service --> Model["模型层<br/>texture.go"]
Repo --> DB["数据库脚本<br/>carrotskin_postgres.sql"]
图表来源
- routes.go
- texture_handler.go
- texture_service.go
- texture_repository.go
- texture.go
- carrotskin_postgres.sql
章节来源
- routes.go
- texture_handler.go
- texture_service.go
- texture_repository.go
- texture.go
- carrotskin_postgres.sql
核心组件
- 收藏切换接口:POST /api/v1/texture/{id}/favorite
- 收藏列表接口:GET /api/v1/texture/favorites
- 数据模型:材质、用户-材质收藏关系、下载日志
- 仓储方法:收藏状态判断、收藏增删、收藏计数增减、收藏列表查询
章节来源
架构总览
收藏能力的端到端流程如下:
- 客户端向收藏接口发起请求
- 路由层匹配到处理器
- 处理器解析参数、调用服务层
- 服务层根据收藏状态决定新增或删除收藏,并同步更新收藏计数
- 仓储层执行数据库操作
- 处理器封装响应返回客户端
sequenceDiagram
participant Client as "客户端"
participant Routes as "路由层"
participant Handler as "处理器"
participant Service as "服务层"
participant Repo as "仓储层"
participant DB as "数据库"
Client->>Routes : "POST /api/v1/texture/{id}/favorite"
Routes->>Handler : "分发到 ToggleFavorite"
Handler->>Service : "ToggleTextureFavorite(userID, textureID)"
Service->>Repo : "IsTextureFavorited(userID, textureID)"
Repo-->>Service : "布尔结果"
alt 已收藏
Service->>Repo : "RemoveTextureFavorite(userID, textureID)"
Service->>Repo : "DecrementTextureFavoriteCount(textureID)"
Repo-->>Service : "OK"
else 未收藏
Service->>Repo : "AddTextureFavorite(userID, textureID)"
Service->>Repo : "IncrementTextureFavoriteCount(textureID)"
Repo-->>Service : "OK"
end
Service-->>Handler : "返回新的收藏状态"
Handler-->>Client : "200 {is_favorited : bool}"
图表来源
详细组件分析
收藏切换 API(ToggleTextureFavorite)
- 接口路径:POST /api/v1/texture/{id}/favorite
- 请求参数
- 路径参数:id(材质ID)
- 认证:Bearer Token(JWT)
- 处理流程
- 校验材质存在性
- 查询当前用户是否已收藏
- 若已收藏:删除收藏记录并减少收藏计数
- 若未收藏:插入收藏记录并增加收藏计数
- 返回布尔值表示新的收藏状态
- 幂等性设计
- 同一用户对同一材质重复调用收藏/取消收藏,最终状态与最后一次操作一致
- 通过唯一约束避免重复收藏记录(见数据库脚本)
- 错误处理
- 材质不存在:返回错误
- 数据库异常:返回错误
- 未认证:返回 401
flowchart TD
Start(["进入 ToggleFavorite"]) --> Parse["解析路径参数 id"]
Parse --> CheckAuth["校验 JWT 有效性"]
CheckAuth --> CallSvc["调用服务层 ToggleTextureFavorite"]
CallSvc --> Exists{"材质存在?"}
Exists --> |否| Err["返回错误:材质不存在"]
Exists --> |是| Favorited{"是否已收藏?"}
Favorited --> |是| Unfav["删除收藏记录"]
Favorited --> |否| Fav["新增收藏记录"]
Unfav --> Dec["收藏计数 -1"]
Fav --> Inc["收藏计数 +1"]
Dec --> Ret["返回 false"]
Inc --> Ret2["返回 true"]
Err --> End(["结束"])
Ret --> End
Ret2 --> End
图表来源
章节来源
用户收藏列表 API(GetUserTextureFavorites)
- 接口路径:GET /api/v1/texture/favorites
- 请求参数
- page:页码,默认 1;最小 1
- page_size:每页数量,默认 20;最小 1,最大 100
- 认证:Bearer Token(JWT)
- 处理流程
- 校验分页参数边界
- 通过子查询获取当前用户收藏的材质ID集合
- 基于材质状态过滤(仅返回正常状态)
- 分页查询并返回总数
- 响应格式
- 包含分页信息与材质列表(每项包含基础元信息)
sequenceDiagram
participant Client as "客户端"
participant Routes as "路由层"
participant Handler as "处理器"
participant Service as "服务层"
participant Repo as "仓储层"
participant DB as "数据库"
Client->>Routes : "GET /api/v1/texture/favorites?page&page_size"
Routes->>Handler : "分发到 GetUserFavorites"
Handler->>Service : "GetUserTextureFavorites(userID, page, pageSize)"
Service->>Repo : "子查询获取收藏的 texture_id"
Repo-->>Service : "ID 列表"
Service->>Repo : "按状态过滤并分页查询材质"
Repo-->>Service : "材质列表 + 总数"
Service-->>Handler : "返回结果"
Handler-->>Client : "200 {data, total, page, page_size}"
图表来源
章节来源
数据模型与数据库结构
- 材质模型(textures)
- 字段:uploader_id、name、description、type、url、hash、size、is_public、download_count、favorite_count、is_slim、status、created_at、updated_at
- 索引:按 uploader_id、公开/类型/状态组合索引、收藏数降序索引
- 用户-材质收藏关系(user_texture_favorites)
- 字段:user_id、texture_id、created_at
- 唯一键:(user_id, texture_id),防止重复收藏
- 索引:user_id、texture_id、created_at
- 下载日志(texture_download_logs)
- 字段:texture_id、user_id、ip_address、user_agent、created_at
- 用于统计与风控
erDiagram
USER {
bigint id PK
varchar username UK
varchar email UK
}
TEXTURES {
bigint id PK
bigint uploader_id FK
varchar name
text description
enum type
varchar url
varchar hash UK
integer size
boolean is_public
integer download_count
integer favorite_count
boolean is_slim
smallint status
timestamp created_at
timestamp updated_at
}
USER_TEXTURE_FAVORITES {
bigint id PK
bigint user_id FK
bigint texture_id FK
timestamp created_at
}
TEXTURE_DOWNLOAD_LOGS {
bigint id PK
bigint texture_id FK
bigint user_id FK
inet ip_address
text user_agent
timestamp created_at
}
USER ||--o{ TEXTURES : "上传"
USER ||--o{ USER_TEXTURE_FAVORITES : "收藏"
TEXTURES ||--o{ USER_TEXTURE_FAVORITES : "被收藏"
USER ||--o{ TEXTURE_DOWNLOAD_LOGS : "下载"
TEXTURES ||--o{ TEXTURE_DOWNLOAD_LOGS : "被下载"
图表来源
章节来源
幂等性与重复收藏防护
- 幂等性
- 对同一用户/材质重复调用收藏/取消收藏,最终状态与最后一次操作一致
- 重复收藏防护
- user_texture_favorites 表的唯一键 (user_id, texture_id) 防止重复插入
- 服务层通过“是否已收藏”的查询结果决定新增或删除,避免多余写入
章节来源
分页查询示例(收藏列表)
- 请求
- 方法:GET
- 路径:/api/v1/texture/favorites
- 查询参数:
- page:页码(默认 1,最小 1)
- page_size:每页数量(默认 20,最小 1,最大 100)
- 响应
- data:材质数组(每项包含基础元信息)
- total:总数
- page、page_size:当前页与每页条数
- 错误处理
- 未认证:401
- 服务器内部错误:500
章节来源
测试用例验证要点
- 收藏切换逻辑
- 已收藏 -> 取消收藏:返回 false
- 未收藏 -> 添加收藏:返回 true
- 收藏列表分页
- page 小于 1:自动修正为 1
- page_size 超过 100:自动修正为 20
- 其他相关测试
- 材质类型验证、默认值、状态验证、分页边界等
章节来源
依赖分析
- 路由到处理器
- /api/v1/texture/{id}/favorite -> ToggleFavorite
- /api/v1/texture/favorites -> GetUserFavorites
- 处理器到服务层
- ToggleFavorite -> ToggleTextureFavorite
- GetUserFavorites -> GetUserTextureFavorites
- 服务层到仓储层
- ToggleTextureFavorite -> IsTextureFavorited、AddTextureFavorite、RemoveTextureFavorite、IncrementTextureFavoriteCount、DecrementTextureFavoriteCount
- GetUserTextureFavorites -> GetUserTextureFavorites(子查询 + 分页)
- 仓储层到数据库
- 使用 GORM 执行查询与更新,依赖唯一键约束保证幂等
graph LR
R["routes.go"] --> H["texture_handler.go"]
H --> S["texture_service.go"]
S --> RE["texture_repository.go"]
RE --> D["carrotskin_postgres.sql"]
图表来源
- routes.go
- texture_handler.go
- texture_handler.go
- texture_service.go
- texture_repository.go
- carrotskin_postgres.sql
章节来源
- routes.go
- texture_handler.go
- texture_handler.go
- texture_service.go
- texture_repository.go
- carrotskin_postgres.sql
性能考虑
- 索引优化
- textures 表的 favorite_count 降序索引有利于排序与统计
- user_texture_favorites 的 user_id、texture_id 索引提升收藏查询与去重效率
- 写入优化
- 收藏计数采用原子更新(+1/-1),避免额外查询
- 分页限制
- 服务层对 page_size 设定上限,防止大页导致的数据库压力
章节来源
故障排查指南
- 常见错误
- 400:无效的材质ID、请求参数错误
- 401:未认证
- 404:材质不存在
- 500:服务器内部错误
- 排查步骤
- 确认 JWT 是否正确传递
- 校验路径参数 id 是否为有效整数
- 检查数据库连接与迁移是否完成
- 查看处理器日志定位具体错误点
章节来源
结论
- 收藏功能以“取反操作”为核心,通过唯一键约束与服务层条件判断实现幂等
- 收藏列表查询采用子查询 + 分页,兼顾准确性与性能
- 测试用例覆盖了关键分支,确保逻辑正确性
- 数据库层面的索引与约束为高并发场景提供了基础保障
附录
- API 列表
- POST /api/v1/texture/{id}/favorite:切换收藏状态
- GET /api/v1/texture/favorites:获取用户收藏列表(分页)
- 关键实现位置
- 收藏切换:texture_service.go
- 收藏列表:texture_service.go、texture_repository.go
- 路由绑定:routes.go
- 处理器实现:texture_handler.go、texture_handler.go
- 数据模型与表结构:texture.go、carrotskin_postgres.sql