Files
backend/.qoder/repowiki/zh/content/数据模型/材质模型.md
lan a4b6c5011e
Some checks failed
SonarQube Analysis / sonarqube (push) Has been cancelled
chore(git): 更新.gitignore以忽略新的本地文件
2025-11-30 08:33:17 +08:00

408 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 材质模型
<cite>
**本文引用的文件**
- [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)
</cite>
## 目录
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<br/>定义 Texture/TextureType/UserTextureFavorite/TextureDownloadLog"]
M2["internal/model/user.go<br/>定义 User 及其关联"]
end
subgraph "仓储层"
R1["internal/repository/texture_repository.go<br/>材质 CRUD、搜索、计数器更新、收藏与下载日志"]
end
subgraph "服务层"
S1["internal/service/texture_service.go<br/>材质创建/更新/删除/搜索、下载记录、收藏切换、上传限制检查"]
end
subgraph "数据库"
D1["scripts/carrotskin_postgres.sql<br/>表结构、索引、约束、触发器"]
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文件大小字节
- IsSlimSlim 模型标识Alex 细臂为 trueSteve 粗臂为 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.idfavorites 与 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 的并发安全更新,实现了高性能、可扩展且易维护的材质管理能力。状态机与公开性控制使内容治理更加灵活,收藏与下载日志完善了用户行为追踪与运营分析基础。建议在后续迭代中持续关注索引命中率与查询计划,配合缓存与异步任务进一步优化热点查询与批量操作。
## 附录
- 字段与索引对照
- texturesuploader_id、is_public+type+status、download_count、favorite_count、hash 唯一索引
- user_texture_favoritesuser_id、texture_id、created_at、uk_user_texture
- texture_download_logstexture_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)