319 lines
10 KiB
Markdown
319 lines
10 KiB
Markdown
# 皮肤服务 (SkinService)
|
||
|
||
此项目是一个使用 **Go-zero** 框架构建的、专注于材质管理的纯 RPC 微服务。它为CarrotSkin皮肤站提供了完整的材质上传、存储、查询和管理功能,并集成了基于 MinIO 的高效文件存储系统。
|
||
|
||
## 核心功能
|
||
|
||
- **安全材质上传**: 利用 MinIO 预签名 POST URL,实现客户端直传,支持PNG格式限制和文件大小控制(1KB-1MB)
|
||
- **智能去重机制**: 基于 SHA-256 哈希值自动检测并复用相同材质,节省存储空间
|
||
- **完整材质管理**: 提供材质的增、删、改、查(CRUD)等全套操作
|
||
- **分类存储**: 支持皮肤(SKIN)和披风(CAPE)两种材质类型的分类管理
|
||
- **个人皮肤库**: 用户可查看和管理自己上传的所有材质
|
||
- **皮肤广场**: 公开材质展示平台,支持按类型浏览和搜索
|
||
- **收藏夹功能**: 支持用户收藏和管理自己的收藏列表
|
||
- **高性能查询**: 支持分页查询、批量获取和条件搜索
|
||
- **权限控制**: 严格的材质所有权验证,确保用户只能操作自己的材质
|
||
|
||
## 技术特性
|
||
|
||
- **微服务架构**: 基于 gRPC 的高性能服务间通信
|
||
- **对象存储**: MinIO 分布式存储,支持海量文件管理
|
||
- **数据库缓存**: go-zero 内置缓存机制,提升查询性能
|
||
- **类型安全**: 完整的 protobuf 定义和数据验证
|
||
- **容错设计**: 优雅的错误处理和日志记录
|
||
- **代码生成**: 使用 `goctl` 从 `.proto` 和 `.sql` 文件自动生成代码
|
||
|
||
## API 接口参考
|
||
|
||
服务通过 gRPC 暴露,接口定义于 `docs/textures.proto`。
|
||
|
||
### 材质上传流程
|
||
|
||
| 方法名 | 功能描述 | 请求类型 | 响应类型 |
|
||
|--------|----------|----------|----------|
|
||
| `GenerateTextureUploadURL` | 生成材质上传预签名URL | `GenerateTextureUploadURLRequest` | `GenerateTextureUploadURLResponse` |
|
||
| `CreateTexture` | 创建材质记录(上传完成后调用) | `CreateTextureRequest` | `CreateTextureResponse` |
|
||
|
||
**上传流程说明**:
|
||
1. 客户端调用 `GenerateTextureUploadURL` 获取预签名上传URL和表单数据
|
||
2. 客户端使用返回的 `post_url` 和 `form_data` 直接向MinIO上传文件
|
||
3. 上传成功后,客户端调用 `CreateTexture` 将材质信息记录到数据库
|
||
|
||
### 材质管理接口
|
||
|
||
| 方法名 | 功能描述 | 请求类型 | 响应类型 |
|
||
|--------|----------|----------|----------|
|
||
| `GetTexture` | 获取单个材质信息 | `GetTextureRequest` | `GetTextureResponse` |
|
||
| `UpdateTexture` | 更新材质信息(公开/私有状态) | `UpdateTextureRequest` | `UpdateTextureResponse` |
|
||
| `DeleteTexture` | 删除材质(含MinIO文件清理) | `DeleteTextureRequest` | `DeleteTextureResponse` |
|
||
|
||
### 查询接口
|
||
|
||
| 方法名 | 功能描述 | 请求类型 | 响应类型 |
|
||
|--------|----------|----------|----------|
|
||
| `GetUserTextures` | 获取用户个人材质库 | `GetUserTexturesRequest` | `GetUserTexturesResponse` |
|
||
| `GetPublicTextures` | 获取皮肤广场公开材质 | `GetPublicTexturesRequest` | `GetPublicTexturesResponse` |
|
||
| `SearchTextures` | 搜索材质 | `SearchTexturesRequest` | `SearchTexturesResponse` |
|
||
|
||
### 高级功能接口
|
||
|
||
| 方法名 | 功能描述 | 请求类型 | 响应类型 |
|
||
|--------|----------|----------|----------|
|
||
| `GetTextureByHash` | 根据哈希值查找材质(防重复上传) | `GetTextureByHashRequest` | `GetTextureByHashResponse` |
|
||
| `GetTexturesByIds` | 批量获取材质信息 | `GetTexturesByIdsRequest` | `GetTexturesByIdsResponse` |
|
||
|
||
### 收藏夹功能接口
|
||
|
||
| 方法名 | 功能描述 | 请求类型 | 响应类型 |
|
||
|---|---|---|---|
|
||
| `AddFavorite` | 添加材质到收藏夹 | `AddFavoriteRequest` | `AddFavoriteResponse` |
|
||
| `RemoveFavorite` | 从收藏夹移除材质 | `RemoveFavoriteRequest` | `RemoveFavoriteResponse` |
|
||
| `GetUserFavorites` | 获取用户收藏列表 | `GetUserFavoritesRequest` | `GetUserFavoritesResponse` |
|
||
| `CheckFavoriteStatus` | 检查材质收藏状态 | `CheckFavoriteStatusRequest` | `CheckFavoriteStatusResponse` |
|
||
|
||
## 数据模型
|
||
|
||
### 材质信息 (TextureInfo)
|
||
|
||
```protobuf
|
||
message TextureInfo {
|
||
int64 id = 1; // 材质ID
|
||
int64 uploader_id = 2; // 上传者用户ID
|
||
TextureType type = 3; // 材质类型(SKIN/CAPE)
|
||
string url = 4; // MinIO中的永久访问URL
|
||
string hash = 5; // SHA-256哈希值
|
||
bool is_public = 6; // 是否公开到皮肤广场
|
||
string created_at = 7; // 创建时间
|
||
string updated_at = 8; // 更新时间
|
||
}
|
||
```
|
||
|
||
### 材质类型枚举
|
||
|
||
```protobuf
|
||
enum TextureType {
|
||
SKIN = 0; // 皮肤
|
||
CAPE = 1; // 披风
|
||
}
|
||
```
|
||
|
||
### 收藏夹相关数据模型
|
||
|
||
#### 收藏材质信息 (FavoriteTextureInfo)
|
||
|
||
```protobuf
|
||
message FavoriteTextureInfo {
|
||
TextureInfo texture = 1; // 材质信息
|
||
string favorite_at = 2; // 收藏时间
|
||
}
|
||
```
|
||
|
||
## 存储结构
|
||
|
||
### MinIO 对象存储结构
|
||
|
||
```
|
||
textures/ # 存储桶根目录
|
||
├── skins/ # 皮肤材质目录
|
||
│ └── user_{userId}/ # 按用户分组
|
||
│ └── {timestamp}_{filename}.png
|
||
└── capes/ # 披风材质目录
|
||
└── user_{userId}/ # 按用户分组
|
||
└── {timestamp}_{filename}.png
|
||
```
|
||
|
||
### 数据库表结构
|
||
|
||
```sql
|
||
CREATE TABLE textures (
|
||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
uploader_id BIGINT NOT NULL,
|
||
type VARCHAR(10) NOT NULL, -- 'SKIN' 或 'CAPE'
|
||
url VARCHAR(500) NOT NULL, -- MinIO访问URL
|
||
hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256哈希值
|
||
is_public BOOLEAN DEFAULT FALSE, -- 是否公开
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
|
||
INDEX idx_uploader_type (uploader_id, type),
|
||
INDEX idx_hash (hash),
|
||
INDEX idx_public_type (is_public, type, created_at)
|
||
);
|
||
```
|
||
|
||
### `user_texture_favorites` 表
|
||
|
||
```sql
|
||
-- 用户材质收藏表
|
||
CREATE TABLE `user_texture_favorites` (
|
||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '收藏记录的唯一ID',
|
||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID (对应UserService中的users.id)',
|
||
`texture_id` BIGINT UNSIGNED NOT NULL COMMENT '收藏的材质ID (对应textures.id)',
|
||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间',
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `uk_user_texture` (`user_id`, `texture_id`),
|
||
INDEX `idx_user_id` (`user_id`),
|
||
INDEX `idx_texture_id` (`texture_id`),
|
||
INDEX `idx_created_at` (`created_at`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户材质收藏表';
|
||
```
|
||
|
||
## 配置说明
|
||
|
||
### 服务配置 (etc/textures.yaml)
|
||
|
||
```yaml
|
||
Name: textures.rpc
|
||
ListenOn: 0.0.0.0:8080
|
||
|
||
# 数据库配置
|
||
DataSource: root:password@tcp(localhost:3306)/carrot_skin?charset=utf8mb4&parseTime=true&loc=Local
|
||
|
||
# 缓存配置
|
||
CacheRedis:
|
||
- Host: localhost:6379
|
||
Pass: ""
|
||
Type: node
|
||
|
||
# MinIO配置
|
||
MinIO:
|
||
Endpoint: "localhost:9000"
|
||
AccessKeyID: "minioadmin"
|
||
SecretAccessKey: "minioadmin"
|
||
UseSSL: false
|
||
Buckets:
|
||
Textures: "carrot-skin-textures"
|
||
```
|
||
|
||
## 安全特性
|
||
|
||
### 文件上传安全
|
||
|
||
- **格式限制**: 仅允许PNG格式文件
|
||
- **大小限制**: 文件大小限制在1KB-1MB之间
|
||
- **时效控制**: 预签名URL 15分钟过期
|
||
- **内容验证**: MinIO层面的Content-Type验证
|
||
|
||
### 权限控制
|
||
|
||
- **所有权验证**: 用户只能删除/更新自己上传的材质
|
||
- **参数校验**: 所有接口都有完整的输入验证
|
||
- **错误处理**: 不暴露敏感的系统信息
|
||
|
||
### 数据完整性
|
||
|
||
- **哈希去重**: SHA-256确保文件唯一性
|
||
- **事务处理**: 数据库操作的原子性保证
|
||
- **文件同步**: 删除材质时同步清理MinIO文件
|
||
|
||
## 性能优化
|
||
|
||
### 查询优化
|
||
|
||
- **分页查询**: 支持高效的大数据量分页
|
||
- **索引优化**: 针对常用查询场景建立复合索引
|
||
- **缓存机制**: go-zero内置的Redis缓存层
|
||
|
||
### 存储优化
|
||
|
||
- **智能去重**: 相同文件自动复用,节省存储空间
|
||
- **分类存储**: 按材质类型和用户分目录存储
|
||
|
||
## 部署说明
|
||
|
||
### 环境要求
|
||
|
||
- Go 1.19+
|
||
- MySQL 8.0+
|
||
- Redis 6.0+
|
||
- MinIO Server
|
||
|
||
### 启动服务
|
||
|
||
```bash
|
||
# 1. 安装依赖
|
||
go mod tidy
|
||
|
||
# 2. 生成代码(如果修改了proto或sql文件)
|
||
goctl rpc protoc docs/textures.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=.
|
||
goctl model mysql ddl --src="docs/textures.sql" --dir="./internal/model"
|
||
|
||
# 3. 启动服务
|
||
go run textures.go -f etc/textures.yaml
|
||
```
|
||
|
||
### Docker 部署
|
||
|
||
```dockerfile
|
||
FROM golang:1.19-alpine AS builder
|
||
WORKDIR /app
|
||
COPY . .
|
||
RUN go mod tidy && go build -o textures textures.go
|
||
|
||
FROM alpine:latest
|
||
RUN apk --no-cache add ca-certificates
|
||
WORKDIR /root/
|
||
COPY --from=builder /app/textures .
|
||
COPY --from=builder /app/etc ./etc
|
||
CMD ["./textures", "-f", "etc/textures.yaml"]
|
||
```
|
||
|
||
## 监控与日志
|
||
|
||
### 日志记录
|
||
|
||
- **操作日志**: 记录所有材质操作的详细信息
|
||
- **错误日志**: 详细的错误堆栈和上下文信息
|
||
- **性能日志**: 查询耗时和系统性能指标
|
||
|
||
### 监控指标
|
||
|
||
- **接口调用量**: 各接口的QPS统计
|
||
- **存储使用量**: MinIO存储空间使用情况
|
||
- **缓存命中率**: Redis缓存效果监控
|
||
- **错误率统计**: 接口错误率和错误类型分析
|
||
|
||
## 开发指南
|
||
|
||
### 添加新接口
|
||
|
||
1. 在 `docs/textures.proto` 中定义新的RPC方法
|
||
2. 运行 `goctl rpc protoc` 生成代码
|
||
3. 在 `internal/logic/` 中实现业务逻辑
|
||
4. 添加相应的数据库查询方法(如需要)
|
||
|
||
### 自定义数据库查询
|
||
|
||
在 `internal/model/texturesModel.go` 中添加自定义查询方法:
|
||
|
||
```go
|
||
// 在 TexturesModel 接口中添加方法声明
|
||
type TexturesModel interface {
|
||
texturesModel
|
||
// 自定义方法
|
||
FindByCustomCondition(ctx context.Context, condition string) ([]*Textures, error)
|
||
}
|
||
|
||
// 在 customTexturesModel 中实现方法
|
||
func (m *customTexturesModel) FindByCustomCondition(ctx context.Context, condition string) ([]*Textures, error) {
|
||
query := `SELECT * FROM textures WHERE custom_field = ?`
|
||
var resp []*Textures
|
||
err := m.QueryRowsNoCacheCtx(ctx, &resp, query, condition)
|
||
return resp, err
|
||
}
|
||
```
|
||
|
||
## 故障排查
|
||
|
||
### 常见问题
|
||
|
||
1. **上传失败**: 检查MinIO连接和存储桶配置
|
||
2. **查询缓慢**: 检查数据库索引和Redis缓存
|
||
3. **文件不一致**: 检查MinIO文件清理逻辑
|
||
4. **权限错误**: 检查用户ID和材质所有权
|
||
|
||
### 调试技巧
|
||
|
||
- 启用详细日志: 设置日志级别为 `debug`
|
||
- 检查MinIO状态: 使用MinIO控制台查看文件状态
|
||
- 监控数据库: 使用慢查询日志分析性能问题 |