chore(git): 更新.gitignore以忽略新的本地文件
Some checks failed
SonarQube Analysis / sonarqube (push) Has been cancelled

This commit is contained in:
lan
2025-11-30 08:33:17 +08:00
parent 4b4980820f
commit a4b6c5011e
58 changed files with 19353 additions and 0 deletions

View File

@@ -0,0 +1,284 @@
# 分页处理机制
<cite>
**本文引用的文件**
- [texture_service_test.go](file://internal/service/texture_service_test.go)
- [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)
- [response.go](file://internal/model/response.go)
- [common.go](file://internal/types/common.go)
- [routes.go](file://internal/handler/routes.go)
- [texture.go](file://internal/model/texture.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
## 简介
本文件围绕材质Texture相关API的分页处理机制进行系统化梳理重点基于测试用例与实现代码阐明以下内容
- 分页参数的边界处理规则page小于1时自动设为1pageSize小于1或大于100时自动设为20。
- repository层如何通过Offset与Limit实现分页查询并说明Preload('Uploader')对查询性能的影响。
- 分页响应中包含总数total的设计目的与客户端使用建议。
- 分页查询的性能优化策略,包括索引使用与大数据量下的性能考虑。
## 项目结构
与分页处理直接相关的模块分布如下:
- Handler层负责接收HTTP请求、解析分页参数、调用服务层并构造分页响应。
- Service层对分页参数进行边界校正然后委托repository层执行查询。
- Repository层使用GORM构建查询统计总数并按Offset/Limit分页必要时Preload关联对象。
- Model层定义实体及索引为分页查询提供索引支持。
- Response模型统一分页响应结构包含total、page、per_page等字段。
```mermaid
graph TB
subgraph "HTTP层"
R["routes.go<br/>注册路由"]
H["texture_handler.go<br/>处理GET /texture/*"]
end
subgraph "服务层"
S["texture_service.go<br/>边界校正与调用仓库"]
end
subgraph "仓库层"
REPO["texture_repository.go<br/>Offset/Limit分页与Preload"]
end
subgraph "模型层"
M["texture.go<br/>实体与索引"]
end
subgraph "响应模型"
RESP["response.go<br/>PaginationResponse(total/page/per_page)"]
end
R --> H
H --> S
S --> REPO
REPO --> M
H --> RESP
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [texture.go](file://internal/model/texture.go#L16-L36)
- [response.go](file://internal/model/response.go#L10-L18)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [texture.go](file://internal/model/texture.go#L16-L36)
- [response.go](file://internal/model/response.go#L10-L18)
## 核心组件
- 分页参数边界处理在服务层对page与pageSize进行强制校正确保合法范围。
- Offset/Limit分页仓库层使用Offset与Limit执行分页查询并在需要时Preload关联对象。
- 分页响应结构统一的PaginationResponse包含total、page、per_page便于前端计算总页数与导航。
- 索引设计:模型层定义了多处索引,有助于提升分页查询与过滤性能。
章节来源
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [response.go](file://internal/model/response.go#L10-L18)
- [texture.go](file://internal/model/texture.go#L16-L36)
## 架构总览
下图展示了从HTTP请求到分页响应的关键流程以及各层之间的职责划分。
```mermaid
sequenceDiagram
participant C as "客户端"
participant G as "Gin路由(routes.go)"
participant H as "纹理处理器(texture_handler.go)"
participant S as "纹理服务(texture_service.go)"
participant R as "纹理仓库(texture_repository.go)"
participant DB as "数据库(GORM)"
C->>G : "GET /api/v1/texture/my?page=...&page_size=..."
G->>H : "转发到 GetUserTextures"
H->>H : "解析page/page_size(默认1/20)"
H->>S : "GetUserTextures(userID, page, page_size)"
S->>S : "边界校正 : page>=1, 1<=page_size<=100"
S->>R : "FindTexturesByUploaderID(uploaderID, page, pageSize)"
R->>DB : "Count统计总数"
R->>DB : "Preload('Uploader') + Order + Offset + Limit"
DB-->>R : "结果集与总数"
R-->>S : "[]Texture, total"
S-->>H : "[]Texture, total"
H->>H : "转换为TextureInfo切片"
H-->>C : "PaginationResponse{data, total, page, per_page}"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L473-L535)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L69)
## 详细组件分析
### 分页参数边界处理规则
- page边界当page小于1时自动修正为1。
- pageSize边界当pageSize小于1或大于100时自动修正为20。
- 适用范围:服务层对“我的材质”、“搜索材质”、“我的收藏”三类接口均执行上述校正。
```mermaid
flowchart TD
Start(["进入服务层分页处理"]) --> CheckPage["校验 page < 1 ?"]
CheckPage --> |是| FixPage["page = 1"]
CheckPage --> |否| KeepPage["保持原值"]
FixPage --> CheckSize["校验 pageSize < 1 或 > 100 ?"]
KeepPage --> CheckSize
CheckSize --> |是| FixSize["pageSize = 20"]
CheckSize --> |否| KeepSize["保持原值"]
FixSize --> End(["返回仓库层查询"])
KeepSize --> End
```
图表来源
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L102-L161)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L376-L428)
章节来源
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L102-L161)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L376-L428)
### repository层的Offset与Limit实现
- 统计总数先以相同过滤条件执行Count得到total。
- 分页查询计算offset=(page-1)*pageSize随后使用Order排序、Offset与Limit分页并在需要时Preload('Uploader')加载关联用户信息。
- 查询范围控制:
- “我的材质”按uploader_id且status!= -1过滤。
- “搜索材质”按status=1过滤可叠加public_only、type、关键词LIKE。
- “我的收藏”通过子查询获取收藏的texture_id再按status=1过滤。
```mermaid
flowchart TD
QStart["开始查询"] --> BuildQuery["构建基础查询(过滤条件)"]
BuildQuery --> CountTotal["Count统计总数(total)"]
CountTotal --> CalcOffset["计算 offset = (page-1)*pageSize"]
CalcOffset --> Preload["必要时 Preload('Uploader')"]
Preload --> Order["Order 排序(如created_at DESC)"]
Order --> ApplyLimit["Offset + Limit 分页"]
ApplyLimit --> ExecFind["执行查询并返回结果集"]
ExecFind --> Done["返回 []Texture, total"]
```
图表来源
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L69)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
章节来源
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L69)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
### Preload('Uploader')对查询性能的影响
- 优点避免N+1查询问题一次性加载每个材质的Uploader信息减少额外查询次数。
- 潜在代价当分页结果较大时会增加单次查询的数据量与网络传输开销同时JOIN或Preload可能带来额外的内存与CPU消耗。
- 建议在高频分页场景中若Uploader信息非必须可考虑延迟加载或仅在详情页Preload若必须显示作者信息则可在业务上控制pageSize上限平衡性能与体验。
章节来源
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L69)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
### 分页响应中的总数total设计目的与客户端使用建议
- 设计目的:
- 提供准确的总量信息便于前端计算总页数与展示“共X条”等提示。
- 协助客户端实现“无限滚动”或“上拉加载”的边界判断。
- 客户端使用建议:
- 使用 total 与 per_page 计算 total_pagestotal_pages = ceil(total / per_page)。
- 在首次加载后缓存 total避免重复Count带来的性能损耗。
- 若total变化频繁可采用“乐观更新”策略在新增/删除后局部更新列表与total而非全量刷新。
章节来源
- [response.go](file://internal/model/response.go#L10-L18)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_handler.go](file://internal/handler/texture_handler.go#L473-L535)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
### 分页查询的性能优化策略
- 索引使用:
- textures表的多处索引有助于过滤与排序如uploader_id、is_public、hash、download_count、favorite_count、status等。
- 搜索场景建议对name/description建立全文索引或使用更高效的检索方案如向量检索以降低LIKE模糊匹配成本。
- 大数据量下的考虑:
- 控制pageSize上限服务层默认20最大100避免单页过大导致内存与网络压力。
- 使用覆盖索引与选择性高的过滤条件优先,减少扫描范围。
- 对频繁访问的列表页可引入缓存如Redis存储热门查询结果与total结合失效策略保证一致性。
- 对Preload('Uploader')若非必须可延迟加载或按需加载减少不必要的JOIN与数据传输。
章节来源
- [texture.go](file://internal/model/texture.go#L16-L36)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
## 依赖分析
- Handler层依赖Service层提供的分页接口将HTTP参数转换为服务层输入并构造统一的分页响应。
- Service层依赖Repository层执行数据库查询并在必要时进行参数边界校正。
- Repository层依赖Model层的实体定义与索引使用GORM执行Count、Offset/Limit与Preload。
- Response模型提供统一的分页响应结构便于前后端约定。
```mermaid
graph LR
Handler["texture_handler.go"] --> Service["texture_service.go"]
Service --> Repo["texture_repository.go"]
Repo --> Model["texture.go"]
Handler --> Resp["response.go"]
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [texture.go](file://internal/model/texture.go#L16-L36)
- [response.go](file://internal/model/response.go#L10-L18)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [texture.go](file://internal/model/texture.go#L16-L36)
- [response.go](file://internal/model/response.go#L10-L18)
## 性能考量
- 参数校正与分页服务层的边界校正与仓库层的Offset/Limit组合确保查询稳定可控。
- Preload策略在高频列表页中谨慎使用Preload('Uploader'),必要时采用延迟加载或缓存。
- 索引与过滤:利用现有索引减少全表扫描;对搜索关键词建立高效索引或采用替代检索方案。
- 缓存与限流:对热门列表页引入缓存与限流,降低数据库压力。
- 分页上限服务层默认pageSize为20最大100有助于控制单次查询负载。
[本节为通用性能指导,不直接分析具体文件]
## 故障排查指南
- 常见问题与定位思路:
- 分页参数异常确认page与pageSize是否被正确校正小于1设为1超出范围设为默认值
- total与实际条目不符检查过滤条件是否一致如status!= -1、public_only=true等
- 查询缓慢检查是否命中索引、是否使用了不必要的Preload、是否超大pageSize。
- N+1查询确认是否在循环中访问Uploader避免多次查询。
- 相关实现参考路径:
- 分页参数边界处理:[texture_service.go](file://internal/service/texture_service.go#L81-L103)
- 分页查询与总数统计:[texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- 分页响应结构:[response.go](file://internal/model/response.go#L10-L18)
- HTTP层参数解析与响应构造[texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
章节来源
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L112)
- [response.go](file://internal/model/response.go#L10-L18)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
## 结论
本项目的材质相关API分页处理遵循清晰的边界校正与统一响应规范。服务层负责参数合法性保障仓库层通过Count+Offset/Limit实现高效分页并在必要时Preload关联对象。分页响应中的total为前端提供了可靠的分页计算依据。结合现有索引与合理的pageSize上限系统在大多数场景下能够稳定运行。针对高频列表页建议进一步引入缓存与按需加载策略以应对更大规模的数据与更高的并发需求。

View File

@@ -0,0 +1,431 @@
# 材质API
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [texture_handler.go](file://internal/handler/texture_handler.go)
- [texture_service.go](file://internal/service/texture_service.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [texture.go](file://internal/model/texture.go)
- [common.go](file://internal/types/common.go)
- [upload_service.go](file://internal/service/upload_service.go)
- [minio.go](file://pkg/storage/minio.go)
- [auth.go](file://internal/middleware/auth.go)
- [texture_service_test.go](file://internal/service/texture_service_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向“材质管理API”的使用者与维护者系统性梳理材质的搜索、上传、创建、更新、删除、收藏及分页查询能力。基于路由组 /api/v1/texture 的公开与认证两类访问模式详细说明各端点的行为、参数、响应与错误处理并深入解析生成上传URL的流程、材质元数据创建、分页查询策略以及收藏功能的实现细节。同时解释材质与用户的关系及权限控制机制帮助读者快速上手并正确集成。
## 项目结构
- 路由注册集中在路由处理器中,按组划分公开与认证两类接口。
- 处理器负责参数校验、鉴权、调用服务层并返回统一响应。
- 服务层封装业务规则(如权限校验、分页边界、收藏增删计数)。
- 仓储层负责数据库查询与写入(含软删除、计数更新、收藏关联)。
- 模型层定义材质、收藏、下载日志等实体及其索引。
- 上传服务与存储客户端负责生成预签名上传URL与对象命名策略。
- 中间件提供JWT认证与可选认证能力。
```mermaid
graph TB
subgraph "HTTP层"
R["路由注册<br/>routes.go"]
H["处理器<br/>texture_handler.go"]
end
subgraph "服务层"
S["服务<br/>texture_service.go"]
end
subgraph "仓储层"
REPO["仓储<br/>texture_repository.go"]
end
subgraph "模型层"
M["模型<br/>texture.go"]
end
subgraph "上传与存储"
U["上传服务<br/>upload_service.go"]
ST["存储客户端<br/>minio.go"]
end
subgraph "鉴权"
A["认证中间件<br/>auth.go"]
end
R --> H
H --> A
H --> S
S --> REPO
REPO --> M
H --> U
U --> ST
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L1-L600)
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [minio.go](file://pkg/storage/minio.go#L1-L121)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
## 核心组件
- 路由与鉴权
- 公开端点GET /api/v1/texture、GET /api/v1/texture/{id}
- 认证端点POST /api/v1/texture/upload-url、POST /api/v1/texture、PUT /api/v1/texture/{id}、DELETE /api/v1/texture/{id}、POST /api/v1/texture/{id}/favorite、GET /api/v1/texture/my、GET /api/v1/texture/favorites
- 认证中间件要求 Authorization: Bearer <token>,通过后将用户信息注入上下文
- 数据模型
- 材质实体包含上传者ID、名称、描述、类型、URL、哈希、大小、公开状态、下载/收藏计数、是否细臂、状态、时间戳等;并关联上传者
- 收藏关联表 user_texture_favorites 记录用户与材质的收藏关系
- 下载日志表 texture_download_logs 记录下载行为
- 上传与存储
- 生成预签名POST URL限定文件类型、大小范围与过期时间对象路径按用户与材质类型组织
- 服务与仓储
- 提供搜索、分页、权限校验、收藏切换、软删除、计数更新等业务逻辑
- 仓储实现分页查询、总数统计、软删除、收藏增删与计数更新
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
## 架构总览
下图展示从HTTP请求到数据库与存储的关键交互路径涵盖公开搜索、认证上传与创建、权限校验、收藏与分页等。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由<br/>routes.go"
participant H as "处理器<br/>texture_handler.go"
participant A as "认证中间件<br/>auth.go"
participant S as "服务<br/>texture_service.go"
participant REPO as "仓储<br/>texture_repository.go"
participant M as "模型<br/>texture.go"
participant U as "上传服务<br/>upload_service.go"
participant ST as "存储客户端<br/>minio.go"
C->>R : 请求 /api/v1/texture/search
R->>H : 调用 SearchTextures
H->>S : 调用 SearchTextures(db, keyword, type, publicOnly, page, pageSize)
S->>REPO : 查询并统计总数
REPO-->>S : 返回列表与总数
S-->>H : 返回结果
H-->>C : 200 + 分页数据
C->>R : 请求 /api/v1/texture/upload-url (认证)
R->>A : 鉴权
A-->>R : 注入用户ID
R->>H : 调用 GenerateTextureUploadURL
H->>U : 生成预签名POST URL
U->>ST : 生成POST策略与URL
ST-->>U : 返回PostURL、FormData、FileURL
U-->>H : 返回结果
H-->>C : 200 + 上传URL与过期时间
C->>R : 请求 /api/v1/texture (认证)
R->>A : 鉴权
A-->>R : 注入用户ID
R->>H : 调用 CreateTexture
H->>S : 检查上传配额与创建材质
S->>REPO : 写入材质记录
REPO-->>S : 返回新记录
S-->>H : 返回材质信息
H-->>C : 200 + 材质详情
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L1-L600)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [minio.go](file://pkg/storage/minio.go#L82-L121)
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [texture.go](file://internal/model/texture.go#L1-L77)
## 详细组件分析
### 路由与访问模式
- 公开访问
- GET /api/v1/texture搜索材质关键词、类型、公开筛选、分页
- GET /api/v1/texture/{id}:获取材质详情
- 认证访问
- POST /api/v1/texture/upload-url生成材质上传URL预签名POST
- POST /api/v1/texture创建材质记录文件已上传至存储
- PUT /api/v1/texture/{id}:更新材质(仅上传者可操作)
- DELETE /api/v1/texture/{id}:删除材质(软删除,仅上传者可操作)
- POST /api/v1/texture/{id}/favorite切换收藏
- GET /api/v1/texture/my获取当前用户上传的材质列表分页
- GET /api/v1/texture/favorites获取当前用户收藏的材质列表分页
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
### 生成上传URL流程
- 客户端向 POST /api/v1/texture/upload-url 发起请求携带文件名与材质类型SKIN/CAPE
- 处理器校验请求体、获取用户ID并调用上传服务
- 上传服务:
- 校验文件名扩展名与类型
- 选择对应上传配置(大小范围、过期时间)
- 解析存储桶名称textures
- 生成对象名user_{userID}/{textureTypeFolder}/{timestamp}_{originalFileName}
- 生成预签名POST策略含内容长度范围、过期时间返回PostURL、FormData与最终访问URL
- 客户端使用返回的PostURL与FormData直传到对象存储成功后调用 POST /api/v1/texture 创建材质记录
```mermaid
flowchart TD
Start(["开始"]) --> Bind["绑定请求体<br/>文件名/类型"]
Bind --> Validate["校验文件名与类型"]
Validate --> Config["加载上传配置"]
Config --> Bucket["解析存储桶名称"]
Bucket --> ObjName["生成对象名<br/>user_{id}/{type}/{ts}_{fileName}"]
ObjName --> Policy["生成预签名POST策略"]
Policy --> Result["返回PostURL/FormData/FileURL"]
Result --> End(["结束"])
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [minio.go](file://pkg/storage/minio.go#L82-L121)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [minio.go](file://pkg/storage/minio.go#L82-L121)
### 材质元数据创建
- 客户端上传完成后,向 POST /api/v1/texture 提交材质元数据名称、描述、类型、URL、哈希、大小、公开状态、是否细臂
- 处理器:
- 校验请求体
- 检查用户上传配额默认最大100条
- 调用服务层创建材质记录(校验用户存在、去重哈希、转换类型、初始化默认值)
- 写入数据库并返回材质信息
- 服务层:
- 校验用户存在
- 哈希去重检查
- 类型转换SKIN/CAPE
- 初始化状态、下载/收藏计数为0
- 仓储层:
- 插入记录
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L85-L172)
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
- [texture_repository.go](file://internal/repository/texture_repository.go#L9-L13)
- [texture.go](file://internal/model/texture.go#L16-L35)
### 搜索与分页查询
- GET /api/v1/texture 支持:
- keyword关键词名称/描述模糊匹配)
- typeSKIN/CAPE
- public_only仅公开材质
- page/page_size分页最小1最大100默认20
- 服务层对分页参数进行边界修正,仓储层:
- 若 public_only 为真,则过滤 is_public=true
- 按 type 进行精确过滤
- 按 name/description 模糊匹配
- 统计总数并按创建时间倒序分页查询
- 返回统一分页响应list、total、page、page_size、total_pages
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
### 更新与删除
- PUT /api/v1/texture/{id}
- 仅材质上传者可更新
- 支持更新名称、描述、公开状态(可选字段)
- 服务层按非空字段构建更新集合并执行
- DELETE /api/v1/texture/{id}
- 仅材质上传者可删除
- 采用软删除status=-1不影响历史下载/收藏计数
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L419)
- [texture_service.go](file://internal/service/texture_service.go#L105-L160)
- [texture_repository.go](file://internal/repository/texture_repository.go#L126-L131)
### 收藏功能
- POST /api/v1/texture/{id}/favorite
- 切换收藏状态(已收藏则取消,未收藏则添加)
- 服务层先检查材质是否存在,再判断是否已收藏
- 成功后分别更新 user_texture_favorites 与 textures.favorite_count
- 返回 is_favorited 结果
- GET /api/v1/texture/favorites
- 获取当前用户收藏的材质列表分页最小1最大100默认20
- 仓储层通过子查询定位收藏的材质ID再查询并统计总数
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L189-L238)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [texture.go](file://internal/model/texture.go#L42-L57)
### 我的材质
- GET /api/v1/texture/my
- 获取当前用户上传的材质列表分页最小1最大100默认20
- 仓储层按 uploader_id 且 status!=-1 过滤,统计总数并分页查询
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L473-L535)
- [texture_service.go](file://internal/service/texture_service.go#L81-L91)
- [texture_repository.go](file://internal/repository/texture_repository.go#L43-L69)
### 权限控制与用户关系
- 认证中间件要求 Authorization: Bearer <token>并将用户ID、用户名、角色注入上下文
- 权限规则:
- 仅上传者可更新/删除材质
- 仅登录用户可收藏/取消收藏
- 搜索公开材质时可匿名访问
- 上传URL生成与创建材质需登录
- 材质与用户:
- 材质实体包含 uploader_id 字段,指向上传者
- 模型中定义了 Uploader 关联,便于返回上传者信息
章节来源
- [auth.go](file://internal/middleware/auth.go#L1-L79)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L419)
- [texture.go](file://internal/model/texture.go#L16-L35)
## 依赖分析
- 处理器依赖:
- 类型定义:请求/响应结构体(来自 internal/types/common.go
- 服务层:业务逻辑封装
- 存储服务生成预签名URL
- 服务层依赖:
- 仓储层:数据库操作
- 模型层:实体定义
- 仓储层依赖:
- 数据库连接与GORM
- 模型层:实体与索引
- 上传服务依赖:
- 存储客户端生成POST策略与URL
- 配置上传大小范围、过期时间、SSL与Endpoint
```mermaid
graph LR
H["texture_handler.go"] --> T["types/common.go"]
H --> S["texture_service.go"]
S --> REPO["texture_repository.go"]
REPO --> M["texture.go"]
H --> U["upload_service.go"]
U --> ST["minio.go"]
H --> A["auth.go"]
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L1-L600)
- [common.go](file://internal/types/common.go#L86-L191)
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
- [texture.go](file://internal/model/texture.go#L1-L77)
- [upload_service.go](file://internal/service/upload_service.go#L1-L161)
- [minio.go](file://pkg/storage/minio.go#L1-L121)
- [auth.go](file://internal/middleware/auth.go#L1-L79)
## 性能考虑
- 分页参数边界
- 所有分页接口均对 page/page_size 进行边界约束最小1最大100默认20避免过大请求导致数据库压力
- 查询优化
- 搜索接口按 status=1 与可选 is_public、type、关键词进行过滤建议在相关列建立索引以提升查询效率
- 分页查询按 created_at 倒序,结合索引可减少排序成本
- 计数更新
- 收藏/下载计数采用原子更新UpdateColumn降低并发冲突概率
- 上传URL
- 预签名POST策略限制文件大小范围与过期时间减少无效请求与存储压力
章节来源
- [texture_service.go](file://internal/service/texture_service.go#L81-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)
## 故障排查指南
- 认证失败
- 缺少Authorization头或格式不正确返回401
- Token无效返回401
- 参数错误
- 请求体绑定失败返回400
- 无效的材质ID返回400
- 权限不足
- 非上传者尝试更新/删除返回403
- 业务异常
- 材质不存在返回404
- 已达到上传数量上限返回400
- 哈希重复返回400
- 上传URL生成失败
- 文件名不合法、类型不支持、存储桶不存在、生成策略失败返回400
- 搜索/分页异常
- 数据库查询失败返回500
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [texture_handler.go](file://internal/handler/texture_handler.go#L85-L172)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L419)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L473-L599)
- [texture_service.go](file://internal/service/texture_service.go#L12-L64)
- [texture_service.go](file://internal/service/texture_service.go#L105-L160)
- [texture_service.go](file://internal/service/texture_service.go#L189-L238)
## 结论
材质API围绕公开搜索与认证上传两条主线设计通过严格的参数校验、权限控制与分页策略保障可用性与性能。上传流程采用预签名POST策略既简化客户端实现又保证安全性。收藏与分页查询进一步完善了用户体验。建议在生产环境中为常用查询列建立索引并结合监控与日志持续优化性能与稳定性。
## 附录
### API端点一览公开与认证
- 公开
- GET /api/v1/texture搜索材质关键词、类型、公开筛选、分页
- GET /api/v1/texture/{id}:获取材质详情
- 认证
- POST /api/v1/texture/upload-url生成材质上传URL预签名POST
- POST /api/v1/texture创建材质记录
- PUT /api/v1/texture/{id}:更新材质
- DELETE /api/v1/texture/{id}:删除材质
- POST /api/v1/texture/{id}/favorite切换收藏
- GET /api/v1/texture/my我的材质分页
- GET /api/v1/texture/favorites我的收藏分页
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
### 请求/响应要点
- 生成上传URL
- 请求体file_name、texture_typeSKIN/CAPE
- 响应体post_url、form_data、texture_url、expires_in
- 创建材质
- 请求体name、description、type、url、hash、size、is_public、is_slim
- 响应体:完整材质信息(含计数与时间戳)
- 搜索
- 查询参数keyword、type、public_only、page、page_size
- 响应体:分页列表与总数
- 收藏
- 请求体:无
- 响应体is_favorited
- 我的材质/收藏
- 查询参数page、page_size
- 响应体:分页列表与总数
章节来源
- [common.go](file://internal/types/common.go#L86-L191)
- [texture_handler.go](file://internal/handler/texture_handler.go#L18-L83)
- [texture_handler.go](file://internal/handler/texture_handler.go#L85-L172)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L473-L599)

View File

@@ -0,0 +1,145 @@
# 材质上传与管理
<cite>
**本文档引用的文件**
- [texture_service.go](file://internal/service/texture_service.go)
- [upload_service.go](file://internal/service/upload_service.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [texture.go](file://internal/model/texture.go)
- [routes.go](file://internal/handler/routes.go)
</cite>
## 目录
1. [简介](#简介)
2. [上传流程与元数据创建](#上传流程与元数据创建)
3. [上传限制检查机制](#上传限制检查机制)
4. [材质更新API权限控制](#材质更新api权限控制)
5. [字段更新策略](#字段更新策略)
6. [错误处理场景](#错误处理场景)
## 简介
本系统提供完整的材质上传与管理功能支持用户上传皮肤SKIN和披风CAPE材质。系统通过预签名URL实现安全的文件上传确保只有经过身份验证的用户才能上传和管理自己的材质。材质元数据存储在数据库中包含名称、描述、类型、哈希值等信息。系统实现了严格的权限控制确保只有上传者才能修改或删除自己的材质。同时系统还提供了收藏、下载统计等功能增强了用户体验。
## 上传流程与元数据创建
材质上传流程分为两个阶段生成上传URL和创建材质元数据。首先用户通过调用`/api/v1/texture/upload-url`接口获取预签名的上传URL。该接口验证用户身份、文件名和材质类型后生成一个临时的上传链接允许用户直接上传文件到存储服务。上传完成后用户调用`/api/v1/texture`接口创建材质元数据。系统会验证用户是否存在、材质哈希是否重复并创建相应的数据库记录。材质元数据包括上传者ID、名称、描述、类型、URL、哈希值、大小、公开状态等信息。
```mermaid
sequenceDiagram
participant 用户 as 用户
participant 处理器 as texture_handler
participant 服务 as upload_service
participant 存储 as 存储服务
用户->>处理器 : POST /api/v1/texture/upload-url
处理器->>服务 : GenerateTextureUploadURL()
服务->>服务 : 验证文件名和类型
服务->>存储 : 生成预签名POST URL
存储-->>服务 : 返回预签名URL
服务-->>处理器 : 返回URL
处理器-->>用户 : 返回上传URL
用户->>存储 : 使用预签名URL上传文件
存储-->>用户 : 上传成功
用户->>处理器 : POST /api/v1/texture
处理器->>服务 : CreateTexture()
服务->>服务 : 验证用户和哈希
服务->>repository : CreateTexture()
repository-->>服务 : 创建成功
服务-->>处理器 : 返回材质信息
处理器-->>用户 : 返回创建结果
```
**图示来源**
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [texture_service.go](file://internal/service/texture_service.go#L12-L63)
- [routes.go](file://internal/handler/routes.go#L53-L54)
**本节来源**
- [upload_service.go](file://internal/service/upload_service.go#L117-L160)
- [texture_service.go](file://internal/service/texture_service.go#L12-L63)
## 上传限制检查机制
系统通过`CheckTextureUploadLimit`函数实现用户上传数量限制。该函数接收数据库连接、上传者ID和最大允许上传数量作为参数。首先函数调用`CountTexturesByUploaderID`从数据库中统计指定用户已上传的材质数量。然后将统计结果与最大限制进行比较。如果当前上传数量大于或等于最大限制函数返回错误信息提示用户已达到最大上传数量限制。否则函数返回nil表示可以继续上传。系统配置中定义了每个用户的最大材质数量限制默认值为100。
```mermaid
flowchart TD
Start([开始]) --> Count["调用CountTexturesByUploaderID<br/>统计用户上传数量"]
Count --> Compare{"当前数量 >= 最大限制?"}
Compare --> |是| ReturnError["返回错误信息:<br/>已达到最大上传数量限制"]
Compare --> |否| ReturnSuccess["返回nil允许上传"]
ReturnError --> End([结束])
ReturnSuccess --> End
```
**图示来源**
- [texture_service.go](file://internal/service/texture_service.go#L239-L251)
- [texture_repository.go](file://internal/repository/texture_repository.go#L223-L231)
**本节来源**
- [texture_service.go](file://internal/service/texture_service.go#L239-L251)
## 材质更新API权限控制
材质更新API实现了严格的权限控制确保只有上传者才能修改自己的材质。当用户尝试更新材质时系统首先通过`FindTextureByID`从数据库中获取材质信息。然后比较请求中的上传者ID与材质记录中的`UploaderID`。如果两者不匹配,系统返回"无权修改此材质"的错误信息。这种基于上传者ID的权限检查机制有效防止了未经授权的修改操作。权限检查在`UpdateTexture`服务函数中实现,是更新操作的第一步,确保在进行任何数据修改之前完成身份验证。
```mermaid
flowchart TD
Start([开始]) --> GetTexture["调用FindTextureByID<br/>获取材质信息"]
GetTexture --> CheckExist{"材质存在?"}
CheckExist --> |否| ReturnError1["返回错误:<br/>材质不存在"]
CheckExist --> |是| CheckPermission{"UploaderID匹配?"}
CheckPermission --> |否| ReturnError2["返回错误:<br/>无权修改此材质"]
CheckPermission --> |是| UpdateFields["更新指定字段"]
UpdateFields --> ReturnSuccess["返回更新后的材质"]
ReturnError1 --> End([结束])
ReturnError2 --> End
ReturnSuccess --> End
```
**图示来源**
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
- [texture_repository.go](file://internal/repository/texture_repository.go#L16-L27)
**本节来源**
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
## 字段更新策略
系统采用灵活的字段更新策略,仅更新客户端提供的字段。当更新材质时,系统创建一个`updates`映射来存储需要更新的字段。如果提供了名称且不为空,将其添加到`updates`映射中;如果提供了描述且不为空,也将其添加到映射中;如果提供了公开状态(`isPublic`),同样将其添加到映射中。只有当`updates`映射不为空时,系统才会调用`UpdateTextureFields`执行数据库更新操作。这种策略避免了不必要的数据库写入,提高了性能,并确保未提供的字段保持原值不变。
```mermaid
flowchart TD
Start([开始]) --> InitMap["初始化updates映射"]
InitMap --> CheckName{"名称提供且不为空?"}
CheckName --> |是| AddName["添加name到updates映射"]
CheckName --> |否| CheckDesc
AddName --> CheckDesc
CheckDesc --> {"描述提供且不为空?"}
CheckDesc --> |是| AddDesc["添加description到updates映射"]
CheckDesc --> |否| CheckPublic
AddDesc --> CheckPublic
CheckPublic --> {"公开状态提供?"}
CheckPublic --> |是| AddPublic["添加is_public到updates映射"]
CheckPublic --> |否| CheckUpdates
AddPublic --> CheckUpdates
CheckUpdates --> {"updates映射为空?"}
CheckUpdates --> |是| ReturnOriginal["返回原材质"]
CheckUpdates --> |否| UpdateDB["调用UpdateTextureFields<br/>更新数据库"]
UpdateDB --> ReturnUpdated["返回更新后的材质"]
ReturnOriginal --> End([结束])
ReturnUpdated --> End
```
**图示来源**
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
**本节来源**
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
## 错误处理场景
系统在材质上传与管理过程中实现了全面的错误处理机制。当用户达到上传上限时,`CheckTextureUploadLimit`函数返回"已达到最大上传数量限制"的错误信息。当用户尝试修改不属于自己的材质时,`UpdateTexture`函数返回"无权修改此材质"的错误信息。其他常见错误包括:用户不存在、材质已存在、无效的材质类型、文件名为空、不支持的文件格式等。所有错误都通过标准的错误响应格式返回给客户端,包含错误代码和描述信息,便于前端进行相应的错误处理和用户提示。
**本节来源**
- [texture_service.go](file://internal/service/texture_service.go#L239-L251)
- [texture_service.go](file://internal/service/texture_service.go#L106-L141)
- [upload_service.go](file://internal/service/upload_service.go#L120-L127)
- [texture_service.go](file://internal/service/texture_service.go#L19-L21)
- [texture_service.go](file://internal/service/texture_service.go#L28-L30)
- [texture_service.go](file://internal/service/texture_service.go#L40-L41)

View File

@@ -0,0 +1,327 @@
# 材质上传流程
<cite>
**本文档引用文件**
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go)
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
- [internal/service/texture_service.go](file://internal/service/texture_service.go)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
- [internal/model/texture.go](file://internal/model/texture.go)
- [pkg/storage/minio.go](file://pkg/storage/minio.go)
- [pkg/config/config.go](file://pkg/config/config.go)
</cite>
## 目录
1. [流程概述](#流程概述)
2. [API接口说明](#api接口说明)
3. [生成预签名上传URL](#生成预签名上传url)
4. [创建材质记录](#创建材质记录)
5. [错误处理机制](#错误处理机制)
6. [流程时序图](#流程时序图)
## 流程概述
材质上传流程采用分步式设计包含两个核心API接口`GenerateTextureUploadURL``CreateTexture`。该流程遵循安全最佳实践通过预签名URL机制实现文件上传确保文件在上传到存储系统后才在数据库中创建相应记录。
整个流程分为两个阶段:
1. **准备阶段**:客户端调用`GenerateTextureUploadURL`接口服务器验证权限后返回预签名上传URL和表单数据
2. **完成阶段**:客户端使用返回的凭证上传文件后,调用`CreateTexture`接口创建材质元数据记录
这种设计确保了只有成功上传的文件才会在系统中创建记录,避免了数据库中出现孤立的记录。
## API接口说明
材质上传相关的API接口定义在路由配置中主要包含以下两个核心接口
```mermaid
flowchart TD
A[客户端] --> B[GenerateTextureUploadURL]
A --> C[CreateTexture]
B --> D[返回预签名URL和表单数据]
C --> E[创建材质记录]
```
**接口来源**
- [internal/handler/routes.go](file://internal/handler/routes.go#L53-L54)
## 生成预签名上传URL
`GenerateTextureUploadURL`接口负责生成临时的文件上传凭证,使客户端能够直接上传文件到对象存储系统。
### 接口实现
该接口的处理流程如下:
```mermaid
flowchart TD
Start([开始]) --> AuthCheck["验证用户身份"]
AuthCheck --> ValidateInput["验证请求参数"]
ValidateInput --> CheckFileName["验证文件名"]
CheckFileName --> CheckTextureType["验证材质类型"]
CheckTextureType --> GetStorageConfig["获取存储配置"]
GetStorageConfig --> GenerateObjectName["生成对象名称"]
GenerateObjectName --> GeneratePresignedURL["生成预签名POST URL"]
GeneratePresignedURL --> ReturnResult["返回PostURL和FormData"]
ReturnResult --> End([结束])
```
**代码来源**
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L28-L83)
- [internal/service/upload_service.go](file://internal/service/upload_service.go#L117-L160)
### 请求参数
请求体包含以下字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| fileName | string | 上传的文件名 |
| textureType | string | 材质类型SKIN或CAPE |
### 响应格式
成功响应包含预签名上传所需的所有信息:
```json
{
"code": 200,
"message": "success",
"data": {
"postURL": "https://storage.example.com/textures",
"formData": {
"key": "user_1/skin/20231201120000_texture.png",
"policy": "base64-encoded-policy",
"signature": "request-signature",
"AWSAccessKeyId": "access-key-id"
},
"textureURL": "https://storage.example.com/textures/user_1/skin/20231201120000_texture.png",
"expiresIn": 900
}
}
```
### 实现细节
1. **文件名验证**:检查文件扩展名是否为`.png`确保只允许上传PNG格式的材质文件
2. **类型验证**:确认材质类型为`SKIN``CAPE`之一
3. **对象名称生成**:采用`user_{userId}/{textureType}/timestamp_{originalFileName}`的格式,确保文件路径的唯一性
4. **预签名URL生成**:调用存储模块的`GeneratePresignedPostURL`方法创建临时上传凭证
**存储实现来源**
- [pkg/storage/minio.go](file://pkg/storage/minio.go#L82-L120)
## 创建材质记录
`CreateTexture`接口在文件上传完成后创建材质的元数据记录,将文件与用户关联起来。
### 接口实现
该接口的处理流程包含多个验证步骤:
```mermaid
flowchart TD
Start([开始]) --> AuthCheck["验证用户身份"]
AuthCheck --> ValidateInput["验证请求参数"]
ValidateInput --> CheckLimit["检查上传数量限制"]
CheckLimit --> CheckHash["检查文件Hash是否重复"]
CheckHash --> ValidateUser["验证用户存在"]
ValidateUser --> ConvertType["转换材质类型"]
ConvertType --> CreateRecord["创建材质记录"]
CreateRecord --> ReturnResult["返回材质信息"]
ReturnResult --> End([结束])
style CheckLimit fill:#f9f,stroke:#333
style CheckHash fill:#f9f,stroke:#333
```
**代码来源**
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L95-L172)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64)
### 请求参数
请求体包含材质的元数据信息:
| 字段 | 类型 | 说明 |
|------|------|------|
| name | string | 材质名称 |
| description | string | 描述信息 |
| type | string | 材质类型SKIN或CAPE |
| url | string | 文件访问URL |
| hash | string | 文件SHA-256哈希值 |
| size | int | 文件大小(字节) |
| isPublic | boolean | 是否公开 |
| isSlim | boolean | 是否为细身模型 |
### 响应格式
成功创建材质后返回材质的详细信息:
```json
{
"code": 200,
"message": "success",
"data": {
"id": 123,
"uploaderID": 1,
"name": "My Skin",
"description": "A custom skin",
"type": "SKIN",
"url": "https://storage.example.com/textures/user_1/skin/20231201120000_texture.png",
"hash": "sha256-hash-value",
"size": 10240,
"isPublic": true,
"downloadCount": 0,
"favoriteCount": 0,
"isSlim": false,
"status": 1,
"createdAt": "2023-12-01T12:00:00Z",
"updatedAt": "2023-12-01T12:00:00Z"
}
}
```
### 核心验证逻辑
#### 上传数量限制检查
系统通过`CheckTextureUploadLimit`函数检查用户是否达到上传上限:
```go
func CheckTextureUploadLimit(db *gorm.DB, uploaderID int64, maxTextures int) error {
count, err := repository.CountTexturesByUploaderID(uploaderID)
if err != nil {
return err
}
if count >= int64(maxTextures) {
return fmt.Errorf("已达到最大上传数量限制(%d)", maxTextures)
}
return nil
}
```
**代码来源**
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L251)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L223-L231)
#### 重复上传防止
通过文件的SHA-256哈希值防止重复上传相同内容的材质
```go
// 检查Hash是否已存在
existingTexture, err := repository.FindTextureByHash(hash)
if err != nil {
return nil, err
}
if existingTexture != nil {
return nil, errors.New("该材质已存在")
}
```
**代码来源**
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L23-L30)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L29-L41)
## 错误处理机制
系统针对不同场景提供了详细的错误响应,帮助客户端正确处理各种异常情况。
### 错误码定义
| 错误码 | HTTP状态码 | 说明 |
|--------|------------|------|
| 400 | 400 | 请求参数错误 |
| 401 | 401 | 未授权访问 |
| 403 | 403 | 无权操作 |
| 404 | 404 | 资源不存在 |
### 常见错误场景
#### 上传数量达到上限
当用户上传的材质数量达到系统限制时,返回以下错误:
```json
{
"code": 400,
"message": "已达到最大上传数量限制(100)",
"data": null
}
```
#### 权限不足
未登录用户或非上传者尝试操作时返回:
```json
{
"code": 401,
"message": "Unauthorized",
"data": null
}
```
#### 文件Hash冲突
上传已存在的材质文件时返回:
```json
{
"code": 400,
"message": "该材质已存在",
"data": null
}
```
#### 无效的材质类型
指定不支持的材质类型时返回:
```json
{
"code": 400,
"message": "无效的材质类型: INVALID",
"data": null
}
```
## 流程时序图
以下是完整的材质上传流程时序图:
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Handler as "处理器"
participant Service as "服务层"
participant Storage as "存储模块"
participant DB as "数据库"
Client->>Handler : 调用GenerateTextureUploadURL
Handler->>Service : 验证参数并调用GenerateTextureUploadURL
Service->>Service : 验证文件名和类型
Service->>Storage : 获取存储桶名称
Storage-->>Service : 返回存储桶名称
Service->>Storage : 生成预签名POST URL
Storage-->>Service : 返回PostURL和FormData
Service-->>Handler : 返回结果
Handler-->>Client : 返回预签名上传信息
Client->>Storage : 使用PostURL上传文件
Storage-->>Client : 上传成功响应
Client->>Handler : 调用CreateTexture
Handler->>Service : 验证参数并调用CreateTexture
Service->>DB : 检查用户上传数量限制
DB-->>Service : 返回数量统计
Service->>DB : 检查文件Hash是否重复
DB-->>Service : 返回查询结果
Service->>DB : 创建材质记录
DB-->>Service : 创建成功
Service-->>Handler : 返回材质信息
Handler-->>Client : 返回创建结果
```
**时序图来源**
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go)
- [internal/service/upload_service.go](file://internal/service/upload_service.go)
- [internal/service/texture_service.go](file://internal/service/texture_service.go)

View File

@@ -0,0 +1,460 @@
# 材质管理操作
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [auth.go](file://internal/middleware/auth.go)
- [texture_handler.go](file://internal/handler/texture_handler.go)
- [texture_service.go](file://internal/service/texture_service.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [texture.go](file://internal/model/texture.go)
- [common.go](file://internal/types/common.go)
- [response.go](file://internal/model/response.go)
- [manager.go](file://pkg/database/manager.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向后端开发者与运维人员,系统化梳理材质管理模块的增删改查与收藏能力,重点覆盖以下内容:
- UpdateTexture 接口的权限控制与字段级更新策略
- DeleteTexture 接口的软删除与权限校验
- ToggleFavorite 接口的收藏切换与 FavoriteCount 同步
- GetTexture、SearchTextures、GetUserTextures 的使用方式、分页与过滤规则
- 请求/响应结构、认证要求与常见错误码说明
## 项目结构
材质管理相关代码采用典型的分层架构:
- 路由层:注册 API 路由与鉴权中间件
- 处理层HTTP 控制器,负责参数解析、鉴权、调用服务层并返回统一响应
- 服务层:业务逻辑编排,包含权限校验、字段级更新、软删除、收藏切换等
- 仓储层:数据库访问封装,提供查询、更新、计数等方法
- 模型层:实体定义与数据库映射
- 类型与响应:请求/响应结构体与统一响应模型
```mermaid
graph TB
subgraph "路由层"
R["routes.go<br/>注册纹理相关路由"]
end
subgraph "中间件"
M["auth.go<br/>JWT鉴权中间件"]
end
subgraph "处理层"
H["texture_handler.go<br/>控制器:更新/删除/收藏/查询"]
end
subgraph "服务层"
S["texture_service.go<br/>业务逻辑:权限/字段更新/软删除/收藏切换"]
end
subgraph "仓储层"
REPO["texture_repository.go<br/>数据库访问:查询/更新/计数"]
end
subgraph "模型层"
MOD["texture.go<br/>实体Texture/UserTextureFavorite"]
end
subgraph "类型与响应"
T["common.go<br/>请求/响应结构体"]
RESP["response.go<br/>统一响应模型"]
end
DB["manager.go<br/>数据库初始化/迁移"]
R --> M --> H --> S --> REPO --> DB
H --> T
H --> RESP
S --> MOD
REPO --> MOD
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
- [texture.go](file://internal/model/texture.go#L15-L77)
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)
- [manager.go](file://pkg/database/manager.go#L52-L99)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
- [texture.go](file://internal/model/texture.go#L15-L77)
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)
- [manager.go](file://pkg/database/manager.go#L52-L99)
## 核心组件
- 路由与鉴权
- 纹理路由组在 v1 下注册,其中部分接口需 JWT 鉴权;公开接口无需认证
- 鉴权中间件从 Authorization 头提取 Bearer Token 并校验,通过后将用户信息注入上下文
- 处理器
- 提供 UpdateTexture、DeleteTexture、ToggleFavorite、GetTexture、SearchTextures、GetUserTextures 等接口
- 参数绑定、分页默认值设置、统一响应封装
- 服务层
- UpdateTexture 字段级更新策略:仅当字段非空/非零时才更新
- DeleteTexture 软删除:将 Status 设为删除态,不影响数据完整性
- ToggleFavorite根据收藏状态切换同步更新 FavoriteCount
- 查询接口:分页参数校验与默认值处理
- 仓储层
- 提供按条件查询、分页、计数、字段更新、软删除、收藏相关 CRUD 等
- 模型层
- Texture包含上传者、名称、描述、类型、URL、哈希、大小、公开状态、下载/收藏计数、状态等
- UserTextureFavorite收藏关联表
- 类型与响应
- UpdateTextureRequest、CreateTextureRequest、TextureInfo 等结构体
- 统一响应模型与常用状态码
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
- [texture.go](file://internal/model/texture.go#L15-L77)
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)
## 架构总览
下图展示“更新材质”接口的端到端调用链路,体现鉴权、参数校验、权限检查、字段级更新与返回结果的完整流程。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由(routes.go)"
participant MW as "鉴权中间件(auth.go)"
participant H as "控制器(texture_handler.go)"
participant S as "服务层(texture_service.go)"
participant REPO as "仓储层(texture_repository.go)"
participant DB as "数据库"
C->>R : "PUT /api/v1/texture/ : id"
R->>MW : "进入AuthMiddleware()"
MW-->>R : "校验通过注入user_id"
R->>H : "转发到UpdateTexture处理器"
H->>H : "参数绑定/校验"
H->>S : "service.UpdateTexture(id, user_id, name, description, is_public?)"
S->>REPO : "FindTextureByID(id)"
REPO-->>S : "返回Texture或nil"
S->>S : "权限校验UploaderID==user_id"
S->>S : "构建字段更新map仅非空/非零字段"
S->>REPO : "UpdateTextureFields(id, updates)"
REPO->>DB : "执行UPDATE"
DB-->>REPO : "OK"
S->>REPO : "FindTextureByID(id)"
REPO-->>S : "返回更新后的Texture"
S-->>H : "返回Texture"
H-->>C : "200 成功响应"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L50-L60)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L369)
- [texture_service.go](file://internal/service/texture_service.go#L105-L141)
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
## 详细组件分析
### UpdateTexture 接口:权限控制与字段级更新
- 权限控制
- 仅允许材质上传者修改;服务层通过比较 Texture 的 UploaderID 与请求用户ID进行校验
- 字段级更新策略
- 仅当请求中的 Name 非空、Description 非空、IsPublic 指针非空时,才将其加入更新集合
- 若无字段需要更新,则跳过数据库写入
- 最终重新查询并返回更新后的材质对象
- 认证要求
- 需携带 Bearer Token 的 Authorization 头
- 常见错误码
- 401 未授权(缺失/无效Token
- 403 权限不足(非上传者)
- 400 请求参数错误如ID非法
```mermaid
flowchart TD
Start(["进入UpdateTexture"]) --> Parse["解析请求参数与用户ID"]
Parse --> Load["加载材质记录"]
Load --> Exists{"是否存在且未删除?"}
Exists --> |否| Err404["返回404/不存在"]
Exists --> |是| Perm["校验权限UploaderID==user_id"]
Perm --> |否| Err403["返回403/权限不足"]
Perm --> |是| Build["构建字段更新map仅非空/非零字段"]
Build --> HasAny{"是否有字段需要更新?"}
HasAny --> |否| Reload["直接重新查询并返回"]
HasAny --> |是| Save["执行字段更新"]
Save --> Reload["重新查询并返回"]
Reload --> End(["结束"])
Err404 --> End
Err403 --> End
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L369)
- [texture_service.go](file://internal/service/texture_service.go#L105-L141)
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L369)
- [texture_service.go](file://internal/service/texture_service.go#L105-L141)
- [texture_repository.go](file://internal/repository/texture_repository.go#L120-L124)
### DeleteTexture 接口:软删除与权限验证
- 软删除实现
- 仓储层通过将 Status 更新为删除态(-1实现软删除保留数据以满足审计与历史追踪
- 权限验证
- 仅允许材质上传者删除;服务层在删除前进行权限校验
- 认证要求
- 需携带 Bearer Token 的 Authorization 头
- 常见错误码
- 401 未授权
- 403 权限不足
- 400 请求参数错误如ID非法
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "控制器(texture_handler.go)"
participant S as "服务层(texture_service.go)"
participant REPO as "仓储层(texture_repository.go)"
participant DB as "数据库"
C->>H : "DELETE /api/v1/texture/ : id"
H->>S : "service.DeleteTexture(id, user_id)"
S->>REPO : "FindTextureByID(id)"
REPO-->>S : "返回Texture或nil"
S->>S : "权限校验UploaderID==user_id"
S->>REPO : "DeleteTexture(id) -> 更新status=-1"
REPO->>DB : "执行UPDATE"
DB-->>REPO : "OK"
S-->>H : "返回nil成功"
H-->>C : "200 成功响应"
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
### ToggleFavorite 接口:收藏切换与计数同步
- 功能流程
- 检查材质是否存在且未删除
- 查询当前用户是否已收藏
- 已收藏:取消收藏并递减 FavoriteCount
- 未收藏:添加收藏并递增 FavoriteCount
- 返回新的收藏状态
- 认证要求
- 需携带 Bearer Token 的 Authorization 头
- 常见错误码
- 401 未授权
- 400 请求参数错误或业务异常
```mermaid
flowchart TD
Start(["进入ToggleFavorite"]) --> Load["加载材质记录"]
Load --> Exists{"是否存在且未删除?"}
Exists --> |否| Err["返回错误"]
Exists --> |是| Check["查询是否已收藏"]
Check --> Favorited{"已收藏?"}
Favorited --> |是| Unfav["删除收藏记录"] --> Dec["递减FavoriteCount"] --> RetFalse["返回false"]
Favorited --> |否| Fav["新增收藏记录"] --> Inc["递增FavoriteCount"] --> RetTrue["返回true"]
RetFalse --> End(["结束"])
RetTrue --> End
Err --> End
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L187)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L187)
### 查询接口GetTexture、SearchTextures、GetUserTextures
- GetTexture
- 公开接口,无需认证
- 根据ID查询材质若不存在或已被软删除则返回404
- SearchTextures
- 公开接口,无需认证
- 支持关键词、类型、公开筛选;分页参数默认值与范围校验
- GetUserTextures
- 需 JWT 鉴权
- 仅返回当前用户上传且未删除的材质,支持分页
- 分页机制
- page 默认 1pageSize 默认 20最大 100
- 数据过滤规则
- SearchTexturesstatus=1可按 is_public 过滤;按 name/description 模糊匹配
- GetUserTexturesuploader_id=user_id 且 status!=-1
- GetTexturestatus!=deleted
```mermaid
sequenceDiagram
participant C as "客户端"
participant H as "控制器(texture_handler.go)"
participant S as "服务层(texture_service.go)"
participant REPO as "仓储层(texture_repository.go)"
C->>H : "GET /api/v1/texture/ : id"
H->>S : "service.GetTextureByID(id)"
S->>REPO : "FindTextureByID(id)"
REPO-->>S : "Texture或nil"
S-->>H : "返回Texture或错误"
H-->>C : "200/404"
C->>H : "GET /api/v1/texture?page&page_size&type=SKIN&public_only=true"
H->>S : "service.SearchTextures(keyword,type,public_only,page,pageSize)"
S->>REPO : "SearchTextures(...)"
REPO-->>S : "[]Texture, total"
S-->>H : "返回列表与总数"
H-->>C : "200 分页响应"
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L174-L291)
- [texture_service.go](file://internal/service/texture_service.go#L66-L104)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L174-L291)
- [texture_service.go](file://internal/service/texture_service.go#L66-L104)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
## 依赖分析
- 组件耦合
- 路由层仅负责注册与中间件装配,低耦合
- 处理器依赖服务层,服务层依赖仓储层,仓储层依赖数据库
- 外部依赖
- JWT 鉴权中间件依赖认证服务
- 数据库初始化与自动迁移由数据库管理器负责
- 潜在循环依赖
- 代码组织清晰,未发现循环依赖迹象
```mermaid
graph LR
Routes["routes.go"] --> Auth["auth.go"]
Routes --> Handler["texture_handler.go"]
Handler --> Service["texture_service.go"]
Service --> Repo["texture_repository.go"]
Repo --> DBMgr["manager.go"]
Handler --> Types["common.go"]
Handler --> Resp["response.go"]
Service --> Model["texture.go"]
Repo --> Model
```
图表来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
- [texture.go](file://internal/model/texture.go#L15-L77)
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)
- [manager.go](file://pkg/database/manager.go#L52-L99)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L105-L225)
- [texture_repository.go](file://internal/repository/texture_repository.go#L114-L180)
- [texture.go](file://internal/model/texture.go#L15-L77)
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)
- [manager.go](file://pkg/database/manager.go#L52-L99)
## 性能考虑
- 分页与索引
- 查询接口均使用分页与排序,仓储层对关键字段建立索引(如公开状态、下载/收藏计数、创建时间等),有助于提升检索性能
- 字段级更新
- UpdateTexture 仅更新提供字段,减少不必要的数据库写入
- 软删除
- 通过状态字段实现软删除,避免全量删除带来的性能与数据恢复成本
- 并发与事务
- 仓储层使用 GORM 执行单条 UPDATE/计数更新,建议在高并发场景下结合数据库层面的乐观锁或唯一约束保障一致性
[本节为通用指导,不涉及具体文件分析]
## 故障排查指南
- 401 未授权
- 检查 Authorization 头格式是否为 Bearer TokenToken 是否有效
- 403 权限不足
- 确认请求用户是否为材质上传者;检查 Update/Delete/ToggleFavorite 的权限校验逻辑
- 400 请求参数错误
- 检查 ID 是否为合法整数;请求体字段是否符合绑定规则
- 404 资源不存在
- 确认材质是否存在且未被软删除;查询接口会过滤 status=-1 的记录
- 分页异常
- page/page_size 默认值与范围校验page<1 设为 1pageSize 超过 100 或小于 1 设为 20
章节来源
- [auth.go](file://internal/middleware/auth.go#L12-L56)
- [texture_handler.go](file://internal/handler/texture_handler.go#L293-L471)
- [texture_service.go](file://internal/service/texture_service.go#L66-L104)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
## 结论
本模块围绕“最小权限+字段级更新+软删除+计数同步”的设计原则,提供了完善的材质管理能力。通过鉴权中间件与服务层权限校验,确保只有上传者可修改/删除材质UpdateTexture 的字段级更新策略降低写放大ToggleFavorite 在变更收藏状态的同时同步 FavoriteCount保证数据一致性查询接口提供灵活的过滤与分页能力兼顾可用性与性能。
[本节为总结性内容,不涉及具体文件分析]
## 附录
### 接口一览与认证要求
- GET /api/v1/texture/:id
- 认证:否
- 功能:获取材质详情
- GET /api/v1/texture
- 认证:否
- 功能:搜索材质(关键词、类型、公开筛选)
- PUT /api/v1/texture/:id
- 认证Bearer
- 功能:更新材质(仅上传者)
- DELETE /api/v1/texture/:id
- 认证Bearer
- 功能:软删除材质(仅上传者)
- POST /api/v1/texture/:id/favorite
- 认证Bearer
- 功能:切换收藏状态
- GET /api/v1/texture/my
- 认证Bearer
- 功能:获取当前用户上传的材质列表
- GET /api/v1/texture/favorites
- 认证Bearer
- 功能:获取当前用户收藏的材质列表
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [auth.go](file://internal/middleware/auth.go#L12-L56)
### 请求/响应结构与字段说明
- UpdateTextureRequest
- 字段name、description、is_public指针
- 说明:仅当字段非空/非零时参与更新
- CreateTextureRequest
- 字段name、description、type、url、hash、size、is_public、is_slim
- TextureInfo
- 字段id、uploader_id、name、description、type、url、hash、size、is_public、download_count、favorite_count、is_slim、status、created_at、updated_at
- 统一响应模型
- 成功code=200message=“操作成功”data=业务数据
- 错误code=400/401/403/404/500message=错误描述error=详细错误信息(开发环境)
章节来源
- [common.go](file://internal/types/common.go#L86-L152)
- [response.go](file://internal/model/response.go#L1-L86)

View File

@@ -0,0 +1,306 @@
# 材质删除
<cite>
**本文引用的文件**
- [internal/service/texture_service.go](file://internal/service/texture_service.go)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go)
- [internal/model/texture.go](file://internal/model/texture.go)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go)
- [internal/handler/routes.go](file://internal/handler/routes.go)
- [internal/service/texture_service_test.go](file://internal/service/texture_service_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
## 简介
本专项文档聚焦“材质删除”API围绕删除操作的权限验证机制展开结合服务层与仓储层的实现说明删除流程、成功响应、错误场景以及删除对数据库记录的影响软删除与关联数据处理收藏关系。同时基于测试用例确认删除权限校验逻辑与行为一致性。
## 项目结构
与材质删除相关的代码分布在以下模块:
- 路由注册定义DELETE /api/v1/texture/:id接口
- 处理器:解析参数、鉴权、调用服务层并返回响应
- 服务层:执行业务规则(权限校验、存在性校验)、调用仓储层
- 仓储层:执行数据库操作(软删除)
- 数据模型:定义材质实体、收藏关系、下载日志等
```mermaid
graph TB
Routes["路由注册<br/>routes.go"] --> Handler["处理器<br/>texture_handler.go"]
Handler --> Service["服务层<br/>texture_service.go"]
Service --> Repo["仓储层<br/>texture_repository.go"]
Repo --> Model["数据模型<br/>texture.go"]
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
## 核心组件
- 路由与鉴权
- DELETE /api/v1/texture/:id 由处理器绑定,使用鉴权中间件,要求携带有效令牌。
- 处理器
- 解析路径参数材质ID从上下文提取当前用户ID调用服务层执行删除并按错误码返回相应HTTP状态与响应体。
- 服务层
- 校验材质存在性;校验删除权限(仅上传者可删);调用仓储层执行软删除。
- 仓储层
- 将材质记录的status字段置为-1实现软删除。
- 数据模型
- 材质实体包含UploaderID、Status等字段收藏关系通过user_texture_favorites表维护。
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
## 架构总览
下图展示从客户端到数据库的完整调用链路,包括鉴权、参数解析、权限校验、软删除与响应返回。
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由(routes.go)"
participant H as "处理器(texture_handler.go)"
participant S as "服务层(texture_service.go)"
participant P as "仓储层(texture_repository.go)"
participant D as "数据库"
C->>R : "DELETE /api/v1/texture/ : id"
R->>H : "进入处理器"
H->>H : "解析路径参数/校验JWT"
H->>S : "service.DeleteTexture(db, textureID, uploaderID)"
S->>P : "FindTextureByID(textureID)"
P-->>S : "返回材质或nil"
alt "材质不存在"
S-->>H : "返回错误:材质不存在"
H-->>C : "403 错误响应"
else "材质存在"
S->>S : "校验 uploaderID == 请求者ID"
alt "非上传者"
S-->>H : "返回错误:无权删除此材质"
H-->>C : "403 错误响应"
else "上传者本人"
S->>P : "DeleteTexture(textureID)"
P->>D : "UPDATE textures SET status=-1 WHERE id=..."
D-->>P : "OK"
P-->>S : "OK"
S-->>H : "OK"
H-->>C : "200 成功响应"
end
end
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
## 详细组件分析
### 删除API请求流程与权限验证
- 路由与鉴权
- DELETE /api/v1/texture/:id 由处理器绑定,使用鉴权中间件,要求携带有效令牌。
- 参数解析与鉴权
- 处理器从路径参数解析材质ID从上下文提取当前用户ID若缺失则返回未授权。
- 权限验证
- 服务层先查询材质是否存在且未被软删除;
- 再校验请求者ID与材质的UploaderID一致否则返回无权删除。
- 执行删除
- 通过仓储层将材质记录的status字段置为-1完成软删除。
- 响应
- 成功返回200与通用成功响应体错误返回403及错误信息。
```mermaid
flowchart TD
Start(["开始"]) --> Parse["解析路径参数<br/>获取材质ID"]
Parse --> Auth{"JWT鉴权通过"}
Auth --> |否| Resp401["返回401 未授权"]
Auth --> |是| Load["查询材质记录"]
Load --> Exists{"是否存在且未软删除?"}
Exists --> |否| Resp403a["返回403 材质不存在/已删除"]
Exists --> |是| Perm{"请求者ID==上传者ID"}
Perm --> |否| Resp403b["返回403 无权删除此材质"]
Perm --> |是| SoftDel["软删除设置status=-1"]
SoftDel --> Resp200["返回200 成功"]
Resp401 --> End(["结束"])
Resp403a --> End
Resp403b --> End
Resp200 --> End
```
图表来源
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
### 权限验证机制
- 上传者身份判定
- 服务层比较请求者的用户ID与材质记录中的UploaderID仅当两者相等时才允许删除。
- 存在性与状态校验
- 查询材质时会忽略status=-1的记录软删除因此若返回nil即视为“不存在”。
- 测试覆盖
- 单元测试包含“删除权限检查”的用例验证相同ID允许删除、不同ID拒绝删除的行为。
章节来源
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/service/texture_service_test.go](file://internal/service/texture_service_test.go#L315-L345)
### 删除API的成功响应与错误情况
- 成功响应
- HTTP 200返回通用成功响应体无额外数据
- 常见错误
- 401 未授权缺少或无效的JWT令牌。
- 400 参数错误材质ID格式非法。
- 403 无权删除:请求者非材质上传者。
- 403 材质不存在:目标材质不存在或已被软删除。
- 处理器侧错误分支
- 参数解析失败返回400
- 服务层返回错误时统一转换为403无权删除/不存在)。
章节来源
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
### 数据库记录影响与关联数据处理
- 删除策略
- 采用软删除将textures表的status字段置为-1不物理移除记录。
- 影响范围
- 材质记录仍保留,便于审计与历史追踪;
- 查询接口如获取详情、搜索、我的材质均会忽略status=-1的记录。
- 关联数据
- 收藏关系删除操作不直接清理user_texture_favorites表中的收藏记录
- 若需清理收藏,应在业务层面另行设计或在仓储层扩展软删除时级联处理(当前实现未体现)。
- 下载日志
- 删除操作不涉及texture_download_logs表的清理。
章节来源
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79)
### 类图(代码级)
```mermaid
classDiagram
class Texture {
+int64 id
+int64 uploader_id
+string name
+string description
+TextureType type
+string url
+string hash
+int size
+bool is_public
+int download_count
+int favorite_count
+bool is_slim
+int16 status
+time created_at
+time updated_at
}
class UserTextureFavorite {
+int64 id
+int64 user_id
+int64 texture_id
+time created_at
}
class TextureDownloadLog {
+int64 id
+int64 texture_id
+*int64 user_id
+string ip_address
+string user_agent
+time created_at
}
Texture --> UserTextureFavorite : "收藏关系"
Texture --> TextureDownloadLog : "下载日志"
```
图表来源
- [internal/model/texture.go](file://internal/model/texture.go#L16-L35)
- [internal/model/texture.go](file://internal/model/texture.go#L42-L57)
- [internal/model/texture.go](file://internal/model/texture.go#L60-L71)
## 依赖分析
- 组件耦合
- 处理器依赖服务层;服务层依赖仓储层;仓储层依赖数据库访问工具。
- 关键依赖链
- 路由 -> 处理器 -> 服务层 -> 仓储层 -> 数据库
- 可能的循环依赖
- 当前文件间未发现循环导入;各层职责清晰,符合分层架构。
```mermaid
graph LR
Routes["routes.go"] --> Handler["texture_handler.go"]
Handler --> Service["texture_service.go"]
Service --> Repo["texture_repository.go"]
Repo --> DB["数据库"]
```
图表来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
章节来源
- [internal/handler/routes.go](file://internal/handler/routes.go#L42-L61)
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
- [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L130)
## 性能考虑
- 查询与软删除
- 删除前的查询与软删除均为单记录操作,复杂度低。
- 索引与过滤
- 材质查询通常按UploaderID、状态、公开性等条件过滤建议确保相关列具备索引以提升查询效率。
- 批量与事务
- 当前删除为单条记录操作,无需事务;若未来扩展批量删除,需评估事务边界与回滚策略。
## 故障排查指南
- 401 未授权
- 检查请求头Authorization是否携带有效JWT确认中间件已正确注入user_id。
- 400 参数错误
- 检查路径参数id是否为合法整数。
- 403 无权删除
- 确认当前用户ID与材质记录的UploaderID一致核对服务层权限校验逻辑。
- 403 材质不存在
- 确认材质ID有效且未被软删除检查仓储层查询是否正确忽略status=-1。
- 日志定位
- 处理器与服务层均记录错误日志,可据此快速定位问题。
章节来源
- [internal/handler/texture_handler.go](file://internal/handler/texture_handler.go#L371-L419)
- [internal/service/texture_service.go](file://internal/service/texture_service.go#L143-L160)
## 结论
- 权限验证严格仅上传者可删除材质服务层明确校验请求者ID与UploaderID一致性。
- 删除策略为软删除通过status字段标记删除不破坏历史数据与关联完整性。
- 错误处理清晰处理器将服务层错误映射为403配合日志便于排障。
- 关联清理:当前实现未清理收藏关系,如需可在业务层补充或扩展仓储层软删除逻辑。

View File

@@ -0,0 +1,243 @@
# 材质搜索
<cite>
**本文引用的文件**
- [routes.go](file://internal/handler/routes.go)
- [texture_handler.go](file://internal/handler/texture_handler.go)
- [texture_service.go](file://internal/service/texture_service.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [texture.go](file://internal/model/texture.go)
- [common.go](file://internal/types/common.go)
- [texture_service_test.go](file://internal/service/texture_service_test.go)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向“材质搜索API”的使用与实现围绕关键词搜索、材质类型过滤、公开性筛选与分页功能进行深入解析。基于仓库中的 SearchTextures 查询流程解释关键词匹配名称与描述、类型过滤、公开状态筛选的实现细节并结合分页测试用例说明分页参数的处理规则page小于1时设为1pageSize超过100时设为20。同时提供请求示例、响应数据结构说明与错误处理机制并解释搜索结果中上传者信息的预加载机制Preload及其对性能的影响。
## 项目结构
材质搜索API由三层协作完成
- 路由层:注册 /api/v1/texture 的 GET 搜索接口
- 处理层:解析查询参数、调用服务层并返回分页响应
- 服务层:规范化分页参数、调用仓储层执行查询
- 仓储层:构建查询条件、统计总数、分页查询并预加载上传者信息
```mermaid
graph TB
Client["客户端"] --> Routes["路由: /api/v1/texture(GET)"]
Routes --> Handler["处理器: SearchTextures"]
Handler --> Service["服务: SearchTextures"]
Service --> Repo["仓储: SearchTextures"]
Repo --> DB["数据库"]
Handler --> Resp["分页响应: list,total,page,page_size,total_pages"]
```
图表来源
- [routes.go](file://internal/handler/routes.go#L43-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
章节来源
- [routes.go](file://internal/handler/routes.go#L43-L61)
## 核心组件
- 路由注册:在 v1 组下将 GET /api/v1/texture 绑定到处理器 SearchTextures
- 处理器:读取 keyword、type、public_only、page、page_size 查询参数,调用服务层,转换为统一响应结构
- 服务层对分页参数进行边界校正page<1 设为1pageSize<1 或 >100 设为20再调用仓储层
- 仓储层:按状态=1 进行基础过滤;公开筛选、类型筛选、关键词模糊匹配;先 Count 再分页查询;使用 Preload 预加载上传者信息
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
## 架构总览
下面以序列图展示一次完整搜索请求的调用链路:
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "路由"
participant H as "处理器"
participant S as "服务层"
participant RE as "仓储层"
participant D as "数据库"
C->>R : GET /api/v1/texture?keyword=&type=&public_only=&page=&page_size=
R->>H : 调用 SearchTextures
H->>H : 解析查询参数<br/>keyword,type,public_only,page,page_size
H->>S : SearchTextures(db, keyword, type, public_only, page, page_size)
S->>S : 校正分页参数<br/>page<1→1pageSize<1或>100→20
S->>RE : SearchTextures(keyword, type, public_only, page, page_size)
RE->>D : 构建查询条件(status=1)<br/>公开筛选(is_public=?)<br/>类型筛选(type=?)
RE->>D : 关键词模糊匹配(name/description)<br/>Count统计总数
RE->>D : 分页查询(Offset/Limit)<br/>Preload上传者信息
D-->>RE : 结果集+总数
RE-->>S : 返回结果集+总数
S-->>H : 返回结果集+总数
H-->>C : 200 + 分页响应
```
图表来源
- [routes.go](file://internal/handler/routes.go#L43-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
## 详细组件分析
### 请求参数与处理规则
- 查询参数
- keyword关键词支持名称与描述的模糊匹配
- type材质类型可选 SKIN/CAPE
- public_only布尔值仅返回公开材质
- page页码默认1
- page_size每页数量默认20
- 参数边界校正(服务层)
- page 小于1时设为1
- page_size 小于1时设为20大于100时也设为20
- 类型与公开筛选
- 类型为空字符串时不参与筛选
- public_only 为真时追加 is_public=true 条件
- 关键词匹配
- 同时对 name 和 description 使用 LIKE 模糊匹配
- 排序与分页
- 默认按 created_at 降序排序
- Offset=(page-1)*page_sizeLimit=page_size
- 预加载上传者信息
- 使用 Preload("Uploader") 预加载关联用户信息,便于直接返回上传者字段
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
### 数据模型与关联
- 材质模型包含上传者外键关联,仓储层通过 Preload 加载上传者信息
- 上传者信息在响应中以嵌套对象形式返回,便于前端直接显示
章节来源
- [texture.go](file://internal/model/texture.go#L16-L35)
### 响应数据结构
- 分页响应包含:
- list结果数组元素为材质信息
- total总条目数
- page当前页码
- page_size每页数量
- total_pages总页数由通用分页响应类型计算
章节来源
- [common.go](file://internal/types/common.go#L18-L25)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
### 错误处理机制
- 参数解析失败:返回 400 错误
- 服务内部错误:返回 500 错误
- 业务错误(如材质不存在等)在其他接口中体现,搜索接口主要返回 500 表示查询失败
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
### 分页测试用例要点
- page<1 → 1
- pageSize<1 → 20
- pageSize>100 → 20
- 以上规则在服务层统一应用,确保查询稳定性
章节来源
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
## 依赖关系分析
- 路由层依赖处理器层
- 处理器层依赖服务层
- 服务层依赖仓储层
- 仓储层依赖数据库层
- 响应结构依赖通用分页类型
```mermaid
graph LR
Routes["routes.go"] --> Handler["texture_handler.go"]
Handler --> Service["texture_service.go"]
Service --> Repo["texture_repository.go"]
Repo --> Model["texture.go"]
Handler --> Types["common.go"]
Service --> Types
```
图表来源
- [routes.go](file://internal/handler/routes.go#L43-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture.go](file://internal/model/texture.go#L16-L35)
- [common.go](file://internal/types/common.go#L18-L25)
## 性能考量
- 预加载上传者信息Preload会增加单次查询的 JOIN 数量,导致额外的网络往返与内存占用。建议:
- 在高频搜索场景中评估是否需要返回上传者信息;若不需要,可在仓储层移除 Preload
- 对搜索结果进行缓存(如 Redis以减少重复 COUNT 与分页查询
- 为常用筛选维度建立合适索引(例如 idx_textures_public_type_status、idx_textures_download_count 等)
- 关键词模糊匹配使用 LIKE 百分号前缀可能导致索引失效,建议:
- 评估是否需要全文检索或倒排索引
- 对高并发场景考虑异步搜索或搜索引擎(如 Elasticsearch
章节来源
- [texture_repository.go](file://internal/repository/texture_repository.go#L71-L112)
- [texture.go](file://internal/model/texture.go#L16-L35)
## 故障排查指南
- 搜索无结果
- 检查 keyword 是否过短或包含特殊字符
- 确认 public_only 是否设置为 true 导致过滤掉私有材质
- 确认 type 是否正确传入SKIN/CAPE
- 分页异常
- page 小于1会被自动修正为1
- page_size 超过100会被修正为20
- 参数错误
- 确认查询参数类型与默认值是否符合预期
- 服务器错误
- 查看服务层日志,确认数据库连接与查询是否报错
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
- [texture_service.go](file://internal/service/texture_service.go#L93-L103)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L163-L215)
## 结论
材质搜索API通过清晰的三层职责划分实现了关键词、类型与公开性三类筛选并以稳健的分页参数校正保障了查询稳定性。预加载上传者信息提升了前端展示效率但需关注其带来的性能成本。建议在生产环境中结合缓存与索引优化进一步提升搜索吞吐与延迟表现。
## 附录
### 请求示例
- 基础搜索(关键词)
- GET /api/v1/texture?keyword=steve
- 类型筛选
- GET /api/v1/texture?type=SKIN
- 公开性筛选
- GET /api/v1/texture?public_only=true
- 组合查询
- GET /api/v1/texture?keyword=cape&type=CAPE&public_only=true&page=1&page_size=20
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)
### 响应示例
- 成功响应包含分页字段list、total、page、page_size、total_pages
- 每个材质项包含id、uploader_id、name、description、type、url、hash、size、is_public、download_count、favorite_count、is_slim、status、created_at、updated_at
章节来源
- [common.go](file://internal/types/common.go#L18-L25)
- [texture_handler.go](file://internal/handler/texture_handler.go#L225-L291)

View File

@@ -0,0 +1,411 @@
# 材质收藏
<cite>
**本文引用的文件**
- [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)
</cite>
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖分析](#依赖分析)
7. [性能考虑](#性能考虑)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件围绕“材质收藏”能力进行系统化文档化,重点覆盖以下方面:
- 收藏/取消收藏的切换逻辑与幂等性设计
- 用户收藏列表的查询机制与分页实现
- API 的请求参数、响应格式与错误处理
- 基于测试用例的逻辑验证与正确性保障
- 数据模型与数据库结构支撑
## 项目结构
与“材质收藏”直接相关的代码分布在如下层次:
- 路由层:定义收藏相关接口路径
- 处理器层:解析请求、调用服务层、封装响应
- 服务层:业务逻辑(收藏切换、收藏列表查询)
- 仓储层:数据库访问(收藏状态判断、收藏增删、收藏计数增减、收藏列表查询)
- 模型层:数据结构(材质、收藏关系、下载日志)
- 数据库脚本:表结构与索引定义
```mermaid
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](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [texture.go](file://internal/model/texture.go#L16-L57)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [texture.go](file://internal/model/texture.go#L16-L57)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
## 核心组件
- 收藏切换接口POST /api/v1/texture/{id}/favorite
- 收藏列表接口GET /api/v1/texture/favorites
- 数据模型:材质、用户-材质收藏关系、下载日志
- 仓储方法:收藏状态判断、收藏增删、收藏计数增减、收藏列表查询
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture.go](file://internal/model/texture.go#L16-L57)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
## 架构总览
收藏能力的端到端流程如下:
- 客户端向收藏接口发起请求
- 路由层匹配到处理器
- 处理器解析参数、调用服务层
- 服务层根据收藏状态决定新增或删除收藏,并同步更新收藏计数
- 仓储层执行数据库操作
- 处理器封装响应返回客户端
```mermaid
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}"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L57-L57)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
## 详细组件分析
### 收藏切换 APIToggleTextureFavorite
- 接口路径POST /api/v1/texture/{id}/favorite
- 请求参数
- 路径参数id材质ID
- 认证Bearer TokenJWT
- 处理流程
- 校验材质存在性
- 查询当前用户是否已收藏
- 若已收藏:删除收藏记录并减少收藏计数
- 若未收藏:插入收藏记录并增加收藏计数
- 返回布尔值表示新的收藏状态
- 幂等性设计
- 同一用户对同一材质重复调用收藏/取消收藏,最终状态与最后一次操作一致
- 通过唯一约束避免重复收藏记录(见数据库脚本)
- 错误处理
- 材质不存在:返回错误
- 数据库异常:返回错误
- 未认证:返回 401
```mermaid
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
```
图表来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110)
### 用户收藏列表 APIGetUserTextureFavorites
- 接口路径GET /api/v1/texture/favorites
- 请求参数
- page页码默认 1最小 1
- page_size每页数量默认 20最小 1最大 100
- 认证Bearer TokenJWT
- 处理流程
- 校验分页参数边界
- 通过子查询获取当前用户收藏的材质ID集合
- 基于材质状态过滤(仅返回正常状态)
- 分页查询并返回总数
- 响应格式
- 包含分页信息与材质列表(每项包含基础元信息)
```mermaid
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}"
```
图表来源
- [routes.go](file://internal/handler/routes.go#L59-L59)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L227-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L227-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
### 数据模型与数据库结构
- 材质模型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
- 用于统计与风控
```mermaid
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 : "被下载"
```
图表来源
- [texture.go](file://internal/model/texture.go#L16-L57)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L272-L292)
章节来源
- [texture.go](file://internal/model/texture.go#L16-L57)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L272-L292)
### 幂等性与重复收藏防护
- 幂等性
- 对同一用户/材质重复调用收藏/取消收藏,最终状态与最后一次操作一致
- 重复收藏防护
- user_texture_favorites 表的唯一键 (user_id, texture_id) 防止重复插入
- 服务层通过“是否已收藏”的查询结果决定新增或删除,避免多余写入
章节来源
- [texture_repository.go](file://internal/repository/texture_repository.go#L172-L187)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110)
### 分页查询示例(收藏列表)
- 请求
- 方法GET
- 路径:/api/v1/texture/favorites
- 查询参数:
- page页码默认 1最小 1
- page_size每页数量默认 20最小 1最大 100
- 响应
- data材质数组每项包含基础元信息
- total总数
- page、page_size当前页与每页条数
- 错误处理
- 未认证401
- 服务器内部错误500
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L227-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
### 测试用例验证要点
- 收藏切换逻辑
- 已收藏 -> 取消收藏:返回 false
- 未收藏 -> 添加收藏:返回 true
- 收藏列表分页
- page 小于 1自动修正为 1
- page_size 超过 100自动修正为 20
- 其他相关测试
- 材质类型验证、默认值、状态验证、分页边界等
章节来源
- [texture_service_test.go](file://internal/service/texture_service_test.go#L347-L374)
- [texture_service_test.go](file://internal/service/texture_service_test.go#L376-L428)
## 依赖分析
- 路由到处理器
- /api/v1/texture/{id}/favorite -> ToggleFavorite
- /api/v1/texture/favorites -> GetUserFavorites
- 处理器到服务层
- ToggleFavorite -> ToggleTextureFavorite
- GetUserFavorites -> GetUserTextureFavorites
- 服务层到仓储层
- ToggleTextureFavorite -> IsTextureFavorited、AddTextureFavorite、RemoveTextureFavorite、IncrementTextureFavoriteCount、DecrementTextureFavoriteCount
- GetUserTextureFavorites -> GetUserTextureFavorites子查询 + 分页)
- 仓储层到数据库
- 使用 GORM 执行查询与更新,依赖唯一键约束保证幂等
```mermaid
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](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
章节来源
- [routes.go](file://internal/handler/routes.go#L42-L61)
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- [texture_service.go](file://internal/service/texture_service.go#L189-L237)
- [texture_repository.go](file://internal/repository/texture_repository.go#L159-L221)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)
## 性能考虑
- 索引优化
- textures 表的 favorite_count 降序索引有利于排序与统计
- user_texture_favorites 的 user_id、texture_id 索引提升收藏查询与去重效率
- 写入优化
- 收藏计数采用原子更新(+1/-1避免额外查询
- 分页限制
- 服务层对 page_size 设定上限,防止大页导致的数据库压力
章节来源
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L63-L68)
- [carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L99-L103)
- [texture_repository.go](file://internal/repository/texture_repository.go#L139-L151)
- [texture_service.go](file://internal/service/texture_service.go#L227-L237)
## 故障排查指南
- 常见错误
- 400无效的材质ID、请求参数错误
- 401未认证
- 404材质不存在
- 500服务器内部错误
- 排查步骤
- 确认 JWT 是否正确传递
- 校验路径参数 id 是否为有效整数
- 检查数据库连接与迁移是否完成
- 查看处理器日志定位具体错误点
章节来源
- [texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)
- [texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
## 结论
- 收藏功能以“取反操作”为核心,通过唯一键约束与服务层条件判断实现幂等
- 收藏列表查询采用子查询 + 分页,兼顾准确性与性能
- 测试用例覆盖了关键分支,确保逻辑正确性
- 数据库层面的索引与约束为高并发场景提供了基础保障
## 附录
- API 列表
- POST /api/v1/texture/{id}/favorite切换收藏状态
- GET /api/v1/texture/favorites获取用户收藏列表分页
- 关键实现位置
- 收藏切换:[texture_service.go](file://internal/service/texture_service.go#L189-L237)
- 收藏列表:[texture_service.go](file://internal/service/texture_service.go#L227-L237)、[texture_repository.go](file://internal/repository/texture_repository.go#L189-L221)
- 路由绑定:[routes.go](file://internal/handler/routes.go#L57-L59)
- 处理器实现:[texture_handler.go](file://internal/handler/texture_handler.go#L421-L471)、[texture_handler.go](file://internal/handler/texture_handler.go#L537-L599)
- 数据模型与表结构:[texture.go](file://internal/model/texture.go#L16-L57)、[carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L43-L110)