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

269 lines
16 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>
**本文档引用文件**
- [user_service.go](file://internal/service/user_service.go)
- [profile_service.go](file://internal/service/profile_service.go)
- [texture_service.go](file://internal/service/texture_service.go)
- [user_repository.go](file://internal/repository/user_repository.go)
- [profile_repository.go](file://internal/repository/profile_repository.go)
- [texture_repository.go](file://internal/repository/texture_repository.go)
- [user.go](file://internal/model/user.go)
- [profile.go](file://internal/model/profile.go)
- [texture.go](file://internal/model/texture.go)
</cite>
## 目录
1. [简介](#简介)
2. [服务层职责概述](#服务层职责概述)
3. [核心服务详细分析](#核心服务详细分析)
4. [服务调用关系与流程](#服务调用关系与流程)
5. [业务规则实现](#业务规则实现)
6. [错误处理与日志记录](#错误处理与日志记录)
7. [结论](#结论)
## 简介
CarrotSkin项目的服务层是业务逻辑的核心负责协调数据访问、执行业务规则并为上层处理器提供接口。本服务架构文档重点介绍`UserService``ProfileService``TextureService`三大核心服务详细说明其职责、方法、内部逻辑以及与Repository层的协作方式。文档旨在为初学者提供清晰的服务调用视图并为经验丰富的开发者提供代码扩展和维护的深入指导。
## 服务层职责概述
服务层(位于`internal/service/`目录是CarrotSkin应用的业务逻辑中枢其主要职责包括
- **业务逻辑封装**:将复杂的业务规则(如用户注册、档案创建、材质上传)封装在独立的服务方法中。
- **数据访问协调**调用Repository层的方法来持久化或检索数据实现与数据库的解耦。
- **事务管理**对于涉及多个数据操作的业务如设置活跃档案通过Repository层的事务功能保证数据一致性。
- **数据验证与转换**:在数据进入或离开系统时进行验证和必要的格式转换。
- **外部服务集成**与认证JWT、存储MinIO、邮件等外部服务进行交互。
服务层通过定义清晰的函数接口,为`handler`层提供稳定、可复用的业务能力,是连接用户请求与数据持久化的关键桥梁。
## 核心服务详细分析
### UserService 分析
`UserService`负责管理用户生命周期的核心操作,包括注册、登录、信息更新和密码管理。
**核心方法与职责**
- `RegisterUser`:处理用户注册流程。该方法首先检查用户名和邮箱的唯一性,然后使用`pkg/auth`包对密码进行加密创建用户记录并生成JWT Token。它还实现了头像URL的逻辑优先使用用户提供的URL否则从系统配置中获取默认头像。
- `LoginUser`:处理用户登录。支持通过用户名或邮箱登录。方法会根据输入是否包含`@`符号来判断登录方式。成功登录后,会更新用户的最后登录时间,并记录登录日志(成功或失败)。
- `ChangeUserPassword``ResetUserPassword`:分别处理用户主动修改密码和通过邮箱重置密码的场景。两者都包含密码验证和加密更新的逻辑。
- `UpdateUserInfo``UpdateUserAvatar`:用于更新用户的基本信息和头像。
该服务通过调用`repository.FindUserByUsername``repository.FindUserByEmail`等方法与Repository层交互确保了数据访问的抽象化。
```mermaid
classDiagram
class UserService {
+RegisterUser(jwtService *auth.JWTService, username, password, email, avatar string) (*model.User, string, error)
+LoginUser(jwtService *auth.JWTService, usernameOrEmail, password, ipAddress, userAgent string) (*model.User, string, error)
+GetUserByID(id int64) (*model.User, error)
+UpdateUserInfo(user *model.User) error
+UpdateUserAvatar(userID int64, avatarURL string) error
+ChangeUserPassword(userID int64, oldPassword, newPassword string) error
+ResetUserPassword(email, newPassword string) error
+ChangeUserEmail(userID int64, newEmail string) error
}
class User {
+ID int64
+Username string
+Email string
+Avatar string
+Points int
+Role string
+Status int16
}
class Repository {
+FindUserByUsername(username string) (*model.User, error)
+FindUserByEmail(email string) (*model.User, error)
+CreateUser(user *model.User) error
+UpdateUserFields(id int64, fields map[string]interface{}) error
}
class Auth {
+HashPassword(password string) (string, error)
+CheckPassword(hashedPassword, password string) bool
+GenerateToken(userID int64, username, role string) (string, error)
}
UserService --> User : "操作"
UserService --> Repository : "调用"
UserService --> Auth : "依赖"
```
**图示来源**
- [user_service.go](file://internal/service/user_service.go#L12-L249)
- [user_repository.go](file://internal/repository/user_repository.go#L1-L137)
- [user.go](file://internal/model/user.go#L1-L71)
**本节来源**
- [user_service.go](file://internal/service/user_service.go#L12-L249)
- [user_repository.go](file://internal/repository/user_repository.go#L1-L137)
### ProfileService 分析
`ProfileService`负责管理用户的Minecraft档案Profile包括创建、查询、更新、删除和状态管理。
**核心方法与职责**
- `CreateProfile`创建新的Minecraft档案。此方法首先验证用户存在且状态正常检查角色名的唯一性然后生成一个UUID作为档案ID并调用`generateRSAPrivateKey`生成RSA-2048私钥PEM格式。创建档案后会调用`SetActiveProfile`确保该档案成为用户的活跃档案。
- `GetUserProfiles``GetProfileByUUID`:分别用于获取用户的所有档案列表和单个档案的详细信息。
- `UpdateProfile`:更新档案信息,如角色名、皮肤和披风。更新角色名时会检查是否重复。更新后会重新加载数据以返回最新状态。
- `DeleteProfile`:删除指定的档案。
- `SetActiveProfile`:将指定档案设置为活跃状态。这是一个关键的业务方法,它在一个数据库事务中执行:首先将用户的所有档案设置为非活跃,然后将指定的档案设置为活跃,从而保证了同一时间只有一个活跃档案。
- `CheckProfileLimit`:检查用户当前的档案数量是否达到了系统设定的上限,用于在创建新档案前进行限制。
该服务通过`repository.FindProfileByUUID``repository.CreateProfile`等方法与Repository层交互并利用GORM的事务功能来保证`SetActiveProfile`操作的原子性。
```mermaid
classDiagram
class ProfileService {
+CreateProfile(db *gorm.DB, userID int64, name string) (*model.Profile, error)
+GetProfileByUUID(db *gorm.DB, uuid string) (*model.Profile, error)
+GetUserProfiles(db *gorm.DB, userID int64) ([]*model.Profile, error)
+UpdateProfile(db *gorm.DB, uuid string, userID int64, name *string, skinID, capeID *int64) (*model.Profile, error)
+DeleteProfile(db *gorm.DB, uuid string, userID int64) error
+SetActiveProfile(db *gorm.DB, uuid string, userID int64) error
+CheckProfileLimit(db *gorm.DB, userID int64, maxProfiles int) error
}
class Profile {
+UUID string
+UserID int64
+Name string
+SkinID *int64
+CapeID *int64
+RSAPrivateKey string
+IsActive bool
}
class Repository {
+FindProfileByUUID(uuid string) (*model.Profile, error)
+CreateProfile(profile *model.Profile) error
+FindProfilesByUserID(userID int64) ([]*model.Profile, error)
+CountProfilesByUserID(userID int64) (int64, error)
+SetActiveProfile(uuid string, userID int64) error
+DeleteProfile(uuid string) error
}
ProfileService --> Profile : "操作"
ProfileService --> Repository : "调用"
```
**图示来源**
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
- [profile.go](file://internal/model/profile.go#L1-L64)
**本节来源**
- [profile_service.go](file://internal/service/profile_service.go#L1-L253)
- [profile_repository.go](file://internal/repository/profile_repository.go#L1-L200)
### TextureService 分析
`TextureService`负责管理用户上传的皮肤Skin和披风Cape材质。
**核心方法与职责**
- `CreateTexture`创建新的材质记录。方法会验证上传者用户的存在性并检查材质的Hash值是否已存在防止重复上传。它通过`switch`语句将字符串类型的`textureType`转换为`model.TextureType`枚举值。创建成功后,材质记录会被持久化。
- `SearchTextures``GetUserTextures`:提供按关键词、类型、公开性等条件搜索材质,以及获取特定用户上传的材质列表的功能。
- `UpdateTexture`:允许上传者更新材质的名称、描述和公开状态。更新时会检查权限,确保只有上传者才能修改。
- `DeleteTexture`:允许上传者删除自己的材质。
- `RecordTextureDownload`:记录一次材质下载行为。该方法会增加材质的下载计数,并创建一条下载日志。
- `ToggleTextureFavorite`:切换用户对某个材质的收藏状态。如果已收藏则取消收藏并减少收藏计数,反之则添加收藏并增加收藏计数。
- `CheckTextureUploadLimit`:检查用户上传的材质数量是否达到了系统设定的上限。
该服务通过`repository.FindTextureByHash``repository.CreateTexture`等方法与Repository层交互并通过`IncrementTextureDownloadCount``DecrementTextureFavoriteCount`等原子操作来安全地更新计数。
```mermaid
classDiagram
class TextureService {
+CreateTexture(db *gorm.DB, uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error)
+GetTextureByID(db *gorm.DB, id int64) (*model.Texture, error)
+GetUserTextures(db *gorm.DB, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error)
+SearchTextures(db *gorm.DB, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error)
+UpdateTexture(db *gorm.DB, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error)
+DeleteTexture(db *gorm.DB, textureID, uploaderID int64) error
+RecordTextureDownload(db *gorm.DB, textureID int64, userID *int64, ipAddress, userAgent string) error
+ToggleTextureFavorite(db *gorm.DB, userID, textureID int64) (bool, error)
+GetUserTextureFavorites(db *gorm.DB, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
+CheckTextureUploadLimit(db *gorm.DB, uploaderID int64, maxTextures int) error
}
class Texture {
+ID int64
+UploaderID int64
+Name string
+Type TextureType
+URL string
+Hash string
+DownloadCount int
+FavoriteCount int
}
class Repository {
+FindTextureByHash(hash string) (*model.Texture, error)
+CreateTexture(texture *model.Texture) error
+FindTexturesByUploaderID(uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error)
+SearchTextures(keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error)
+UpdateTextureFields(id int64, fields map[string]interface{}) error
+DeleteTexture(id int64) error
+IncrementTextureDownloadCount(id int64) error
+IncrementTextureFavoriteCount(id int64) error
+DecrementTextureFavoriteCount(id int64) error
+IsTextureFavorited(userID, textureID int64) (bool, error)
}
TextureService --> Texture : "操作"
TextureService --> Repository : "调用"
```
**图示来源**
- [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)
**本节来源**
- [texture_service.go](file://internal/service/texture_service.go#L1-L252)
- [texture_repository.go](file://internal/repository/texture_repository.go#L1-L232)
## 服务调用关系与流程
服务层的调用流程通常始于`handler`层的HTTP请求处理器。例如当用户发起注册请求时`auth_handler.go`中的处理器会调用`UserService.RegisterUser`方法。该方法内部会协调多个Repository操作检查用户、创建用户并依赖`pkg/auth`包进行密码加密和Token生成。
服务之间通常是独立的,但它们都依赖于`Repository`层来访问数据。例如,`ProfileService.CreateProfile`在创建档案时,会先调用`repository.FindUserByID`来验证用户,这与`UserService`使用的Repository是同一个体现了数据访问层的共享性。
一个典型的跨服务流程是用户上传材质并将其设置为档案皮肤:
1. `TextureService.CreateTexture` 被调用,创建材质记录。
2. `ProfileService.UpdateProfile` 被调用传入新创建的材质ID作为`skinID`参数,更新档案的皮肤引用。
这种设计保证了服务的单一职责同时通过Repository层实现了数据的统一管理。
## 业务规则实现
服务层是复杂业务规则的主要实现者。
**档案数量限制**
`ProfileService.CheckProfileLimit`方法通过调用`repository.CountProfilesByUserID`获取用户当前的档案数量,并与传入的`maxProfiles`上限进行比较。这个检查通常在`CreateProfile`方法的开头执行,以防止用户创建过多的档案。
**材质上传流程**
`TextureService.CreateTexture`方法实现了严格的上传流程:
1. **用户验证**:确保上传者是有效的用户。
2. **去重检查**:通过`repository.FindTextureByHash`检查材质的Hash值避免存储重复内容。
3. **类型验证**:使用`switch`语句确保`textureType`是有效的("SKIN"或"CAPE")。
4. **数据创建**:构建`model.Texture`对象并调用`repository.CreateTexture`进行持久化。
**活跃档案管理**
`ProfileService.SetActiveProfile`是实现“一个用户只能有一个活跃档案”这一业务规则的核心。它利用GORM的事务功能在一个原子操作中完成两步
1. 将用户所有档案的`is_active`字段更新为`false`
2. 将指定档案的`is_active`字段更新为`true`
这确保了即使在高并发场景下,也不会出现多个档案同时为活跃状态的情况。
## 错误处理与日志记录
服务层实现了细粒度的错误处理。每个方法都会返回一个`error`类型的值,用于向调用者传递错误信息。错误通常使用`fmt.Errorf`进行包装,以提供上下文信息,例如`fmt.Errorf("查询用户失败: %w", err)`
此外,服务层还直接负责记录关键操作日志:
- `UserService`中的`logSuccessLogin``logFailedLogin`方法会创建`UserLoginLog`记录,用于审计登录行为。
- `TextureService`中的`RecordTextureDownload`方法会创建`TextureDownloadLog`记录,用于追踪下载量。
这些日志记录通过调用相应的Repository方法`repository.CreateLoginLog`)来持久化,为系统监控和问题排查提供了重要依据。
**本节来源**
- [user_service.go](file://internal/service/user_service.go#L203-L226)
- [texture_service.go](file://internal/service/texture_service.go#L162-L187)
- [user_repository.go](file://internal/repository/user_repository.go#L77-L81)
- [texture_repository.go](file://internal/repository/texture_repository.go#L153-L157)
## 结论
CarrotSkin项目的服务层设计清晰、职责分明有效地封装了核心业务逻辑。`UserService``ProfileService``TextureService`三个核心服务各司其职通过调用统一的Repository层来访问数据实现了业务逻辑与数据持久化的解耦。服务层成功实现了档案数量限制、材质上传去重、活跃档案管理等复杂业务规则并通过事务和原子操作保证了数据的一致性。其良好的错误处理和日志记录机制为系统的稳定运行和维护提供了保障。该架构为未来的功能扩展如积分系统、审核流程奠定了坚实的基础。