# 材质模型 **本文引用的文件** - [internal/model/texture.go](file://internal/model/texture.go) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go) - [internal/service/texture_service.go](file://internal/service/texture_service.go) - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql) - [internal/types/common.go](file://internal/types/common.go) - [internal/model/user.go](file://internal/model/user.go) ## 目录 1. [简介](#简介) 2. [项目结构](#项目结构) 3. [核心组件](#核心组件) 4. [架构总览](#架构总览) 5. [详细组件分析](#详细组件分析) 6. [依赖分析](#依赖分析) 7. [性能考虑](#性能考虑) 8. [故障排查指南](#故障排查指南) 9. [结论](#结论) 10. [附录](#附录) ## 简介 本文件围绕材质模型进行系统化技术文档整理,覆盖以下主题: - Texture 结构体及关联类型(TextureType、UserTextureFavorite、TextureDownloadLog)的字段语义与实现细节 - 材质类型(SKIN/CAPE)、哈希值(Hash)、URL 存储、尺寸与 Slim 模型标识的技术实现 - 材质状态机(正常、审核中、已删除)与公开性控制(IsPublic)的业务逻辑 - 与用户(Uploader)、收藏系统、下载日志的关联关系与索引策略 - 材质元数据管理、下载计数器并发更新优化、收藏功能去重机制的实践指导 - 基于 GORM 的复杂查询模式示例与最佳实践 ## 项目结构 材质模型相关代码分布于模型层、仓储层与服务层,并通过数据库初始化脚本定义表结构与索引。 ```mermaid graph TB subgraph "模型层" M1["internal/model/texture.go
定义 Texture/TextureType/UserTextureFavorite/TextureDownloadLog"] M2["internal/model/user.go
定义 User 及其关联"] end subgraph "仓储层" R1["internal/repository/texture_repository.go
材质 CRUD、搜索、计数器更新、收藏与下载日志"] end subgraph "服务层" S1["internal/service/texture_service.go
材质创建/更新/删除/搜索、下载记录、收藏切换、上传限制检查"] end subgraph "数据库" D1["scripts/carrotskin_postgres.sql
表结构、索引、约束、触发器"] end M1 --> R1 M2 --> R1 S1 --> R1 R1 --> D1 ``` 图表来源 - [internal/model/texture.go](file://internal/model/texture.go#L1-L77) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232) - [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252) - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L1-L344) 章节来源 - [internal/model/texture.go](file://internal/model/texture.go#L1-L77) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232) - [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252) - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L1-L344) ## 核心组件 - Texture:材质主实体,包含上传者、名称、描述、类型、URL、哈希、尺寸、公开性、下载/收藏计数、Slim 标识、状态与时间戳,并与 User 建立外键关联 - TextureType:材质类型枚举(SKIN/CAPE) - UserTextureFavorite:用户对材质的收藏关系,具备联合唯一索引以保证去重 - TextureDownloadLog:材质下载记录,包含用户、IP、UA、时间等信息,并与 Texture/User 建立外键关联 章节来源 - [internal/model/texture.go](file://internal/model/texture.go#L1-L77) - [internal/types/common.go](file://internal/types/common.go#L127-L152) - [internal/model/user.go](file://internal/model/user.go#L1-L71) ## 架构总览 材质模型在服务层完成业务编排,在仓储层封装数据库访问,在模型层定义数据结构与关系。数据库层面通过外键、索引与触发器保障一致性与性能。 ```mermaid classDiagram class Texture { +int64 ID +int64 UploaderID +string Name +string Description +TextureType Type +string URL +string Hash +int Size +bool IsPublic +int DownloadCount +int FavoriteCount +bool IsSlim +int16 Status +time CreatedAt +time UpdatedAt } class User { +int64 ID +string Username +string Email +string Avatar +int Points +string Role +int16 Status +string Properties +time LastLoginAt +time CreatedAt +time UpdatedAt } class UserTextureFavorite { +int64 ID +int64 UserID +int64 TextureID +time CreatedAt } class TextureDownloadLog { +int64 ID +int64 TextureID +*int64 UserID +string IPAddress +string UserAgent +time CreatedAt } Texture --> User : "Uploader" UserTextureFavorite --> User : "User" UserTextureFavorite --> Texture : "Texture" TextureDownloadLog --> Texture : "Texture" TextureDownloadLog --> User : "User" ``` 图表来源 - [internal/model/texture.go](file://internal/model/texture.go#L1-L77) - [internal/model/user.go](file://internal/model/user.go#L1-L71) ## 详细组件分析 ### Texture 字段与业务语义 - 类型与哈希 - Type:材质类型,取值为 SKIN 或 CAPE - Hash:材质文件的 SHA-256 哈希,作为全局唯一标识,用于去重与校验 - 存储与元数据 - URL:材质在对象存储中的访问地址 - Size:文件大小(字节) - IsSlim:Slim 模型标识(Alex 细臂为 true,Steve 粗臂为 false) - 公开性与状态 - IsPublic:是否公开到皮肤广场 - Status:状态机(1 正常、0 审核中、-1 已删除) - 计数器与排序 - DownloadCount、FavoriteCount:下载与收藏计数,分别建立降序索引以便高效排序与统计 - 时间戳与索引 - CreatedAt/UpdatedAt:自动维护 - UploaderID、IsPublic+Type+Status、DownloadCount/FavoriteCount 等索引支撑查询与排序 章节来源 - [internal/model/texture.go](file://internal/model/texture.go#L16-L35) - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L39-L85) ### TextureType 与 Slim 模型 - TextureType 枚举在模型与类型定义中保持一致,确保序列化与校验的一致性 - Slim 模型标识用于区分 Alex 与 Steve 模型,便于客户端渲染与兼容 章节来源 - [internal/model/texture.go](file://internal/model/texture.go#L7-L13) - [internal/types/common.go](file://internal/types/common.go#L127-L152) ### 状态机与公开性控制 - 状态机 - 1:正常可用 - 0:审核中(可视为待审状态) - -1:已删除(软删除) - 公开性控制 - IsPublic 控制是否对外可见 - 查询侧影响 - 搜索接口默认仅返回状态为正常的材质 - 用户上传列表过滤掉已删除材质 章节来源 - [internal/model/texture.go](file://internal/model/texture.go#L28-L31) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112) - [internal/service/texture_service.go](file://internal/service/texture_service.go#L93-L103) ### 与用户的关联关系 - 上传者关联 - Texture 与 User 通过 UploaderID 外键关联,查询时可预加载上传者信息 - 角色档案关联 - Profiles 表通过 skin_id/cape_id 引用 textures,形成角色与材质的绑定关系 章节来源 - [internal/model/texture.go](file://internal/model/texture.go#L33-L35) - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L111-L127) ### 收藏系统(UserTextureFavorite) - 关系模型 - 用户与材质的多对多中间表,字段包含 user_id、texture_id、created_at - 联合唯一索引 uk_user_texture,保证同一用户对同一材质只能收藏一次 - 业务流程 - 切换收藏:先检查是否已收藏,再执行新增或删除,并同步更新材质的收藏计数 - 收藏列表:子查询获取用户收藏的材质ID,再按创建时间倒序分页查询 ```mermaid sequenceDiagram participant Client as "客户端" participant Service as "TextureService" participant Repo as "TextureRepository" participant DB as "数据库" Client->>Service : "ToggleTextureFavorite(userID, textureID)" Service->>Repo : "FindTextureByID(textureID)" Repo->>DB : "SELECT * FROM textures WHERE id=?" DB-->>Repo : "Texture" Repo-->>Service : "Texture" Service->>Repo : "IsTextureFavorited(userID, textureID)" Repo->>DB : "SELECT COUNT(*) FROM user_texture_favorites WHERE user_id=? AND texture_id=?" DB-->>Repo : "count" Repo-->>Service : "bool" alt 已收藏 Service->>Repo : "RemoveTextureFavorite(userID, textureID)" Repo->>DB : "DELETE FROM user_texture_favorites WHERE user_id=? AND texture_id=?" Service->>Repo : "DecrementTextureFavoriteCount(textureID)" Repo->>DB : "UPDATE textures SET favorite_count=favorite_count-1 WHERE id=?" else 未收藏 Service->>Repo : "AddTextureFavorite(userID, textureID)" Repo->>DB : "INSERT INTO user_texture_favorites(user_id, texture_id)" Service->>Repo : "IncrementTextureFavoriteCount(textureID)" Repo->>DB : "UPDATE textures SET favorite_count=favorite_count+1 WHERE id=?" end Service-->>Client : "返回收藏状态" ``` 图表来源 - [internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L159-L201) 章节来源 - [internal/model/texture.go](file://internal/model/texture.go#L42-L57) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L159-L201) - [internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225) - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110) ### 下载日志(TextureDownloadLog) - 记录每次下载的材质、用户、IP、UA、时间等信息 - 下载计数器采用 GORM 表达式更新,避免并发写冲突导致的计数不准 - 提供按时间倒序索引,便于统计与审计 ```mermaid sequenceDiagram participant Client as "客户端" participant Service as "TextureService" participant Repo as "TextureRepository" participant DB as "数据库" Client->>Service : "RecordTextureDownload(textureID, userID, ip, ua)" Service->>Repo : "FindTextureByID(textureID)" Repo->>DB : "SELECT * FROM textures WHERE id=?" DB-->>Repo : "Texture" Service->>Repo : "IncrementTextureDownloadCount(textureID)" Repo->>DB : "UPDATE textures SET download_count=download_count+1 WHERE id=?" Service->>Repo : "CreateTextureDownloadLog(log)" Repo->>DB : "INSERT INTO texture_download_logs(texture_id, user_id, ip_address, user_agent)" Service-->>Client : "成功" ``` 图表来源 - [internal/service/texture_service.go](file://internal/service/texture_service.go#L162-L187) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L157) 章节来源 - [internal/model/texture.go](file://internal/model/texture.go#L59-L76) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L157) - [internal/service/texture_service.go](file://internal/service/texture_service.go#L162-L187) - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L271-L291) ### GORM 使用示例与复杂查询模式 - 创建材质 - 参数校验与去重:先检查用户存在与哈希唯一性,再创建材质 - 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64) - 搜索材质 - 过滤条件:状态=正常、公开筛选、类型筛选、关键词模糊匹配 - 分页与总数:先 Count 再分页查询,支持预加载上传者 - 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112) - 更新材质 - 权限校验:仅上传者可更新 - 动态字段更新:根据传入字段选择性更新 - 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L141) - 删除材质(软删除) - 仅将状态置为 -1,保留历史数据与日志 - 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L131) - 收藏切换 - 原子性:先判断是否已收藏,再执行新增/删除并更新计数 - 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225) - 收藏列表 - 子查询:先查出用户收藏的材质ID集合,再查询材质并分页 - 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L190-L221) - 上传限制检查 - 统计用户已上传材质数量,与系统配置的最大值比较 - 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L252) 章节来源 - [internal/service/texture_service.go](file://internal/service/texture_service.go#L12-L64) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112) - [internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L141) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L126-L131) - [internal/service/texture_service.go](file://internal/service/texture_service.go#L189-L225) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L190-L221) - [internal/service/texture_service.go](file://internal/service/texture_service.go#L239-L252) ## 依赖分析 - 模型层依赖 - Texture 依赖 TextureType、User - UserTextureFavorite 依赖 User、Texture - TextureDownloadLog 依赖 User、Texture - 仓储层依赖 - 通过数据库连接池访问 textures、user_texture_favorites、texture_download_logs - 使用 GORM 的表达式更新计数器,避免并发竞争 - 服务层依赖 - 负责业务规则编排:权限校验、去重、状态机、计数器更新、日志记录 - 数据库层 - 外键约束:textures.uploader_id -> user.id;favorites 与 logs 对 textures/user 的引用 - 索引:textures 上的多列索引与计数器降序索引;favorites/logs 上的单列索引 ```mermaid graph LR Service["TextureService"] --> Repo["TextureRepository"] Repo --> Model["Model: Texture/User/Favorite/DownloadLog"] Repo --> DB["PostgreSQL 表: textures/favorites/logs"] Model --> DB ``` 图表来源 - [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232) - [internal/model/texture.go](file://internal/model/texture.go#L1-L77) - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L39-L110) 章节来源 - [internal/service/texture_service.go](file://internal/service/texture_service.go#L1-L252) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L1-L232) - [internal/model/texture.go](file://internal/model/texture.go#L1-L77) - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L39-L110) ## 性能考虑 - 并发计数器更新 - 下载计数与收藏计数均使用 GORM 表达式更新,避免读取-计算-写回的竞态,降低锁竞争 - 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151) - 索引设计 - textures 表的多列索引(is_public, type, status)与计数器降序索引(download_count/favorite_count)支撑高频查询与排序 - favorites/logs 表的关键列建立索引,提升收藏去重与日志检索效率 - 参考路径:[scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L63-L85) - 分页与总数 - 搜索与收藏列表先 Count 再分页查询,避免一次性加载大量数据 - 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112) - 预加载 - 查询时预加载上传者信息,减少 N+1 查询风险 - 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L18-L21) 章节来源 - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151) - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L63-L85) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112) ## 故障排查指南 - 材质不存在或已被删除 - 查询时若返回空或状态为已删除,服务层会抛出明确错误 - 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79) - 权限不足 - 更新/删除材质需校验上传者身份,否则返回权限错误 - 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L120) - 哈希重复 - 创建材质前检查哈希唯一性,重复则拒绝创建 - 参考路径:[internal/service/texture_service.go](file://internal/service/texture_service.go#L23-L31) - 收藏去重失败 - 联合唯一索引 uk_user_texture 保证同一用户对同一材质仅一条收藏记录 - 若出现重复插入,需检查业务层是否正确先查询后插入 - 参考路径:[internal/model/texture.go](file://internal/model/texture.go#L42-L57),[scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110) - 下载计数不准确 - 确认使用表达式更新而非普通更新,避免并发写入导致计数偏差 - 参考路径:[internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151) 章节来源 - [internal/service/texture_service.go](file://internal/service/texture_service.go#L66-L79) - [internal/service/texture_service.go](file://internal/service/texture_service.go#L105-L120) - [internal/service/texture_service.go](file://internal/service/texture_service.go#L23-L31) - [internal/model/texture.go](file://internal/model/texture.go#L42-L57) - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L87-L110) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151) ## 结论 材质模型通过清晰的数据结构、严格的外键与索引设计、以及基于 GORM 的并发安全更新,实现了高性能、可扩展且易维护的材质管理能力。状态机与公开性控制使内容治理更加灵活,收藏与下载日志完善了用户行为追踪与运营分析基础。建议在后续迭代中持续关注索引命中率与查询计划,配合缓存与异步任务进一步优化热点查询与批量操作。 ## 附录 - 字段与索引对照 - textures:uploader_id、is_public+type+status、download_count、favorite_count、hash 唯一索引 - user_texture_favorites:user_id、texture_id、created_at、uk_user_texture - texture_download_logs:texture_id、user_id、ip_address、created_at - 关键查询模式 - 搜索与分页:先 Count 再分页,支持关键词、类型、公开性筛选 - 收藏列表:子查询 + 分页 + 预加载 - 并发计数:表达式更新,避免竞态 章节来源 - [scripts/carrotskin_postgres.sql](file://scripts/carrotskin_postgres.sql#L63-L110) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L71-L112) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L190-L221) - [internal/repository/texture_repository.go](file://internal/repository/texture_repository.go#L132-L151)