# 材质收藏 **本文引用的文件** - [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) ## 目录 1. [简介](#简介) 2. [项目结构](#项目结构) 3. [核心组件](#核心组件) 4. [架构总览](#架构总览) 5. [详细组件分析](#详细组件分析) 6. [依赖分析](#依赖分析) 7. [性能考虑](#性能考虑) 8. [故障排查指南](#故障排查指南) 9. [结论](#结论) 10. [附录](#附录) ## 简介 本文件围绕“材质收藏”能力进行系统化文档化,重点覆盖以下方面: - 收藏/取消收藏的切换逻辑与幂等性设计 - 用户收藏列表的查询机制与分页实现 - API 的请求参数、响应格式与错误处理 - 基于测试用例的逻辑验证与正确性保障 - 数据模型与数据库结构支撑 ## 项目结构 与“材质收藏”直接相关的代码分布在如下层次: - 路由层:定义收藏相关接口路径 - 处理器层:解析请求、调用服务层、封装响应 - 服务层:业务逻辑(收藏切换、收藏列表查询) - 仓储层:数据库访问(收藏状态判断、收藏增删、收藏计数增减、收藏列表查询) - 模型层:数据结构(材质、收藏关系、下载日志) - 数据库脚本:表结构与索引定义 ```mermaid graph TB Routes["路由层
routes.go"] --> Handler["处理器层
texture_handler.go"] Handler --> Service["服务层
texture_service.go"] Service --> Repo["仓储层
texture_repository.go"] Service --> Model["模型层
texture.go"] Repo --> DB["数据库脚本
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) ## 详细组件分析 ### 收藏切换 API(ToggleTextureFavorite) - 接口路径:POST /api/v1/texture/{id}/favorite - 请求参数 - 路径参数:id(材质ID) - 认证:Bearer Token(JWT) - 处理流程 - 校验材质存在性 - 查询当前用户是否已收藏 - 若已收藏:删除收藏记录并减少收藏计数 - 若未收藏:插入收藏记录并增加收藏计数 - 返回布尔值表示新的收藏状态 - 幂等性设计 - 同一用户对同一材质重复调用收藏/取消收藏,最终状态与最后一次操作一致 - 通过唯一约束避免重复收藏记录(见数据库脚本) - 错误处理 - 材质不存在:返回错误 - 数据库异常:返回错误 - 未认证:返回 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) ### 用户收藏列表 API(GetUserTextureFavorites) - 接口路径:GET /api/v1/texture/favorites - 请求参数 - page:页码,默认 1;最小 1 - page_size:每页数量,默认 20;最小 1,最大 100 - 认证:Bearer Token(JWT) - 处理流程 - 校验分页参数边界 - 通过子查询获取当前用户收藏的材质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)