Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52b61be822 | |||
| 17a2792ac4 | |||
| 68d7318285 | |||
| 432b875ba4 | |||
| 116612ffec |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -60,7 +60,7 @@ configs/config.yaml
|
|||||||
.env.production
|
.env.production
|
||||||
|
|
||||||
# Keep example files
|
# Keep example files
|
||||||
!.env.example
|
!.env
|
||||||
|
|
||||||
# Database files
|
# Database files
|
||||||
*.db
|
*.db
|
||||||
|
|||||||
136
README.md
136
README.md
@@ -72,7 +72,7 @@ backend/
|
|||||||
|
|
||||||
3. **配置环境变量**
|
3. **配置环境变量**
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env .env
|
||||||
# 根据实际环境填写数据库、Redis、对象存储、邮件等信息
|
# 根据实际环境填写数据库、Redis、对象存储、邮件等信息
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -144,140 +144,6 @@ golangci-lint run (若已安装)
|
|||||||
- 若手动运行,需要保证 `docs/` 下的 `docs.go`、`swagger.json`、`swagger.yaml` 与代码同步
|
- 若手动运行,需要保证 `docs/` 下的 `docs.go`、`swagger.json`、`swagger.yaml` 与代码同步
|
||||||
- 通过 `SERVER_SWAGGER_ENABLED=false` 可在生产环境关闭 Swagger UI 暴露
|
- 通过 `SERVER_SWAGGER_ENABLED=false` 可在生产环境关闭 Swagger UI 暴露
|
||||||
|
|
||||||
## 🔐 管理后台 API
|
|
||||||
|
|
||||||
管理后台接口均需要管理员权限(`role=admin`),所有接口路径前缀为 `/api/v1/admin`。
|
|
||||||
|
|
||||||
### 📊 统计信息
|
|
||||||
|
|
||||||
| 接口 | 方法 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `/stats` | GET | 获取系统统计数据(用户数、材质数、下载量等) |
|
|
||||||
|
|
||||||
**响应示例:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "success",
|
|
||||||
"data": {
|
|
||||||
"total_users": 100,
|
|
||||||
"active_users": 80,
|
|
||||||
"banned_users": 5,
|
|
||||||
"admin_users": 3,
|
|
||||||
"total_textures": 500,
|
|
||||||
"public_textures": 300,
|
|
||||||
"pending_textures": 10,
|
|
||||||
"total_downloads": 1000,
|
|
||||||
"total_favorites": 500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 👥 角色管理
|
|
||||||
|
|
||||||
| 接口 | 方法 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `/roles` | GET | 获取所有可用角色列表 |
|
|
||||||
|
|
||||||
**响应示例:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "success",
|
|
||||||
"data": {
|
|
||||||
"roles": [
|
|
||||||
{
|
|
||||||
"name": "user",
|
|
||||||
"display_name": "普通用户",
|
|
||||||
"description": "拥有基本用户权限"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "admin",
|
|
||||||
"display_name": "管理员",
|
|
||||||
"description": "拥有所有管理权限"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 👤 用户管理
|
|
||||||
|
|
||||||
| 接口 | 方法 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `/users` | GET | 获取用户列表(分页) |
|
|
||||||
| `/users/search` | GET | 搜索用户(支持关键词、角色、状态筛选、排序) |
|
|
||||||
| `/users/{id}` | GET | 获取用户详情 |
|
|
||||||
| `/users/{id}` | DELETE | 删除用户(软删除) |
|
|
||||||
| `/users/role` | PUT | 设置单个用户角色 |
|
|
||||||
| `/users/status` | PUT | 设置单个用户状态(封禁/解封) |
|
|
||||||
| `/users/batch-role` | PUT | 批量设置用户角色 |
|
|
||||||
| `/users/batch-delete` | DELETE | 批量删除用户 |
|
|
||||||
|
|
||||||
**搜索用户请求参数:**
|
|
||||||
- `keyword` (string): 搜索关键词(用户名或邮箱)
|
|
||||||
- `role` (string): 角色筛选
|
|
||||||
- `status` (int): 状态筛选(1=正常,0=禁用,-1=删除)
|
|
||||||
- `sort_by` (string): 排序字段
|
|
||||||
- `sort_desc` (bool): 是否降序
|
|
||||||
- `page` (int): 页码
|
|
||||||
- `page_size` (int): 每页数量
|
|
||||||
|
|
||||||
**设置用户状态请求示例:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user_id": 123,
|
|
||||||
"status": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- `status`: 1=正常,0=禁用,-1=删除
|
|
||||||
|
|
||||||
**批量设置角色请求示例:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user_ids": [1, 2, 3],
|
|
||||||
"role": "admin"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🎨 材质管理
|
|
||||||
|
|
||||||
| 接口 | 方法 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `/textures` | GET | 获取材质列表(分页) |
|
|
||||||
| `/textures/search` | GET | 搜索材质(支持关键词、类型、状态、上传者筛选、排序) |
|
|
||||||
| `/textures/{id}` | PUT | 更新材质信息(名称、描述、公开状态、审核状态) |
|
|
||||||
| `/textures/{id}` | DELETE | 删除材质 |
|
|
||||||
| `/textures/batch-delete` | DELETE | 批量删除材质 |
|
|
||||||
|
|
||||||
**搜索材质请求参数:**
|
|
||||||
- `keyword` (string): 搜索关键词
|
|
||||||
- `type` (string): 材质类型(SKIN/CAPE)
|
|
||||||
- `status` (int): 状态筛选
|
|
||||||
- `uploader_id` (int): 上传者ID筛选
|
|
||||||
- `sort_by` (string): 排序字段
|
|
||||||
- `sort_desc` (bool): 是否降序
|
|
||||||
- `page` (int): 页码
|
|
||||||
- `page_size` (int): 每页数量
|
|
||||||
|
|
||||||
**更新材质请求示例:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "新皮肤名称",
|
|
||||||
"description": "新描述",
|
|
||||||
"is_public": true,
|
|
||||||
"status": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔒 安全特性
|
|
||||||
|
|
||||||
1. **权限保护**:所有管理接口都需要管理员权限(`role=admin`)
|
|
||||||
2. **安全限制**:管理员不能修改/删除自己的角色和状态
|
|
||||||
3. **批量操作**:支持批量设置角色和批量删除
|
|
||||||
4. **操作日志**:所有管理操作都会记录日志
|
|
||||||
5. **封禁功能**:通过 `/users/status` 接口可以封禁/解封用户
|
|
||||||
|
|
||||||
## 🤝 贡献指南
|
## 🤝 贡献指南
|
||||||
|
|
||||||
1. Fork & Clone
|
1. Fork & Clone
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
_ "time/tzdata"
|
||||||
|
|
||||||
"carrotskin/internal/container"
|
"carrotskin/internal/container"
|
||||||
"carrotskin/internal/handler"
|
"carrotskin/internal/handler"
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -112,7 +112,7 @@ require (
|
|||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/swaggo/swag v1.16.6 // indirect
|
github.com/swaggo/swag v1.16.6
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type Container struct {
|
|||||||
TextureRepo repository.TextureRepository
|
TextureRepo repository.TextureRepository
|
||||||
ClientRepo repository.ClientRepository
|
ClientRepo repository.ClientRepository
|
||||||
YggdrasilRepo repository.YggdrasilRepository
|
YggdrasilRepo repository.YggdrasilRepository
|
||||||
|
ReportRepo repository.ReportRepository
|
||||||
|
|
||||||
// Service层
|
// Service层
|
||||||
UserService service.UserService
|
UserService service.UserService
|
||||||
@@ -43,6 +44,7 @@ type Container struct {
|
|||||||
SecurityService service.SecurityService
|
SecurityService service.SecurityService
|
||||||
CaptchaService service.CaptchaService
|
CaptchaService service.CaptchaService
|
||||||
SignatureService *service.SignatureService
|
SignatureService *service.SignatureService
|
||||||
|
ReportService service.ReportService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContainer 创建依赖容器
|
// NewContainer 创建依赖容器
|
||||||
@@ -86,6 +88,7 @@ func NewContainer(
|
|||||||
c.TextureRepo = repository.NewTextureRepository(db)
|
c.TextureRepo = repository.NewTextureRepository(db)
|
||||||
c.ClientRepo = repository.NewClientRepository(db)
|
c.ClientRepo = repository.NewClientRepository(db)
|
||||||
c.YggdrasilRepo = repository.NewYggdrasilRepository(db)
|
c.YggdrasilRepo = repository.NewYggdrasilRepository(db)
|
||||||
|
c.ReportRepo = repository.NewReportRepository(db)
|
||||||
|
|
||||||
// 初始化SignatureService(作为依赖注入,避免在容器中创建并立即调用)
|
// 初始化SignatureService(作为依赖注入,避免在容器中创建并立即调用)
|
||||||
// 将SignatureService添加到容器中,供其他服务使用
|
// 将SignatureService添加到容器中,供其他服务使用
|
||||||
@@ -95,6 +98,7 @@ func NewContainer(
|
|||||||
c.UserService = service.NewUserService(c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger)
|
c.UserService = service.NewUserService(c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger)
|
||||||
c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, cacheManager, logger)
|
c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, cacheManager, logger)
|
||||||
c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger)
|
c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger)
|
||||||
|
c.ReportService = service.NewReportService(c.ReportRepo, c.UserRepo, logger)
|
||||||
|
|
||||||
// 获取Yggdrasil私钥并创建JWT服务(TokenService需要)
|
// 获取Yggdrasil私钥并创建JWT服务(TokenService需要)
|
||||||
// 注意:这里仍然需要预先初始化,因为TokenService在创建时需要YggdrasilJWT
|
// 注意:这里仍然需要预先初始化,因为TokenService在创建时需要YggdrasilJWT
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"carrotskin/internal/container"
|
"carrotskin/internal/container"
|
||||||
"carrotskin/internal/model"
|
"carrotskin/internal/model"
|
||||||
"carrotskin/internal/types"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -366,527 +365,18 @@ func (h *AdminHandler) GetTextureList(c *gin.Context) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchUsers 搜索用户
|
// GetPermissions 获取权限列表
|
||||||
// @Summary 搜索用户
|
// @Summary 获取权限列表
|
||||||
// @Description 管理员根据条件搜索用户
|
// @Description 管理员获取所有Casbin权限规则
|
||||||
// @Tags Admin
|
// @Tags Admin
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param keyword query string false "搜索关键词(用户名或邮箱)"
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||||
// @Param role query string false "角色筛选"
|
|
||||||
// @Param status query int false "状态筛选"
|
|
||||||
// @Param sort_by query string false "排序字段"
|
|
||||||
// @Param sort_desc query bool false "是否降序"
|
|
||||||
// @Param page query int false "页码"
|
|
||||||
// @Param page_size query int false "每页数量"
|
|
||||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "搜索成功"
|
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Router /api/v1/admin/users/search [get]
|
// @Router /api/v1/admin/permissions [get]
|
||||||
func (h *AdminHandler) SearchUsers(c *gin.Context) {
|
func (h *AdminHandler) GetPermissions(c *gin.Context) {
|
||||||
var req types.AdminUserSearchRequest
|
// 获取所有权限规则
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
policies, _ := h.container.Casbin.GetEnforcer().GetPolicy()
|
||||||
RespondBadRequest(c, "参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Page < 1 {
|
|
||||||
req.Page = 1
|
|
||||||
}
|
|
||||||
if req.PageSize < 1 || req.PageSize > 100 {
|
|
||||||
req.PageSize = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
db := h.container.DB.Model(&model.User{})
|
|
||||||
|
|
||||||
// 关键词搜索
|
|
||||||
if req.Keyword != "" {
|
|
||||||
db = db.Where("username LIKE ? OR email LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 角色筛选
|
|
||||||
if req.Role != "" {
|
|
||||||
db = db.Where("role = ?", req.Role)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态筛选
|
|
||||||
if req.Status != nil {
|
|
||||||
db = db.Where("status = ?", *req.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序
|
|
||||||
sortBy := "id"
|
|
||||||
if req.SortBy != "" {
|
|
||||||
sortBy = req.SortBy
|
|
||||||
}
|
|
||||||
order := sortBy
|
|
||||||
if req.SortDesc {
|
|
||||||
order += " DESC"
|
|
||||||
}
|
|
||||||
db = db.Order(order)
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
var total int64
|
|
||||||
db.Count(&total)
|
|
||||||
|
|
||||||
var users []model.User
|
|
||||||
db.Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&users)
|
|
||||||
|
|
||||||
// 构建响应
|
|
||||||
userList := make([]gin.H, len(users))
|
|
||||||
for i, u := range users {
|
|
||||||
userList[i] = gin.H{
|
|
||||||
"id": u.ID,
|
|
||||||
"username": u.Username,
|
|
||||||
"email": u.Email,
|
|
||||||
"avatar": u.Avatar,
|
|
||||||
"role": u.Role,
|
|
||||||
"status": u.Status,
|
|
||||||
"points": u.Points,
|
|
||||||
"last_login_at": u.LastLoginAt,
|
|
||||||
"created_at": u.CreatedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
||||||
"users": userList,
|
"policies": policies,
|
||||||
"total": total,
|
|
||||||
"page": req.Page,
|
|
||||||
"page_size": req.PageSize,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser 删除用户
|
|
||||||
// @Summary 删除用户
|
|
||||||
// @Description 管理员删除用户(软删除)
|
|
||||||
// @Tags Admin
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path int true "用户ID"
|
|
||||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功"
|
|
||||||
// @Failure 400 {object} model.ErrorResponse "不能删除自己"
|
|
||||||
// @Failure 404 {object} model.ErrorResponse "用户不存在"
|
|
||||||
// @Security BearerAuth
|
|
||||||
// @Router /api/v1/admin/users/{id} [delete]
|
|
||||||
func (h *AdminHandler) DeleteUser(c *gin.Context) {
|
|
||||||
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
RespondBadRequest(c, "无效的用户ID", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
operatorID, _ := c.Get("user_id")
|
|
||||||
|
|
||||||
// 不能删除自己
|
|
||||||
if userID == operatorID.(int64) {
|
|
||||||
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
|
|
||||||
model.CodeBadRequest,
|
|
||||||
"不能删除自己",
|
|
||||||
nil,
|
|
||||||
))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查用户是否存在
|
|
||||||
user, err := h.container.UserRepo.FindByID(c.Request.Context(), userID)
|
|
||||||
if err != nil || user == nil {
|
|
||||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
|
||||||
model.CodeNotFound,
|
|
||||||
"用户不存在",
|
|
||||||
nil,
|
|
||||||
))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 软删除
|
|
||||||
if err := h.container.DB.Delete(user).Error; err != nil {
|
|
||||||
RespondServerError(c, "删除用户失败", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.container.Logger.Info("管理员删除用户",
|
|
||||||
zap.Int64("operator_id", operatorID.(int64)),
|
|
||||||
zap.Int64("deleted_user_id", userID),
|
|
||||||
zap.String("username", user.Username),
|
|
||||||
)
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
|
||||||
"message": "用户删除成功",
|
|
||||||
"user_id": userID,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchSetUserRole 批量设置用户角色
|
|
||||||
// @Summary 批量设置用户角色
|
|
||||||
// @Description 管理员批量设置多个用户的角色
|
|
||||||
// @Tags Admin
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body map[string]interface{} true "批量设置请求"
|
|
||||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "设置成功"
|
|
||||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
|
||||||
// @Security BearerAuth
|
|
||||||
// @Router /api/v1/admin/users/batch-role [put]
|
|
||||||
func (h *AdminHandler) BatchSetUserRole(c *gin.Context) {
|
|
||||||
var req struct {
|
|
||||||
UserIDs []int64 `json:"user_ids" binding:"required,min=1"`
|
|
||||||
Role string `json:"role" binding:"required,oneof=user admin"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
RespondBadRequest(c, "参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
operatorID, _ := c.Get("user_id")
|
|
||||||
|
|
||||||
// 检查是否包含自己
|
|
||||||
for _, uid := range req.UserIDs {
|
|
||||||
if uid == operatorID.(int64) {
|
|
||||||
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
|
|
||||||
model.CodeBadRequest,
|
|
||||||
"不能修改自己的角色",
|
|
||||||
nil,
|
|
||||||
))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量更新
|
|
||||||
if err := h.container.DB.Model(&model.User{}).
|
|
||||||
Where("id IN ?", req.UserIDs).
|
|
||||||
Update("role", req.Role).Error; err != nil {
|
|
||||||
RespondServerError(c, "批量更新角色失败", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.container.Logger.Info("管理员批量设置用户角色",
|
|
||||||
zap.Int64("operator_id", operatorID.(int64)),
|
|
||||||
zap.Int("count", len(req.UserIDs)),
|
|
||||||
zap.String("role", req.Role),
|
|
||||||
)
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
|
||||||
"message": "批量设置角色成功",
|
|
||||||
"count": len(req.UserIDs),
|
|
||||||
"role": req.Role,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchDeleteUsers 批量删除用户
|
|
||||||
// @Summary 批量删除用户
|
|
||||||
// @Description 管理员批量删除多个用户
|
|
||||||
// @Tags Admin
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body map[string]interface{} true "批量删除请求"
|
|
||||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功"
|
|
||||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
|
||||||
// @Security BearerAuth
|
|
||||||
// @Router /api/v1/admin/users/batch-delete [delete]
|
|
||||||
func (h *AdminHandler) BatchDeleteUsers(c *gin.Context) {
|
|
||||||
var req struct {
|
|
||||||
UserIDs []int64 `json:"user_ids" binding:"required,min=1"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
RespondBadRequest(c, "参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
operatorID, _ := c.Get("user_id")
|
|
||||||
|
|
||||||
// 检查是否包含自己
|
|
||||||
for _, uid := range req.UserIDs {
|
|
||||||
if uid == operatorID.(int64) {
|
|
||||||
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
|
|
||||||
model.CodeBadRequest,
|
|
||||||
"不能删除自己",
|
|
||||||
nil,
|
|
||||||
))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量软删除
|
|
||||||
if err := h.container.DB.Where("id IN ?", req.UserIDs).Delete(&model.User{}).Error; err != nil {
|
|
||||||
RespondServerError(c, "批量删除用户失败", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.container.Logger.Info("管理员批量删除用户",
|
|
||||||
zap.Int64("operator_id", operatorID.(int64)),
|
|
||||||
zap.Int("count", len(req.UserIDs)),
|
|
||||||
)
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
|
||||||
"message": "批量删除用户成功",
|
|
||||||
"count": len(req.UserIDs),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRoles 获取角色列表
|
|
||||||
// @Summary 获取角色列表
|
|
||||||
// @Description 管理员获取所有可用角色
|
|
||||||
// @Tags Admin
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} model.Response{data=types.AdminRoleListResponse} "获取成功"
|
|
||||||
// @Security BearerAuth
|
|
||||||
// @Router /api/v1/admin/roles [get]
|
|
||||||
func (h *AdminHandler) GetRoles(c *gin.Context) {
|
|
||||||
roles := []types.RoleInfo{
|
|
||||||
{
|
|
||||||
Name: "user",
|
|
||||||
DisplayName: "普通用户",
|
|
||||||
Description: "拥有基本用户权限",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "admin",
|
|
||||||
DisplayName: "管理员",
|
|
||||||
Description: "拥有所有管理权限",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewSuccessResponse(types.AdminRoleListResponse{
|
|
||||||
Roles: roles,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchTextures 搜索材质
|
|
||||||
// @Summary 搜索材质
|
|
||||||
// @Description 管理员根据条件搜索材质
|
|
||||||
// @Tags Admin
|
|
||||||
// @Produce json
|
|
||||||
// @Param keyword query string false "搜索关键词"
|
|
||||||
// @Param type query string false "材质类型"
|
|
||||||
// @Param status query int false "状态筛选"
|
|
||||||
// @Param uploader_id query int false "上传者ID"
|
|
||||||
// @Param sort_by query string false "排序字段"
|
|
||||||
// @Param sort_desc query bool false "是否降序"
|
|
||||||
// @Param page query int false "页码"
|
|
||||||
// @Param page_size query int false "每页数量"
|
|
||||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "搜索成功"
|
|
||||||
// @Security BearerAuth
|
|
||||||
// @Router /api/v1/admin/textures/search [get]
|
|
||||||
func (h *AdminHandler) SearchTextures(c *gin.Context) {
|
|
||||||
var req types.AdminTextureSearchRequest
|
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
|
||||||
RespondBadRequest(c, "参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Page < 1 {
|
|
||||||
req.Page = 1
|
|
||||||
}
|
|
||||||
if req.PageSize < 1 || req.PageSize > 100 {
|
|
||||||
req.PageSize = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
db := h.container.DB.Model(&model.Texture{})
|
|
||||||
|
|
||||||
// 关键词搜索
|
|
||||||
if req.Keyword != "" {
|
|
||||||
db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 类型筛选
|
|
||||||
if req.Type != "" {
|
|
||||||
db = db.Where("type = ?", req.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态筛选
|
|
||||||
if req.Status != nil {
|
|
||||||
db = db.Where("status = ?", *req.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传者筛选
|
|
||||||
if req.UploaderID != nil {
|
|
||||||
db = db.Where("uploader_id = ?", *req.UploaderID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序
|
|
||||||
sortBy := "id"
|
|
||||||
if req.SortBy != "" {
|
|
||||||
sortBy = req.SortBy
|
|
||||||
}
|
|
||||||
order := sortBy
|
|
||||||
if req.SortDesc {
|
|
||||||
order += " DESC"
|
|
||||||
}
|
|
||||||
db = db.Order(order)
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
var total int64
|
|
||||||
db.Count(&total)
|
|
||||||
|
|
||||||
var textures []model.Texture
|
|
||||||
db.Preload("Uploader").Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&textures)
|
|
||||||
|
|
||||||
// 构建响应
|
|
||||||
textureList := make([]gin.H, len(textures))
|
|
||||||
for i, t := range textures {
|
|
||||||
uploaderName := ""
|
|
||||||
if t.Uploader != nil {
|
|
||||||
uploaderName = t.Uploader.Username
|
|
||||||
}
|
|
||||||
textureList[i] = gin.H{
|
|
||||||
"id": t.ID,
|
|
||||||
"name": t.Name,
|
|
||||||
"type": t.Type,
|
|
||||||
"hash": t.Hash,
|
|
||||||
"uploader_id": t.UploaderID,
|
|
||||||
"uploader_name": uploaderName,
|
|
||||||
"is_public": t.IsPublic,
|
|
||||||
"download_count": t.DownloadCount,
|
|
||||||
"favorite_count": t.FavoriteCount,
|
|
||||||
"status": t.Status,
|
|
||||||
"created_at": t.CreatedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
|
||||||
"textures": textureList,
|
|
||||||
"total": total,
|
|
||||||
"page": req.Page,
|
|
||||||
"page_size": req.PageSize,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateTexture 更新材质
|
|
||||||
// @Summary 更新材质
|
|
||||||
// @Description 管理员更新材质信息
|
|
||||||
// @Tags Admin
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path int true "材质ID"
|
|
||||||
// @Param request body types.AdminTextureUpdateRequest true "更新材质请求"
|
|
||||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "更新成功"
|
|
||||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
|
||||||
// @Failure 404 {object} model.ErrorResponse "材质不存在"
|
|
||||||
// @Security BearerAuth
|
|
||||||
// @Router /api/v1/admin/textures/{id} [put]
|
|
||||||
func (h *AdminHandler) UpdateTexture(c *gin.Context) {
|
|
||||||
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
RespondBadRequest(c, "无效的材质ID", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req types.AdminTextureUpdateRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
RespondBadRequest(c, "参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查材质是否存在
|
|
||||||
var texture model.Texture
|
|
||||||
if err := h.container.DB.First(&texture, textureID).Error; err != nil {
|
|
||||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
|
||||||
model.CodeNotFound,
|
|
||||||
"材质不存在",
|
|
||||||
nil,
|
|
||||||
))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建更新字段
|
|
||||||
updates := make(map[string]interface{})
|
|
||||||
|
|
||||||
if req.Name != nil {
|
|
||||||
updates["name"] = *req.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Description != nil {
|
|
||||||
updates["description"] = *req.Description
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.IsPublic != nil {
|
|
||||||
updates["is_public"] = *req.IsPublic
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Status != nil {
|
|
||||||
updates["status"] = *req.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行更新
|
|
||||||
if len(updates) > 0 {
|
|
||||||
if err := h.container.DB.Model(&texture).Updates(updates).Error; err != nil {
|
|
||||||
RespondServerError(c, "更新材质失败", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
operatorID, _ := c.Get("user_id")
|
|
||||||
h.container.Logger.Info("管理员更新材质",
|
|
||||||
zap.Int64("operator_id", operatorID.(int64)),
|
|
||||||
zap.Int64("texture_id", textureID),
|
|
||||||
)
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
|
||||||
"message": "材质更新成功",
|
|
||||||
"texture_id": textureID,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchDeleteTextures 批量删除材质
|
|
||||||
// @Summary 批量删除材质
|
|
||||||
// @Description 管理员批量删除多个材质
|
|
||||||
// @Tags Admin
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body map[string]interface{} true "批量删除请求"
|
|
||||||
// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功"
|
|
||||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
|
||||||
// @Security BearerAuth
|
|
||||||
// @Router /api/v1/admin/textures/batch-delete [delete]
|
|
||||||
func (h *AdminHandler) BatchDeleteTextures(c *gin.Context) {
|
|
||||||
var req struct {
|
|
||||||
TextureIDs []int64 `json:"texture_ids" binding:"required,min=1"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
RespondBadRequest(c, "参数错误", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
operatorID, _ := c.Get("user_id")
|
|
||||||
|
|
||||||
// 批量删除
|
|
||||||
if err := h.container.DB.Where("id IN ?", req.TextureIDs).Delete(&model.Texture{}).Error; err != nil {
|
|
||||||
RespondServerError(c, "批量删除材质失败", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.container.Logger.Info("管理员批量删除材质",
|
|
||||||
zap.Int64("operator_id", operatorID.(int64)),
|
|
||||||
zap.Int("count", len(req.TextureIDs)),
|
|
||||||
)
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
|
||||||
"message": "批量删除材质成功",
|
|
||||||
"count": len(req.TextureIDs),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStats 获取统计信息
|
|
||||||
// @Summary 获取统计信息
|
|
||||||
// @Description 管理员获取系统统计数据
|
|
||||||
// @Tags Admin
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} model.Response{data=types.AdminStatsResponse} "获取成功"
|
|
||||||
// @Security BearerAuth
|
|
||||||
// @Router /api/v1/admin/stats [get]
|
|
||||||
func (h *AdminHandler) GetStats(c *gin.Context) {
|
|
||||||
var stats types.AdminStatsResponse
|
|
||||||
|
|
||||||
// 用户统计
|
|
||||||
h.container.DB.Model(&model.User{}).Count(&stats.TotalUsers)
|
|
||||||
h.container.DB.Model(&model.User{}).Where("status = ?", 1).Count(&stats.ActiveUsers)
|
|
||||||
h.container.DB.Model(&model.User{}).Where("status = ?", 0).Count(&stats.BannedUsers)
|
|
||||||
h.container.DB.Model(&model.User{}).Where("role = ?", "admin").Count(&stats.AdminUsers)
|
|
||||||
|
|
||||||
// 材质统计
|
|
||||||
h.container.DB.Model(&model.Texture{}).Count(&stats.TotalTextures)
|
|
||||||
h.container.DB.Model(&model.Texture{}).Where("is_public = ?", true).Count(&stats.PublicTextures)
|
|
||||||
h.container.DB.Model(&model.Texture{}).Where("status = ?", 0).Count(&stats.PendingTextures)
|
|
||||||
|
|
||||||
// 下载和收藏统计
|
|
||||||
h.container.DB.Model(&model.Texture{}).Select("COALESCE(SUM(download_count), 0)").Scan(&stats.TotalDownloads)
|
|
||||||
h.container.DB.Model(&model.Texture{}).Select("COALESCE(SUM(favorite_count), 0)").Scan(&stats.TotalFavorites)
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewSuccessResponse(stats))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -117,16 +117,6 @@ func (h *AuthHandler) SendVerificationCode(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证邮箱格式
|
|
||||||
if !isValidEmail(req.Email) {
|
|
||||||
h.logger.Warn("发送验证码失败:邮箱格式错误",
|
|
||||||
zap.String("email", req.Email),
|
|
||||||
)
|
|
||||||
RespondBadRequest(c, "邮箱格式错误", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用服务发送验证码
|
|
||||||
if err := h.container.VerificationService.SendCode(c.Request.Context(), req.Email, req.Type); err != nil {
|
if err := h.container.VerificationService.SendCode(c.Request.Context(), req.Email, req.Type); err != nil {
|
||||||
h.logger.Error("发送验证码失败",
|
h.logger.Error("发送验证码失败",
|
||||||
zap.String("email", req.Email),
|
zap.String("email", req.Email),
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"carrotskin/internal/model"
|
"carrotskin/internal/model"
|
||||||
"carrotskin/internal/types"
|
"carrotskin/internal/types"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -228,14 +227,3 @@ func RespondWithError(c *gin.Context, err error) {
|
|||||||
// 默认返回500错误
|
// 默认返回500错误
|
||||||
RespondServerError(c, err.Error(), err)
|
RespondServerError(c, err.Error(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isValidEmail 验证邮箱格式
|
|
||||||
func isValidEmail(email string) bool {
|
|
||||||
if email == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 更严格的邮箱格式验证
|
|
||||||
emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
|
|
||||||
matched, _ := regexp.MatchString(emailRegex, email)
|
|
||||||
return matched
|
|
||||||
}
|
|
||||||
|
|||||||
495
internal/handler/report_handler.go
Normal file
495
internal/handler/report_handler.go
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrotskin/internal/container"
|
||||||
|
"carrotskin/internal/model"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReportHandler 举报处理器
|
||||||
|
type ReportHandler struct {
|
||||||
|
container *container.Container
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReportHandler 创建ReportHandler实例
|
||||||
|
func NewReportHandler(c *container.Container) *ReportHandler {
|
||||||
|
return &ReportHandler{
|
||||||
|
container: c,
|
||||||
|
logger: c.Logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateReportRequest 创建举报请求
|
||||||
|
type CreateReportRequest struct {
|
||||||
|
TargetType string `json:"target_type" binding:"required"` // "texture" 或 "user"
|
||||||
|
TargetID int64 `json:"target_id" binding:"required"`
|
||||||
|
Reason string `json:"reason" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateReport 创建举报
|
||||||
|
// @Summary 创建举报
|
||||||
|
// @Description 用户举报皮肤或其他用户
|
||||||
|
// @Tags report
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param request body CreateReportRequest true "举报信息"
|
||||||
|
// @Success 200 {object} model.Response{data=model.Report} "创建成功"
|
||||||
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||||
|
// @Failure 401 {object} model.ErrorResponse "未授权"
|
||||||
|
// @Router /api/v1/report [post]
|
||||||
|
func (h *ReportHandler) CreateReport(c *gin.Context) {
|
||||||
|
userID, ok := GetUserIDFromContext(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateReportRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
RespondBadRequest(c, "参数错误", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换目标类型
|
||||||
|
var targetType model.ReportType
|
||||||
|
switch req.TargetType {
|
||||||
|
case "texture":
|
||||||
|
targetType = model.ReportTypeTexture
|
||||||
|
case "user":
|
||||||
|
targetType = model.ReportTypeUser
|
||||||
|
default:
|
||||||
|
RespondBadRequest(c, "无效的举报类型", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
report, err := h.container.ReportService.CreateReport(c.Request.Context(), userID, targetType, req.TargetID, req.Reason)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, err.Error(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondSuccess(c, report)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID 获取举报详情
|
||||||
|
// @Summary 获取举报详情
|
||||||
|
// @Description 获取指定ID的举报详细信息
|
||||||
|
// @Tags report
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path int true "举报ID"
|
||||||
|
// @Success 200 {object} model.Response{data=model.Report} "获取成功"
|
||||||
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||||
|
// @Failure 404 {object} model.ErrorResponse "举报不存在"
|
||||||
|
// @Router /api/v1/report/{id} [get]
|
||||||
|
func (h *ReportHandler) GetByID(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, "无效的举报ID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
report, err := h.container.ReportService.GetByID(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
RespondNotFound(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondSuccess(c, report)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByReporterID 获取举报人的举报记录
|
||||||
|
// @Summary 获取举报人的举报记录
|
||||||
|
// @Description 获取指定用户的举报记录列表
|
||||||
|
// @Tags report
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param reporter_id path int true "举报人ID"
|
||||||
|
// @Param page query int false "页码" default(1)
|
||||||
|
// @Param page_size query int false "每页数量" default(20)
|
||||||
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||||
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||||
|
// @Router /api/v1/report/reporter/{reporter_id} [get]
|
||||||
|
func (h *ReportHandler) GetByReporterID(c *gin.Context) {
|
||||||
|
userID, ok := GetUserIDFromContext(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reporterID, err := strconv.ParseInt(c.Param("reporter_id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, "无效的举报人ID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := parseIntWithDefault(c.DefaultQuery("page", "1"), 1)
|
||||||
|
pageSize := parseIntWithDefault(c.DefaultQuery("page_size", "20"), 20)
|
||||||
|
|
||||||
|
reports, total, err := h.container.ReportService.GetByReporterID(c.Request.Context(), reporterID, userID, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, err.Error(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondSuccess(c, gin.H{
|
||||||
|
"list": reports,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"per_page": pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByTarget 获取目标对象的举报记录
|
||||||
|
// @Summary 获取目标对象的举报记录
|
||||||
|
// @Description 获取指定目标对象的举报记录列表(仅管理员)
|
||||||
|
// @Tags report
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param target_type path string true "目标类型 (texture/user)"
|
||||||
|
// @Param target_id path int true "目标ID"
|
||||||
|
// @Param page query int false "页码" default(1)
|
||||||
|
// @Param page_size query int false "每页数量" default(20)
|
||||||
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||||
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||||
|
// @Failure 403 {object} model.ErrorResponse "无权访问"
|
||||||
|
// @Router /api/v1/report/target/{target_type}/{target_id} [get]
|
||||||
|
func (h *ReportHandler) GetByTarget(c *gin.Context) {
|
||||||
|
userID, ok := GetUserIDFromContext(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetTypeStr := c.Param("target_type")
|
||||||
|
targetID, err := strconv.ParseInt(c.Param("target_id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, "无效的目标ID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetType model.ReportType
|
||||||
|
switch targetTypeStr {
|
||||||
|
case "texture":
|
||||||
|
targetType = model.ReportTypeTexture
|
||||||
|
case "user":
|
||||||
|
targetType = model.ReportTypeUser
|
||||||
|
default:
|
||||||
|
RespondBadRequest(c, "无效的目标类型", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := parseIntWithDefault(c.DefaultQuery("page", "1"), 1)
|
||||||
|
pageSize := parseIntWithDefault(c.DefaultQuery("page_size", "20"), 20)
|
||||||
|
|
||||||
|
reports, total, err := h.container.ReportService.GetByTarget(c.Request.Context(), targetType, targetID, userID, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, err.Error(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondSuccess(c, gin.H{
|
||||||
|
"list": reports,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"per_page": pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByStatus 根据状态查询举报记录
|
||||||
|
// @Summary 根据状态查询举报记录
|
||||||
|
// @Description 根据状态查询举报记录列表(仅管理员)
|
||||||
|
// @Tags report
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param status path string true "状态 (pending/approved/rejected)"
|
||||||
|
// @Param page query int false "页码" default(1)
|
||||||
|
// @Param page_size query int false "每页数量" default(20)
|
||||||
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||||
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||||
|
// @Router /api/v1/report/status/{status} [get]
|
||||||
|
func (h *ReportHandler) GetByStatus(c *gin.Context) {
|
||||||
|
statusStr := c.Param("status")
|
||||||
|
|
||||||
|
var status model.ReportStatus
|
||||||
|
switch statusStr {
|
||||||
|
case "pending":
|
||||||
|
status = model.ReportStatusPending
|
||||||
|
case "approved":
|
||||||
|
status = model.ReportStatusApproved
|
||||||
|
case "rejected":
|
||||||
|
status = model.ReportStatusRejected
|
||||||
|
default:
|
||||||
|
RespondBadRequest(c, "无效的状态", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := parseIntWithDefault(c.DefaultQuery("page", "1"), 1)
|
||||||
|
pageSize := parseIntWithDefault(c.DefaultQuery("page_size", "20"), 20)
|
||||||
|
|
||||||
|
reports, total, err := h.container.ReportService.GetByStatus(c.Request.Context(), status, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
RespondServerError(c, err.Error(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondSuccess(c, gin.H{
|
||||||
|
"list": reports,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"per_page": pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search 搜索举报记录
|
||||||
|
// @Summary 搜索举报记录
|
||||||
|
// @Description 搜索举报记录(仅管理员)
|
||||||
|
// @Tags report
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param keyword query int false "关键词(举报人ID或目标ID)"
|
||||||
|
// @Param page query int false "页码" default(1)
|
||||||
|
// @Param page_size query int false "每页数量" default(20)
|
||||||
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||||
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||||
|
// @Failure 403 {object} model.ErrorResponse "无权访问"
|
||||||
|
// @Router /api/v1/report/search [get]
|
||||||
|
func (h *ReportHandler) Search(c *gin.Context) {
|
||||||
|
userID, ok := GetUserIDFromContext(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keywordStr := c.Query("keyword")
|
||||||
|
keyword, err := strconv.ParseInt(keywordStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, "无效的关键词", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := parseIntWithDefault(c.DefaultQuery("page", "1"), 1)
|
||||||
|
pageSize := parseIntWithDefault(c.DefaultQuery("page_size", "20"), 20)
|
||||||
|
|
||||||
|
reports, total, err := h.container.ReportService.Search(c.Request.Context(), keyword, userID, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, err.Error(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondSuccess(c, gin.H{
|
||||||
|
"list": reports,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"per_page": pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReviewRequest 处理举报请求
|
||||||
|
type ReviewRequest struct {
|
||||||
|
Status string `json:"status" binding:"required"` // "approved" 或 "rejected"
|
||||||
|
ReviewNote string `json:"review_note"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Review 处理举报记录
|
||||||
|
// @Summary 处理举报记录
|
||||||
|
// @Description 管理员处理举报记录
|
||||||
|
// @Tags report
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path int true "举报ID"
|
||||||
|
// @Param request body ReviewRequest true "处理信息"
|
||||||
|
// @Success 200 {object} model.Response{data=model.Report} "处理成功"
|
||||||
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||||
|
// @Failure 403 {object} model.ErrorResponse "无权访问"
|
||||||
|
// @Router /api/v1/report/{id}/review [put]
|
||||||
|
func (h *ReportHandler) Review(c *gin.Context) {
|
||||||
|
userID, ok := GetUserIDFromContext(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, "无效的举报ID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req ReviewRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
RespondBadRequest(c, "参数错误", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换状态
|
||||||
|
var status model.ReportStatus
|
||||||
|
switch req.Status {
|
||||||
|
case "approved":
|
||||||
|
status = model.ReportStatusApproved
|
||||||
|
case "rejected":
|
||||||
|
status = model.ReportStatusRejected
|
||||||
|
default:
|
||||||
|
RespondBadRequest(c, "无效的状态", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
report, err := h.container.ReportService.Review(c.Request.Context(), id, userID, status, req.ReviewNote)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, err.Error(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondSuccess(c, report)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchReviewRequest 批量处理举报请求
|
||||||
|
type BatchReviewRequest struct {
|
||||||
|
IDs []int64 `json:"ids" binding:"required"`
|
||||||
|
Status string `json:"status" binding:"required"` // "approved" 或 "rejected"
|
||||||
|
ReviewNote string `json:"review_note"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchReview 批量处理举报记录
|
||||||
|
// @Summary 批量处理举报记录
|
||||||
|
// @Description 管理员批量处理举报记录
|
||||||
|
// @Tags report
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param request body BatchReviewRequest true "处理信息"
|
||||||
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "处理成功"
|
||||||
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||||
|
// @Failure 403 {object} model.ErrorResponse "无权访问"
|
||||||
|
// @Router /api/v1/report/batch-review [put]
|
||||||
|
func (h *ReportHandler) BatchReview(c *gin.Context) {
|
||||||
|
userID, ok := GetUserIDFromContext(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req BatchReviewRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
RespondBadRequest(c, "参数错误", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换状态
|
||||||
|
var status model.ReportStatus
|
||||||
|
switch req.Status {
|
||||||
|
case "approved":
|
||||||
|
status = model.ReportStatusApproved
|
||||||
|
case "rejected":
|
||||||
|
status = model.ReportStatusRejected
|
||||||
|
default:
|
||||||
|
RespondBadRequest(c, "无效的状态", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
affected, err := h.container.ReportService.BatchReview(c.Request.Context(), req.IDs, userID, status, req.ReviewNote)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, err.Error(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondSuccess(c, gin.H{
|
||||||
|
"affected": affected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除举报记录
|
||||||
|
// @Summary 删除举报记录
|
||||||
|
// @Description 删除指定的举报记录
|
||||||
|
// @Tags report
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path int true "举报ID"
|
||||||
|
// @Success 200 {object} model.Response "删除成功"
|
||||||
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||||
|
// @Failure 403 {object} model.ErrorResponse "无权访问"
|
||||||
|
// @Router /api/v1/report/{id} [delete]
|
||||||
|
func (h *ReportHandler) Delete(c *gin.Context) {
|
||||||
|
userID, ok := GetUserIDFromContext(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, "无效的举报ID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.container.ReportService.Delete(c.Request.Context(), id, userID); err != nil {
|
||||||
|
RespondBadRequest(c, err.Error(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondSuccess(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchDeleteRequest 批量删除请求
|
||||||
|
type BatchDeleteRequest struct {
|
||||||
|
IDs []int64 `json:"ids" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchDelete 批量删除举报记录
|
||||||
|
// @Summary 批量删除举报记录
|
||||||
|
// @Description 管理员批量删除举报记录
|
||||||
|
// @Tags report
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param request body BatchDeleteRequest true "删除信息"
|
||||||
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功"
|
||||||
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||||
|
// @Failure 403 {object} model.ErrorResponse "无权访问"
|
||||||
|
// @Router /api/v1/report/batch-delete [delete]
|
||||||
|
func (h *ReportHandler) BatchDelete(c *gin.Context) {
|
||||||
|
userID, ok := GetUserIDFromContext(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req BatchDeleteRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
RespondBadRequest(c, "参数错误", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
affected, err := h.container.ReportService.BatchDelete(c.Request.Context(), req.IDs, userID)
|
||||||
|
if err != nil {
|
||||||
|
RespondBadRequest(c, err.Error(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondSuccess(c, gin.H{
|
||||||
|
"affected": affected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats 获取举报统计信息
|
||||||
|
// @Summary 获取举报统计信息
|
||||||
|
// @Description 获取举报统计信息(仅管理员)
|
||||||
|
// @Tags report
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
||||||
|
// @Router /api/v1/report/stats [get]
|
||||||
|
func (h *ReportHandler) GetStats(c *gin.Context) {
|
||||||
|
stats, err := h.container.ReportService.GetStats(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
RespondServerError(c, err.Error(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
RespondSuccess(c, stats)
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ type Handlers struct {
|
|||||||
Yggdrasil *YggdrasilHandler
|
Yggdrasil *YggdrasilHandler
|
||||||
CustomSkin *CustomSkinHandler
|
CustomSkin *CustomSkinHandler
|
||||||
Admin *AdminHandler
|
Admin *AdminHandler
|
||||||
|
Report *ReportHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandlers 创建所有Handler实例
|
// NewHandlers 创建所有Handler实例
|
||||||
@@ -34,6 +35,7 @@ func NewHandlers(c *container.Container) *Handlers {
|
|||||||
Yggdrasil: NewYggdrasilHandler(c),
|
Yggdrasil: NewYggdrasilHandler(c),
|
||||||
CustomSkin: NewCustomSkinHandler(c),
|
CustomSkin: NewCustomSkinHandler(c),
|
||||||
Admin: NewAdminHandler(c),
|
Admin: NewAdminHandler(c),
|
||||||
|
Report: NewReportHandler(c),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +79,9 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
|
|||||||
|
|
||||||
// 管理员路由(需要管理员权限)
|
// 管理员路由(需要管理员权限)
|
||||||
registerAdminRoutes(v1, c, h.Admin)
|
registerAdminRoutes(v1, c, h.Admin)
|
||||||
|
|
||||||
|
// 举报路由
|
||||||
|
registerReportRoutes(v1, h.Report, c.JWT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,28 +208,18 @@ func registerAdminRoutes(v1 *gin.RouterGroup, c *container.Container, h *AdminHa
|
|||||||
admin.Use(middleware.RequireAdmin())
|
admin.Use(middleware.RequireAdmin())
|
||||||
{
|
{
|
||||||
|
|
||||||
// 统计信息
|
|
||||||
admin.GET("/stats", h.GetStats)
|
|
||||||
|
|
||||||
// 角色管理
|
|
||||||
admin.GET("/roles", h.GetRoles)
|
|
||||||
|
|
||||||
// 用户管理
|
// 用户管理
|
||||||
admin.GET("/users", h.GetUserList)
|
admin.GET("/users", h.GetUserList)
|
||||||
admin.GET("/users/search", h.SearchUsers)
|
|
||||||
admin.GET("/users/:id", h.GetUserDetail)
|
admin.GET("/users/:id", h.GetUserDetail)
|
||||||
admin.DELETE("/users/:id", h.DeleteUser)
|
|
||||||
admin.PUT("/users/role", h.SetUserRole)
|
admin.PUT("/users/role", h.SetUserRole)
|
||||||
admin.PUT("/users/status", h.SetUserStatus)
|
admin.PUT("/users/status", h.SetUserStatus)
|
||||||
admin.PUT("/users/batch-role", h.BatchSetUserRole)
|
|
||||||
admin.DELETE("/users/batch-delete", h.BatchDeleteUsers)
|
|
||||||
|
|
||||||
// 材质管理(审核)
|
// 材质管理(审核)
|
||||||
admin.GET("/textures", h.GetTextureList)
|
admin.GET("/textures", h.GetTextureList)
|
||||||
admin.GET("/textures/search", h.SearchTextures)
|
|
||||||
admin.PUT("/textures/:id", h.UpdateTexture)
|
|
||||||
admin.DELETE("/textures/:id", h.DeleteTexture)
|
admin.DELETE("/textures/:id", h.DeleteTexture)
|
||||||
admin.DELETE("/textures/batch-delete", h.BatchDeleteTextures)
|
|
||||||
|
// 权限管理
|
||||||
|
admin.GET("/permissions", h.GetPermissions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,3 +241,28 @@ func registerCustomSkinRoutes(v1 *gin.RouterGroup, h *CustomSkinHandler) {
|
|||||||
csl.GET("/textures/:hash", h.GetTexture)
|
csl.GET("/textures/:hash", h.GetTexture)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// registerReportRoutes 注册举报路由
|
||||||
|
func registerReportRoutes(v1 *gin.RouterGroup, h *ReportHandler, jwtService *auth.JWTService) {
|
||||||
|
reportGroup := v1.Group("/report")
|
||||||
|
{
|
||||||
|
// 公开路由(无需认证)
|
||||||
|
reportGroup.GET("/stats", h.GetStats)
|
||||||
|
|
||||||
|
// 需要认证的路由
|
||||||
|
reportAuth := reportGroup.Group("")
|
||||||
|
reportAuth.Use(middleware.AuthMiddleware(jwtService))
|
||||||
|
{
|
||||||
|
reportAuth.POST("", h.CreateReport)
|
||||||
|
reportAuth.GET("/:id", h.GetByID)
|
||||||
|
reportAuth.GET("/reporter_id", h.GetByReporterID)
|
||||||
|
reportAuth.GET("/target", h.GetByTarget)
|
||||||
|
reportAuth.GET("/status", h.GetByStatus)
|
||||||
|
reportAuth.GET("/search", h.Search)
|
||||||
|
reportAuth.PUT("/:id/review", h.Review)
|
||||||
|
reportAuth.POST("/batch-review", h.BatchReview)
|
||||||
|
reportAuth.DELETE("/:id", h.Delete)
|
||||||
|
reportAuth.POST("/batch-delete", h.BatchDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
49
internal/model/report.go
Normal file
49
internal/model/report.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReportType 举报类型
|
||||||
|
// @Description 举报类型枚举:TEXTURE(皮肤)或USER(用户)
|
||||||
|
type ReportType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReportTypeTexture ReportType = "TEXTURE"
|
||||||
|
ReportTypeUser ReportType = "USER"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReportStatus 举报状态
|
||||||
|
// @Description 举报状态枚举:PENDING(待处理)、APPROVED(已通过)、REJECTED(已驳回)
|
||||||
|
type ReportStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReportStatusPending ReportStatus = "PENDING"
|
||||||
|
ReportStatusApproved ReportStatus = "APPROVED"
|
||||||
|
ReportStatusRejected ReportStatus = "REJECTED"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Report 举报模型
|
||||||
|
// @Description 用户举报记录模型,用于举报皮肤或用户
|
||||||
|
type Report struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||||
|
ReporterID int64 `gorm:"column:reporter_id;not null;index:idx_reports_reporter_created,priority:1" json:"reporter_id"` // 举报人ID
|
||||||
|
TargetType ReportType `gorm:"column:target_type;type:varchar(50);not null;index:idx_reports_target_status,priority:1" json:"target_type"` // TEXTURE 或 USER
|
||||||
|
TargetID int64 `gorm:"column:target_id;not null;index:idx_reports_target_status,priority:2" json:"target_id"` // 被举报对象ID(皮肤ID或用户ID)
|
||||||
|
Reason string `gorm:"column:reason;type:text;not null" json:"reason"` // 举报原因
|
||||||
|
Status ReportStatus `gorm:"column:status;type:varchar(50);not null;default:'PENDING';index:idx_reports_status_created,priority:1;index:idx_reports_target_status,priority:3" json:"status"` // PENDING, APPROVED, REJECTED
|
||||||
|
ReviewerID *int64 `gorm:"column:reviewer_id;type:bigint" json:"reviewer_id,omitempty"` // 处理人ID(管理员)
|
||||||
|
ReviewNote string `gorm:"column:review_note;type:text" json:"review_note,omitempty"` // 处理备注
|
||||||
|
ReviewedAt *time.Time `gorm:"column:reviewed_at;type:timestamp" json:"reviewed_at,omitempty"` // 处理时间
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_reports_reporter_created,priority:2,sort:desc;index:idx_reports_status_created,priority:2,sort:desc" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
|
||||||
|
|
||||||
|
// 关联
|
||||||
|
Reporter *User `gorm:"foreignKey:ReporterID;constraint:OnDelete:CASCADE" json:"reporter,omitempty"`
|
||||||
|
Reviewer *User `gorm:"foreignKey:ReviewerID;constraint:OnDelete:SET NULL" json:"reviewer,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName 指定表名
|
||||||
|
func (Report) TableName() string {
|
||||||
|
return "reports"
|
||||||
|
}
|
||||||
@@ -56,17 +56,12 @@ type TextureRepository interface {
|
|||||||
Delete(ctx context.Context, id int64) error
|
Delete(ctx context.Context, id int64) error
|
||||||
BatchDelete(ctx context.Context, ids []int64) (int64, error) // 批量删除
|
BatchDelete(ctx context.Context, ids []int64) (int64, error) // 批量删除
|
||||||
IncrementDownloadCount(ctx context.Context, id int64) error
|
IncrementDownloadCount(ctx context.Context, id int64) error
|
||||||
IncrementFavoriteCount(ctx context.Context, id int64) error
|
|
||||||
DecrementFavoriteCount(ctx context.Context, id int64) error
|
|
||||||
CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error
|
CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error
|
||||||
IsFavorited(ctx context.Context, userID, textureID int64) (bool, error)
|
ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error)
|
||||||
AddFavorite(ctx context.Context, userID, textureID int64) error
|
|
||||||
RemoveFavorite(ctx context.Context, userID, textureID int64) error
|
|
||||||
GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
||||||
CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error)
|
CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// YggdrasilRepository Yggdrasil仓储接口
|
// YggdrasilRepository Yggdrasil仓储接口
|
||||||
type YggdrasilRepository interface {
|
type YggdrasilRepository interface {
|
||||||
GetPasswordByID(ctx context.Context, id int64) (string, error)
|
GetPasswordByID(ctx context.Context, id int64) (string, error)
|
||||||
@@ -84,3 +79,21 @@ type ClientRepository interface {
|
|||||||
DeleteByClientToken(ctx context.Context, clientToken string) error
|
DeleteByClientToken(ctx context.Context, clientToken string) error
|
||||||
DeleteByUserID(ctx context.Context, userID int64) error
|
DeleteByUserID(ctx context.Context, userID int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReportRepository 举报仓储接口
|
||||||
|
type ReportRepository interface {
|
||||||
|
Create(ctx context.Context, report *model.Report) error
|
||||||
|
FindByID(ctx context.Context, id int64) (*model.Report, error)
|
||||||
|
FindByReporterID(ctx context.Context, reporterID int64, page, pageSize int) ([]*model.Report, int64, error)
|
||||||
|
FindByTarget(ctx context.Context, targetType model.ReportType, targetID int64, page, pageSize int) ([]*model.Report, int64, error)
|
||||||
|
FindByStatus(ctx context.Context, status model.ReportStatus, page, pageSize int) ([]*model.Report, int64, error)
|
||||||
|
Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Report, int64, error)
|
||||||
|
Update(ctx context.Context, report *model.Report) error
|
||||||
|
UpdateFields(ctx context.Context, id int64, fields map[string]interface{}) error
|
||||||
|
Review(ctx context.Context, id int64, status model.ReportStatus, reviewerID int64, reviewNote string) error
|
||||||
|
BatchReview(ctx context.Context, ids []int64, status model.ReportStatus, reviewerID int64, reviewNote string) (int64, error)
|
||||||
|
Delete(ctx context.Context, id int64) error
|
||||||
|
BatchDelete(ctx context.Context, ids []int64) (int64, error)
|
||||||
|
CountByStatus(ctx context.Context, status model.ReportStatus) (int64, error)
|
||||||
|
CheckDuplicate(ctx context.Context, reporterID int64, targetType model.ReportType, targetID int64) (bool, error)
|
||||||
|
}
|
||||||
|
|||||||
225
internal/repository/report_repository.go
Normal file
225
internal/repository/report_repository.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrotskin/internal/model"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// reportRepository 举报仓储实现
|
||||||
|
type reportRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReportRepository 创建举报仓储实例
|
||||||
|
func NewReportRepository(db *gorm.DB) ReportRepository {
|
||||||
|
return &reportRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建举报记录
|
||||||
|
func (r *reportRepository) Create(ctx context.Context, report *model.Report) error {
|
||||||
|
return r.db.WithContext(ctx).Create(report).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByID 根据ID查找举报记录
|
||||||
|
func (r *reportRepository) FindByID(ctx context.Context, id int64) (*model.Report, error) {
|
||||||
|
var report model.Report
|
||||||
|
err := r.db.WithContext(ctx).Preload("Reporter").Preload("Reviewer").First(&report, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByReporterID 根据举报人ID查找举报记录
|
||||||
|
func (r *reportRepository) FindByReporterID(ctx context.Context, reporterID int64, page, pageSize int) ([]*model.Report, int64, error) {
|
||||||
|
var reports []*model.Report
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
// 查询总数
|
||||||
|
if err := r.db.WithContext(ctx).Model(&model.Report{}).Where("reporter_id = ?", reporterID).Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Reporter").
|
||||||
|
Preload("Reviewer").
|
||||||
|
Where("reporter_id = ?", reporterID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(pageSize).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&reports).Error
|
||||||
|
|
||||||
|
return reports, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByTarget 根据目标对象查找举报记录
|
||||||
|
func (r *reportRepository) FindByTarget(ctx context.Context, targetType model.ReportType, targetID int64, page, pageSize int) ([]*model.Report, int64, error) {
|
||||||
|
var reports []*model.Report
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
// 查询总数
|
||||||
|
if err := r.db.WithContext(ctx).Model(&model.Report{}).Where("target_type = ? AND target_id = ?", targetType, targetID).Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Reporter").
|
||||||
|
Preload("Reviewer").
|
||||||
|
Where("target_type = ? AND target_id = ?", targetType, targetID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(pageSize).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&reports).Error
|
||||||
|
|
||||||
|
return reports, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByStatus 根据状态查找举报记录
|
||||||
|
func (r *reportRepository) FindByStatus(ctx context.Context, status model.ReportStatus, page, pageSize int) ([]*model.Report, int64, error) {
|
||||||
|
var reports []*model.Report
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
// 查询总数
|
||||||
|
if err := r.db.WithContext(ctx).Model(&model.Report{}).Where("status = ?", status).Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Reporter").
|
||||||
|
Preload("Reviewer").
|
||||||
|
Where("status = ?", status).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(pageSize).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&reports).Error
|
||||||
|
|
||||||
|
return reports, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search 搜索举报记录
|
||||||
|
func (r *reportRepository) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Report, int64, error) {
|
||||||
|
var reports []*model.Report
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
query := r.db.WithContext(ctx).Model(&model.Report{}).Where("reason LIKE ?", "%"+keyword+"%")
|
||||||
|
|
||||||
|
// 查询总数
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
err := query.
|
||||||
|
Preload("Reporter").
|
||||||
|
Preload("Reviewer").
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(pageSize).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&reports).Error
|
||||||
|
|
||||||
|
return reports, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新举报记录
|
||||||
|
func (r *reportRepository) Update(ctx context.Context, report *model.Report) error {
|
||||||
|
return r.db.WithContext(ctx).Save(report).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFields 更新举报记录的指定字段
|
||||||
|
func (r *reportRepository) UpdateFields(ctx context.Context, id int64, fields map[string]interface{}) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&model.Report{}).Where("id = ?", id).Updates(fields).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Review 处理举报记录
|
||||||
|
func (r *reportRepository) Review(ctx context.Context, id int64, status model.ReportStatus, reviewerID int64, reviewNote string) error {
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
var report model.Report
|
||||||
|
if err := tx.First(&report, id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查状态是否已被处理
|
||||||
|
if report.Status != model.ReportStatusPending {
|
||||||
|
return errors.New("report has already been reviewed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新举报状态
|
||||||
|
now := time.Now()
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"status": status,
|
||||||
|
"reviewer_id": reviewerID,
|
||||||
|
"review_note": reviewNote,
|
||||||
|
"reviewed_at": &now,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Model(&report).Updates(updates).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchReview 批量处理举报记录
|
||||||
|
func (r *reportRepository) BatchReview(ctx context.Context, ids []int64, status model.ReportStatus, reviewerID int64, reviewNote string) (int64, error) {
|
||||||
|
var affected int64
|
||||||
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
now := time.Now()
|
||||||
|
result := tx.Model(&model.Report{}).
|
||||||
|
Where("id IN ? AND status = ?", ids, model.ReportStatusPending).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"status": status,
|
||||||
|
"reviewer_id": reviewerID,
|
||||||
|
"review_note": reviewNote,
|
||||||
|
"reviewed_at": &now,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
affected = result.RowsAffected
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return affected, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除举报记录
|
||||||
|
func (r *reportRepository) Delete(ctx context.Context, id int64) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&model.Report{}, id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchDelete 批量删除举报记录
|
||||||
|
func (r *reportRepository) BatchDelete(ctx context.Context, ids []int64) (int64, error) {
|
||||||
|
result := r.db.WithContext(ctx).Delete(&model.Report{}, ids)
|
||||||
|
return result.RowsAffected, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountByStatus 根据状态统计举报数量
|
||||||
|
func (r *reportRepository) CountByStatus(ctx context.Context, status model.ReportStatus) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.WithContext(ctx).Model(&model.Report{}).Where("status = ?", status).Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckDuplicate 检查是否重复举报
|
||||||
|
func (r *reportRepository) CheckDuplicate(ctx context.Context, reporterID int64, targetType model.ReportType, targetID int64) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.WithContext(ctx).Model(&model.Report{}).
|
||||||
|
Where("reporter_id = ? AND target_type = ? AND target_id = ? AND status = ?",
|
||||||
|
reporterID, targetType, targetID, model.ReportStatusPending).
|
||||||
|
Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
@@ -98,7 +98,6 @@ func TestProfileRepository_Basic(t *testing.T) {
|
|||||||
t.Fatalf("CountByUserID mismatch: %d err=%v", count, err)
|
t.Fatalf("CountByUserID mismatch: %d err=%v", count, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if err := profileRepo.UpdateLastUsedAt(ctx, "p-uuid"); err != nil {
|
if err := profileRepo.UpdateLastUsedAt(ctx, "p-uuid"); err != nil {
|
||||||
t.Fatalf("UpdateLastUsedAt err: %v", err)
|
t.Fatalf("UpdateLastUsedAt err: %v", err)
|
||||||
}
|
}
|
||||||
@@ -150,22 +149,20 @@ func TestTextureRepository_Basic(t *testing.T) {
|
|||||||
t.Fatalf("FindByHashAndUploaderID mismatch")
|
t.Fatalf("FindByHashAndUploaderID mismatch")
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = textureRepo.IncrementFavoriteCount(ctx, tex.ID)
|
_, _ = textureRepo.ToggleFavorite(ctx, u.ID, tex.ID)
|
||||||
_ = textureRepo.DecrementFavoriteCount(ctx, tex.ID)
|
favList, _, _ := textureRepo.GetUserFavorites(ctx, u.ID, 1, 10)
|
||||||
|
if len(favList) == 0 {
|
||||||
|
t.Fatalf("GetUserFavorites expected at least 1 favorite")
|
||||||
|
}
|
||||||
|
_, _ = textureRepo.ToggleFavorite(ctx, u.ID, tex.ID)
|
||||||
|
favList, _, _ = textureRepo.GetUserFavorites(ctx, u.ID, 1, 10)
|
||||||
|
if len(favList) != 0 {
|
||||||
|
t.Fatalf("GetUserFavorites expected 0 favorites after toggle off")
|
||||||
|
}
|
||||||
|
|
||||||
_ = textureRepo.IncrementDownloadCount(ctx, tex.ID)
|
_ = textureRepo.IncrementDownloadCount(ctx, tex.ID)
|
||||||
_ = textureRepo.CreateDownloadLog(ctx, &model.TextureDownloadLog{TextureID: tex.ID, UserID: &u.ID, IPAddress: "127.0.0.1"})
|
_ = textureRepo.CreateDownloadLog(ctx, &model.TextureDownloadLog{TextureID: tex.ID, UserID: &u.ID, IPAddress: "127.0.0.1"})
|
||||||
|
|
||||||
// 收藏
|
|
||||||
_ = textureRepo.AddFavorite(ctx, u.ID, tex.ID)
|
|
||||||
if fav, err := textureRepo.IsFavorited(ctx, u.ID, tex.ID); err == nil {
|
|
||||||
if !fav {
|
|
||||||
t.Fatalf("IsFavorited expected true")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
t.Skipf("IsFavorited not supported by sqlite: %v", err)
|
|
||||||
}
|
|
||||||
_ = textureRepo.RemoveFavorite(ctx, u.ID, tex.ID)
|
|
||||||
|
|
||||||
// 批量更新与删除
|
// 批量更新与删除
|
||||||
if affected, err := textureRepo.BatchUpdate(ctx, []int64{tex.ID}, map[string]interface{}{"name": "tex-new"}); err != nil || affected != 1 {
|
if affected, err := textureRepo.BatchUpdate(ctx, []int64{tex.ID}, map[string]interface{}{"name": "tex-new"}); err != nil || affected != 1 {
|
||||||
t.Fatalf("BatchUpdate mismatch, affected=%d err=%v", affected, err)
|
t.Fatalf("BatchUpdate mismatch, affected=%d err=%v", affected, err)
|
||||||
@@ -187,7 +184,7 @@ func TestTextureRepository_Basic(t *testing.T) {
|
|||||||
if list, total, err := textureRepo.Search(ctx, "search", model.TextureTypeCape, true, 1, 10); err != nil || total == 0 || len(list) == 0 {
|
if list, total, err := textureRepo.Search(ctx, "search", model.TextureTypeCape, true, 1, 10); err != nil || total == 0 || len(list) == 0 {
|
||||||
t.Fatalf("Search mismatch, total=%d len=%d err=%v", total, len(list), err)
|
t.Fatalf("Search mismatch, total=%d len=%d err=%v", total, len(list), err)
|
||||||
}
|
}
|
||||||
_ = textureRepo.AddFavorite(ctx, u.ID, tex.ID+1)
|
_, _ = textureRepo.ToggleFavorite(ctx, u.ID, tex.ID+1)
|
||||||
if favList, total, err := textureRepo.GetUserFavorites(ctx, u.ID, 1, 10); err != nil || total == 0 || len(favList) == 0 {
|
if favList, total, err := textureRepo.GetUserFavorites(ctx, u.ID, 1, 10); err != nil || total == 0 || len(favList) == 0 {
|
||||||
t.Fatalf("GetUserFavorites mismatch, total=%d len=%d err=%v", total, len(favList), err)
|
t.Fatalf("GetUserFavorites mismatch, total=%d len=%d err=%v", total, len(favList), err)
|
||||||
}
|
}
|
||||||
@@ -206,7 +203,6 @@ func TestTextureRepository_Basic(t *testing.T) {
|
|||||||
_ = textureRepo.Delete(ctx, tex.ID)
|
_ = textureRepo.Delete(ctx, tex.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func TestClientRepository_Basic(t *testing.T) {
|
func TestClientRepository_Basic(t *testing.T) {
|
||||||
db := testutil.NewTestDB(t)
|
db := testutil.NewTestDB(t)
|
||||||
repo := NewClientRepository(db)
|
repo := NewClientRepository(db)
|
||||||
|
|||||||
@@ -138,42 +138,52 @@ func (r *textureRepository) IncrementDownloadCount(ctx context.Context, id int64
|
|||||||
UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error
|
UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *textureRepository) IncrementFavoriteCount(ctx context.Context, id int64) error {
|
|
||||||
return r.db.WithContext(ctx).Model(&model.Texture{}).Where("id = ?", id).
|
|
||||||
UpdateColumn("favorite_count", gorm.Expr("favorite_count + ?", 1)).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *textureRepository) DecrementFavoriteCount(ctx context.Context, id int64) error {
|
|
||||||
return r.db.WithContext(ctx).Model(&model.Texture{}).Where("id = ?", id).
|
|
||||||
UpdateColumn("favorite_count", gorm.Expr("favorite_count - ?", 1)).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *textureRepository) CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error {
|
func (r *textureRepository) CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error {
|
||||||
return r.db.WithContext(ctx).Create(log).Error
|
return r.db.WithContext(ctx).Create(log).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *textureRepository) IsFavorited(ctx context.Context, userID, textureID int64) (bool, error) {
|
func (r *textureRepository) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
|
||||||
|
var isAdded bool
|
||||||
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
var count int64
|
var count int64
|
||||||
// 使用 Select("1") 优化,只查询是否存在,不需要查询所有字段
|
err := tx.Model(&model.UserTextureFavorite{}).
|
||||||
err := r.db.WithContext(ctx).Model(&model.UserTextureFavorite{}).
|
|
||||||
Select("1").
|
|
||||||
Where("user_id = ? AND texture_id = ?", userID, textureID).
|
Where("user_id = ? AND texture_id = ?", userID, textureID).
|
||||||
Limit(1).
|
|
||||||
Count(&count).Error
|
Count(&count).Error
|
||||||
return count > 0, err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
result := tx.Where("user_id = ? AND texture_id = ?", userID, textureID).
|
||||||
|
Delete(&model.UserTextureFavorite{})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected > 0 {
|
||||||
|
if err := tx.Model(&model.Texture{}).Where("id = ?", textureID).
|
||||||
|
UpdateColumn("favorite_count", gorm.Expr("GREATEST(favorite_count - 1, 0)")).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isAdded = false
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *textureRepository) AddFavorite(ctx context.Context, userID, textureID int64) error {
|
|
||||||
favorite := &model.UserTextureFavorite{
|
favorite := &model.UserTextureFavorite{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
TextureID: textureID,
|
TextureID: textureID,
|
||||||
}
|
}
|
||||||
return r.db.WithContext(ctx).Create(favorite).Error
|
if err := tx.Create(favorite).Error; err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
if err := tx.Model(&model.Texture{}).Where("id = ?", textureID).
|
||||||
func (r *textureRepository) RemoveFavorite(ctx context.Context, userID, textureID int64) error {
|
UpdateColumn("favorite_count", gorm.Expr("favorite_count + 1")).Error; err != nil {
|
||||||
return r.db.WithContext(ctx).Where("user_id = ? AND texture_id = ?", userID, textureID).
|
return err
|
||||||
Delete(&model.UserTextureFavorite{}).Error
|
}
|
||||||
|
isAdded = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return isAdded, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *textureRepository) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
func (r *textureRepository) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||||
|
|||||||
@@ -137,6 +137,30 @@ type SecurityService interface {
|
|||||||
ClearVerifyAttempts(ctx context.Context, email, codeType string) error
|
ClearVerifyAttempts(ctx context.Context, email, codeType string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReportService 举报服务接口
|
||||||
|
type ReportService interface {
|
||||||
|
// 创建举报
|
||||||
|
CreateReport(ctx context.Context, reporterID int64, targetType model.ReportType, targetID int64, reason string) (*model.Report, error)
|
||||||
|
|
||||||
|
// 查询举报
|
||||||
|
GetByID(ctx context.Context, id int64) (*model.Report, error)
|
||||||
|
GetByReporterID(ctx context.Context, reporterID, userID int64, page, pageSize int) ([]*model.Report, int64, error)
|
||||||
|
GetByTarget(ctx context.Context, targetType model.ReportType, targetID, userID int64, page, pageSize int) ([]*model.Report, int64, error)
|
||||||
|
GetByStatus(ctx context.Context, status model.ReportStatus, page, pageSize int) ([]*model.Report, int64, error)
|
||||||
|
Search(ctx context.Context, keyword, userID int64, page, pageSize int) ([]*model.Report, int64, error)
|
||||||
|
|
||||||
|
// 处理举报
|
||||||
|
Review(ctx context.Context, reportID, reviewerID int64, status model.ReportStatus, reviewNote string) (*model.Report, error)
|
||||||
|
BatchReview(ctx context.Context, ids []int64, reviewerID int64, status model.ReportStatus, reviewNote string) (int64, error)
|
||||||
|
|
||||||
|
// 删除举报
|
||||||
|
Delete(ctx context.Context, reportID, userID int64) error
|
||||||
|
BatchDelete(ctx context.Context, ids []int64, userID int64) (int64, error)
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
GetStats(ctx context.Context) (map[string]int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Services 服务集合
|
// Services 服务集合
|
||||||
type Services struct {
|
type Services struct {
|
||||||
User UserService
|
User UserService
|
||||||
@@ -147,6 +171,7 @@ type Services struct {
|
|||||||
Captcha CaptchaService
|
Captcha CaptchaService
|
||||||
Yggdrasil YggdrasilService
|
Yggdrasil YggdrasilService
|
||||||
Security SecurityService
|
Security SecurityService
|
||||||
|
Report ReportService
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceDeps 服务依赖
|
// ServiceDeps 服务依赖
|
||||||
|
|||||||
@@ -391,37 +391,24 @@ func (m *MockTextureRepository) IncrementFavoriteCount(ctx context.Context, id i
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTextureRepository) DecrementFavoriteCount(ctx context.Context, id int64) error {
|
|
||||||
if texture, ok := m.textures[id]; ok && texture.FavoriteCount > 0 {
|
|
||||||
texture.FavoriteCount--
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockTextureRepository) CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error {
|
func (m *MockTextureRepository) CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTextureRepository) IsFavorited(ctx context.Context, userID, textureID int64) (bool, error) {
|
func (m *MockTextureRepository) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
|
||||||
if userFavs, ok := m.favorites[userID]; ok {
|
|
||||||
return userFavs[textureID], nil
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockTextureRepository) AddFavorite(ctx context.Context, userID, textureID int64) error {
|
|
||||||
if m.favorites[userID] == nil {
|
if m.favorites[userID] == nil {
|
||||||
m.favorites[userID] = make(map[int64]bool)
|
m.favorites[userID] = make(map[int64]bool)
|
||||||
}
|
}
|
||||||
m.favorites[userID][textureID] = true
|
isFavorited := m.favorites[userID][textureID]
|
||||||
return nil
|
m.favorites[userID][textureID] = !isFavorited
|
||||||
|
if texture, ok := m.textures[textureID]; ok {
|
||||||
|
if !isFavorited {
|
||||||
|
texture.FavoriteCount++
|
||||||
|
} else if texture.FavoriteCount > 0 {
|
||||||
|
texture.FavoriteCount--
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTextureRepository) RemoveFavorite(ctx context.Context, userID, textureID int64) error {
|
|
||||||
if userFavs, ok := m.favorites[userID]; ok {
|
|
||||||
delete(userFavs, textureID)
|
|
||||||
}
|
}
|
||||||
return nil
|
return !isFavorited, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockTextureRepository) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
func (m *MockTextureRepository) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||||
@@ -474,7 +461,6 @@ func (m *MockTextureRepository) BatchDelete(ctx context.Context, ids []int64) (i
|
|||||||
return deleted, nil
|
return deleted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Service Mocks
|
// Service Mocks
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
335
internal/service/report_service.go
Normal file
335
internal/service/report_service.go
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
apperrors "carrotskin/internal/errors"
|
||||||
|
"carrotskin/internal/model"
|
||||||
|
"carrotskin/internal/repository"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// reportService ReportService的实现
|
||||||
|
type reportService struct {
|
||||||
|
reportRepo repository.ReportRepository
|
||||||
|
userRepo repository.UserRepository
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReportService 创建ReportService实例
|
||||||
|
func NewReportService(
|
||||||
|
reportRepo repository.ReportRepository,
|
||||||
|
userRepo repository.UserRepository,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) ReportService {
|
||||||
|
return &reportService{
|
||||||
|
reportRepo: reportRepo,
|
||||||
|
userRepo: userRepo,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateReport 创建举报
|
||||||
|
func (s *reportService) CreateReport(ctx context.Context, reporterID int64, targetType model.ReportType, targetID int64, reason string) (*model.Report, error) {
|
||||||
|
// 验证举报人存在
|
||||||
|
reporter, err := s.userRepo.FindByID(ctx, reporterID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("举报人不存在", zap.Int64("reporter_id", reporterID), zap.Error(err))
|
||||||
|
return nil, apperrors.ErrUserNotFound
|
||||||
|
}
|
||||||
|
if reporter == nil {
|
||||||
|
return nil, apperrors.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证举报原因
|
||||||
|
if reason == "" {
|
||||||
|
return nil, errors.New("举报原因不能为空")
|
||||||
|
}
|
||||||
|
if len(reason) > 500 {
|
||||||
|
return nil, errors.New("举报原因不能超过500字符")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证目标类型
|
||||||
|
if targetType != model.ReportTypeTexture && targetType != model.ReportTypeUser {
|
||||||
|
return nil, errors.New("无效的举报类型")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否重复举报
|
||||||
|
isDuplicate, err := s.reportRepo.CheckDuplicate(ctx, reporterID, targetType, targetID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("检查重复举报失败", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if isDuplicate {
|
||||||
|
return nil, errors.New("您已经举报过该对象,请勿重复举报")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建举报记录
|
||||||
|
report := &model.Report{
|
||||||
|
ReporterID: reporterID,
|
||||||
|
TargetType: targetType,
|
||||||
|
TargetID: targetID,
|
||||||
|
Reason: reason,
|
||||||
|
Status: model.ReportStatusPending,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.reportRepo.Create(ctx, report); err != nil {
|
||||||
|
s.logger.Error("创建举报失败", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("创建举报成功", zap.Int64("report_id", report.ID), zap.Int64("reporter_id", reporterID))
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID 根据ID查询举报
|
||||||
|
func (s *reportService) GetByID(ctx context.Context, id int64) (*model.Report, error) {
|
||||||
|
report, err := s.reportRepo.FindByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("查询举报失败", zap.Int64("report_id", id), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByReporterID 根据举报人ID查询举报记录
|
||||||
|
func (s *reportService) GetByReporterID(ctx context.Context, reporterID, userID int64, page, pageSize int) ([]*model.Report, int64, error) {
|
||||||
|
// 验证用户存在
|
||||||
|
user, err := s.userRepo.FindByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return nil, 0, apperrors.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有本人或管理员可以查看自己的举报记录
|
||||||
|
if reporterID != userID && !(user.Role == "admin") {
|
||||||
|
return nil, 0, errors.New("无权查看其他用户的举报记录")
|
||||||
|
}
|
||||||
|
|
||||||
|
reports, total, err := s.reportRepo.FindByReporterID(ctx, reporterID, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("查询举报记录失败", zap.Error(err))
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return reports, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByTarget 根据目标对象查询举报记录
|
||||||
|
func (s *reportService) GetByTarget(ctx context.Context, targetType model.ReportType, targetID, userID int64, page, pageSize int) ([]*model.Report, int64, error) {
|
||||||
|
// 验证用户存在
|
||||||
|
user, err := s.userRepo.FindByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return nil, 0, apperrors.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有管理员可以查看目标对象的举报记录
|
||||||
|
if !(user.Role == "admin") {
|
||||||
|
return nil, 0, errors.New("无权查看举报记录")
|
||||||
|
}
|
||||||
|
|
||||||
|
reports, total, err := s.reportRepo.FindByTarget(ctx, targetType, targetID, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("查询举报记录失败", zap.Error(err))
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return reports, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByStatus 根据状态查询举报记录
|
||||||
|
func (s *reportService) GetByStatus(ctx context.Context, status model.ReportStatus, page, pageSize int) ([]*model.Report, int64, error) {
|
||||||
|
reports, total, err := s.reportRepo.FindByStatus(ctx, status, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("查询举报记录失败", zap.Error(err))
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return reports, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search 搜索举报记录
|
||||||
|
func (s *reportService) Search(ctx context.Context, keyword, userID int64, page, pageSize int) ([]*model.Report, int64, error) {
|
||||||
|
// 验证用户存在
|
||||||
|
user, err := s.userRepo.FindByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return nil, 0, apperrors.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有管理员可以搜索举报记录
|
||||||
|
if !(user.Role == "admin") {
|
||||||
|
return nil, 0, errors.New("无权搜索举报记录")
|
||||||
|
}
|
||||||
|
|
||||||
|
reports, total, err := s.reportRepo.Search(ctx, strconv.FormatInt(keyword, 10), page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("搜索举报记录失败", zap.Error(err))
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return reports, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Review 处理举报记录
|
||||||
|
func (s *reportService) Review(ctx context.Context, reportID, reviewerID int64, status model.ReportStatus, reviewNote string) (*model.Report, error) {
|
||||||
|
// 验证处理人存在且是管理员
|
||||||
|
reviewer, err := s.userRepo.FindByID(ctx, reviewerID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("处理人不存在", zap.Int64("reviewer_id", reviewerID), zap.Error(err))
|
||||||
|
return nil, apperrors.ErrUserNotFound
|
||||||
|
}
|
||||||
|
if reviewer == nil || !(reviewer.Role == "admin") {
|
||||||
|
return nil, errors.New("只有管理员可以处理举报")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证状态
|
||||||
|
if status != model.ReportStatusApproved && status != model.ReportStatusRejected {
|
||||||
|
return nil, errors.New("无效的举报处理状态")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理举报
|
||||||
|
if err := s.reportRepo.Review(ctx, reportID, status, reviewerID, reviewNote); err != nil {
|
||||||
|
s.logger.Error("处理举报失败", zap.Int64("report_id", reportID), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回更新后的举报记录
|
||||||
|
report, err := s.reportRepo.FindByID(ctx, reportID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("查询举报记录失败", zap.Int64("report_id", reportID), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("处理举报成功", zap.Int64("report_id", reportID), zap.Int64("reviewer_id", reviewerID), zap.String("status", string(status)))
|
||||||
|
return report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchReview 批量处理举报记录
|
||||||
|
func (s *reportService) BatchReview(ctx context.Context, ids []int64, reviewerID int64, status model.ReportStatus, reviewNote string) (int64, error) {
|
||||||
|
// 验证处理人存在且是管理员
|
||||||
|
reviewer, err := s.userRepo.FindByID(ctx, reviewerID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("处理人不存在", zap.Int64("reviewer_id", reviewerID), zap.Error(err))
|
||||||
|
return 0, apperrors.ErrUserNotFound
|
||||||
|
}
|
||||||
|
if reviewer == nil || !(reviewer.Role == "admin") {
|
||||||
|
return 0, errors.New("只有管理员可以处理举报")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证状态
|
||||||
|
if status != model.ReportStatusApproved && status != model.ReportStatusRejected {
|
||||||
|
return 0, errors.New("无效的举报处理状态")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量处理举报
|
||||||
|
affected, err := s.reportRepo.BatchReview(ctx, ids, status, reviewerID, reviewNote)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("批量处理举报失败", zap.Error(err))
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("批量处理举报成功", zap.Int("count", int(affected)), zap.Int64("reviewer_id", reviewerID), zap.String("status", string(status)))
|
||||||
|
return affected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除举报记录
|
||||||
|
func (s *reportService) Delete(ctx context.Context, reportID, userID int64) error {
|
||||||
|
// 验证用户存在
|
||||||
|
user, err := s.userRepo.FindByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return apperrors.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询举报记录
|
||||||
|
report, err := s.reportRepo.FindByID(ctx, reportID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if report == nil {
|
||||||
|
return errors.New("举报记录不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有举报人、管理员或处理人可以删除举报记录
|
||||||
|
if report.ReporterID != userID && !(user.Role == "admin") && (report.ReviewerID == nil || *report.ReviewerID != userID) {
|
||||||
|
return errors.New("无权删除此举报记录")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.reportRepo.Delete(ctx, reportID); err != nil {
|
||||||
|
s.logger.Error("删除举报记录失败", zap.Int64("report_id", reportID), zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("删除举报记录成功", zap.Int64("report_id", reportID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchDelete 批量删除举报记录
|
||||||
|
func (s *reportService) BatchDelete(ctx context.Context, ids []int64, userID int64) (int64, error) {
|
||||||
|
// 验证用户存在
|
||||||
|
user, err := s.userRepo.FindByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return 0, apperrors.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有管理员可以批量删除
|
||||||
|
if !(user.Role == "admin") {
|
||||||
|
return 0, errors.New("无权批量删除举报记录")
|
||||||
|
}
|
||||||
|
|
||||||
|
affected, err := s.reportRepo.BatchDelete(ctx, ids)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("批量删除举报记录失败", zap.Error(err))
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("批量删除举报记录成功", zap.Int("count", int(affected)))
|
||||||
|
return affected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStats 获取举报统计信息
|
||||||
|
func (s *reportService) GetStats(ctx context.Context) (map[string]int64, error) {
|
||||||
|
stats := make(map[string]int64)
|
||||||
|
|
||||||
|
// 统计各状态的举报数量
|
||||||
|
pendingCount, err := s.reportRepo.CountByStatus(ctx, model.ReportStatusPending)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stats["pending"] = pendingCount
|
||||||
|
|
||||||
|
approvedCount, err := s.reportRepo.CountByStatus(ctx, model.ReportStatusApproved)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stats["approved"] = approvedCount
|
||||||
|
|
||||||
|
rejectedCount, err := s.reportRepo.CountByStatus(ctx, model.ReportStatusRejected)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stats["rejected"] = rejectedCount
|
||||||
|
|
||||||
|
stats["total"] = pendingCount + approvedCount + rejectedCount
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
@@ -219,39 +219,22 @@ func (s *textureService) Delete(ctx context.Context, textureID, uploaderID int64
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *textureService) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
|
func (s *textureService) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
|
||||||
// 确保材质存在
|
|
||||||
texture, err := s.textureRepo.FindByID(ctx, textureID)
|
texture, err := s.textureRepo.FindByID(ctx, textureID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if texture == nil {
|
if texture == nil || texture.Status != 1 || !texture.IsPublic {
|
||||||
return false, ErrTextureNotFound
|
return false, ErrTextureNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
isFavorited, err := s.textureRepo.IsFavorited(ctx, userID, textureID)
|
isAdded, err := s.textureRepo.ToggleFavorite(ctx, userID, textureID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if isFavorited {
|
s.cacheInv.BatchInvalidate(ctx, s.cacheKeys.UserFavoritesPattern(userID))
|
||||||
// 已收藏 -> 取消收藏
|
|
||||||
if err := s.textureRepo.RemoveFavorite(ctx, userID, textureID); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if err := s.textureRepo.DecrementFavoriteCount(ctx, textureID); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未收藏 -> 添加收藏
|
return isAdded, nil
|
||||||
if err := s.textureRepo.AddFavorite(ctx, userID, textureID); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if err := s.textureRepo.IncrementFavoriteCount(ctx, textureID); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *textureService) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
func (s *textureService) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"carrotskin/internal/model"
|
"carrotskin/internal/model"
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -564,7 +565,7 @@ func TestTextureServiceImpl_Create(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
// UploadTexture需要文件数据,这里创建一个简单的测试数据
|
// UploadTexture需要文件数据,这里创建一个简单的测试数据
|
||||||
fileData := []byte("fake png data for testing")
|
fileData := []byte(strings.Repeat("x", 512))
|
||||||
texture, err := textureService.UploadTexture(
|
texture, err := textureService.UploadTexture(
|
||||||
ctx,
|
ctx,
|
||||||
tt.uploaderID,
|
tt.uploaderID,
|
||||||
@@ -760,7 +761,7 @@ func TestTextureServiceImpl_FavoritesAndLimit(t *testing.T) {
|
|||||||
UploaderID: 1,
|
UploaderID: 1,
|
||||||
Name: "T",
|
Name: "T",
|
||||||
})
|
})
|
||||||
_ = textureRepo.AddFavorite(context.Background(), 1, i)
|
_, _ = textureRepo.ToggleFavorite(context.Background(), 1, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
|
|||||||
@@ -206,84 +206,3 @@ type SystemConfigResponse struct {
|
|||||||
MaxTexturesPerUser int `json:"max_textures_per_user" example:"100"`
|
MaxTexturesPerUser int `json:"max_textures_per_user" example:"100"`
|
||||||
MaxProfilesPerUser int `json:"max_profiles_per_user" example:"5"`
|
MaxProfilesPerUser int `json:"max_profiles_per_user" example:"5"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminUserSearchRequest 管理员用户搜索请求
|
|
||||||
// @Description 管理员搜索用户请求参数
|
|
||||||
type AdminUserSearchRequest struct {
|
|
||||||
PaginationRequest
|
|
||||||
Keyword string `json:"keyword" form:"keyword" example:"testuser"`
|
|
||||||
Role string `json:"role" form:"role" binding:"omitempty,oneof=user admin"`
|
|
||||||
Status *int16 `json:"status" form:"status" binding:"omitempty,oneof=1 0 -1"`
|
|
||||||
SortBy string `json:"sort_by" form:"sort_by" binding:"omitempty,oneof=id username email points created_at"`
|
|
||||||
SortDesc bool `json:"sort_desc" form:"sort_desc"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdminUserCreateRequest 管理员创建用户请求
|
|
||||||
// @Description 管理员创建用户请求参数
|
|
||||||
type AdminUserCreateRequest struct {
|
|
||||||
Username string `json:"username" binding:"required,min=3,max=50" example:"newuser"`
|
|
||||||
Email string `json:"email" binding:"required,email" example:"user@example.com"`
|
|
||||||
Password string `json:"password" binding:"required,min=6,max=128" example:"password123"`
|
|
||||||
Role string `json:"role" binding:"required,oneof=user admin" example:"user"`
|
|
||||||
Points int `json:"points" binding:"omitempty,min=0" example:"0"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdminUserUpdateRequest 管理员更新用户请求
|
|
||||||
// @Description 管理员更新用户请求参数
|
|
||||||
type AdminUserUpdateRequest struct {
|
|
||||||
Username *string `json:"username" binding:"omitempty,min=3,max=50" example:"newusername"`
|
|
||||||
Email *string `json:"email" binding:"omitempty,email" example:"newemail@example.com"`
|
|
||||||
Password *string `json:"password" binding:"omitempty,min=6,max=128" example:"newpassword"`
|
|
||||||
Role *string `json:"role" binding:"omitempty,oneof=user admin" example:"admin"`
|
|
||||||
Points *int `json:"points" binding:"omitempty,min=0" example:"100"`
|
|
||||||
Status *int16 `json:"status" binding:"omitempty,oneof=1 0 -1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdminTextureSearchRequest 管理员材质搜索请求
|
|
||||||
// @Description 管理员搜索材质请求参数
|
|
||||||
type AdminTextureSearchRequest struct {
|
|
||||||
PaginationRequest
|
|
||||||
Keyword string `json:"keyword" form:"keyword" example:"skin"`
|
|
||||||
Type TextureType `json:"type" form:"type" binding:"omitempty,oneof=SKIN CAPE"`
|
|
||||||
Status *int16 `json:"status" form:"status" binding:"omitempty,oneof=1 0 -1"`
|
|
||||||
UploaderID *int64 `json:"uploader_id" form:"uploader_id" example:"1"`
|
|
||||||
SortBy string `json:"sort_by" form:"sort_by" binding:"omitempty,oneof=id name download_count favorite_count created_at"`
|
|
||||||
SortDesc bool `json:"sort_desc" form:"sort_desc"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdminTextureUpdateRequest 管理员更新材质请求
|
|
||||||
// @Description 管理员更新材质请求参数
|
|
||||||
type AdminTextureUpdateRequest struct {
|
|
||||||
Name *string `json:"name" binding:"omitempty,min=1,max=100" example:"New Skin Name"`
|
|
||||||
Description *string `json:"description" binding:"omitempty,max=500" example:"New description"`
|
|
||||||
IsPublic *bool `json:"is_public" example:"true"`
|
|
||||||
Status *int16 `json:"status" binding:"omitempty,oneof=1 0 -1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdminRoleListResponse 角色列表响应
|
|
||||||
// @Description 角色列表响应数据
|
|
||||||
type AdminRoleListResponse struct {
|
|
||||||
Roles []RoleInfo `json:"roles"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoleInfo 角色信息
|
|
||||||
// @Description 角色详细信息
|
|
||||||
type RoleInfo struct {
|
|
||||||
Name string `json:"name" example:"admin"`
|
|
||||||
DisplayName string `json:"display_name" example:"管理员"`
|
|
||||||
Description string `json:"description" example:"拥有所有管理权限"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdminStatsResponse 管理员统计信息响应
|
|
||||||
// @Description 管理员统计信息
|
|
||||||
type AdminStatsResponse struct {
|
|
||||||
TotalUsers int64 `json:"total_users" example:"100"`
|
|
||||||
ActiveUsers int64 `json:"active_users" example:"80"`
|
|
||||||
BannedUsers int64 `json:"banned_users" example:"5"`
|
|
||||||
AdminUsers int64 `json:"admin_users" example:"3"`
|
|
||||||
TotalTextures int64 `json:"total_textures" example:"500"`
|
|
||||||
PublicTextures int64 `json:"public_textures" example:"300"`
|
|
||||||
PendingTextures int64 `json:"pending_textures" example:"10"`
|
|
||||||
TotalDownloads int64 `json:"total_downloads" example:"1000"`
|
|
||||||
TotalFavorites int64 `json:"total_favorites" example:"500"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -369,6 +369,11 @@ func (b *CacheKeyBuilder) ProfilePattern(userID int64) string {
|
|||||||
return fmt.Sprintf("%sprofile:*:%d*", b.prefix, userID)
|
return fmt.Sprintf("%sprofile:*:%d*", b.prefix, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserFavoritesPattern 用户收藏相关的所有缓存键模式
|
||||||
|
func (b *CacheKeyBuilder) UserFavoritesPattern(userID int64) string {
|
||||||
|
return fmt.Sprintf("%sfavorites:*:%d*", b.prefix, userID)
|
||||||
|
}
|
||||||
|
|
||||||
// Exists 检查缓存键是否存在
|
// Exists 检查缓存键是否存在
|
||||||
func (cm *CacheManager) Exists(ctx context.Context, key string) (bool, error) {
|
func (cm *CacheManager) Exists(ctx context.Context, key string) (bool, error) {
|
||||||
if !cm.config.Enabled || cm.redis == nil {
|
if !cm.config.Enabled || cm.redis == nil {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"html"
|
|
||||||
|
|
||||||
"carrotskin/pkg/config"
|
"carrotskin/pkg/config"
|
||||||
|
|
||||||
@@ -71,6 +70,8 @@ func (s *Service) send(to []string, subject, body string) error {
|
|||||||
addr := fmt.Sprintf("%s:%d", s.cfg.SMTPHost, s.cfg.SMTPPort)
|
addr := fmt.Sprintf("%s:%d", s.cfg.SMTPHost, s.cfg.SMTPPort)
|
||||||
|
|
||||||
// 判断端口决定发送方式
|
// 判断端口决定发送方式
|
||||||
|
// 465端口使用SSL/TLS(隐式TLS)
|
||||||
|
// 587端口使用STARTTLS(显式TLS)
|
||||||
var err error
|
var err error
|
||||||
if s.cfg.SMTPPort == 465 {
|
if s.cfg.SMTPPort == 465 {
|
||||||
// 使用SSL/TLS连接(适用于465端口)
|
// 使用SSL/TLS连接(适用于465端口)
|
||||||
@@ -131,10 +132,6 @@ func (s *Service) getBody(code, purpose string) string {
|
|||||||
message = "您的验证码为:"
|
message = "您的验证码为:"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转义 HTML 特殊字符
|
|
||||||
escapedMessage := html.EscapeString(message)
|
|
||||||
escapedCode := html.EscapeString(code)
|
|
||||||
|
|
||||||
return fmt.Sprintf(`
|
return fmt.Sprintf(`
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@@ -161,5 +158,5 @@ func (s *Service) getBody(code, purpose string) string {
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`, escapedMessage, escapedCode)
|
`, message, code)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user