Compare commits
18 Commits
d952ddd4ea
...
yongye
| Author | SHA1 | Date | |
|---|---|---|---|
| 52b61be822 | |||
| 17a2792ac4 | |||
| 68d7318285 | |||
| 432b875ba4 | |||
| 116612ffec | |||
|
|
3e8b7d150d | ||
|
|
fd5a0e8405 | ||
|
|
573c10ed1d | ||
|
|
3b8d8bd7a7 | ||
|
|
6338592d27 | ||
|
|
ef460ec891 | ||
|
|
62d9432a2d | ||
|
|
e1d79ed445 | ||
|
|
c5d7e317a4 | ||
|
|
06539dc086 | ||
|
|
22142db782 | ||
|
|
2c9c6ecfc0 | ||
|
|
c5db489d72 |
73
.gitea/workflows/build.yml
Normal file
73
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,73 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
cache: false
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
CGO_ENABLED: 0
|
||||
run: go build -v -o mcauth-linux-amd64 ./cmd/server
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: mcauth-linux-amd64
|
||||
path: mcauth-linux-amd64
|
||||
|
||||
build-docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: code.littlelan.cn
|
||||
username: ${{ secrets.GIT_USERNAME }}
|
||||
password: ${{ secrets.GIT_TOKEN }}
|
||||
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: mcauth-linux-amd64
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
code.littlelan.cn/carrotskin/mcauth:latest
|
||||
code.littlelan.cn/carrotskin/mcauth:${{ github.sha }}
|
||||
platforms: linux/amd64
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -60,7 +60,7 @@ configs/config.yaml
|
||||
.env.production
|
||||
|
||||
# Keep example files
|
||||
!.env.example
|
||||
!.env
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
|
||||
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
# 运行阶段
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装必要的运行时依赖
|
||||
RUN apk add --no-cache ca-certificates tzdata wget
|
||||
|
||||
# 创建非 root 用户
|
||||
RUN addgroup -g 1000 appuser && \
|
||||
adduser -D -u 1000 -G appuser appuser
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制已经编译好的二进制文件
|
||||
ARG BINARY_NAME=mcauth-linux-amd64
|
||||
COPY ${BINARY_NAME} /app/server
|
||||
|
||||
# 复制配置文件(如果需要)
|
||||
COPY configs/ /app/configs/
|
||||
|
||||
# 设置权限
|
||||
RUN chown -R appuser:appuser /app
|
||||
|
||||
# 切换到非 root 用户
|
||||
USER appuser
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
# 启动应用
|
||||
ENTRYPOINT ["/app/server"]
|
||||
@@ -72,7 +72,7 @@ backend/
|
||||
|
||||
3. **配置环境变量**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
cp .env .env
|
||||
# 根据实际环境填写数据库、Redis、对象存储、邮件等信息
|
||||
```
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
|
||||
"carrotskin/internal/container"
|
||||
"carrotskin/internal/handler"
|
||||
@@ -32,7 +33,6 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
// _ "carrotskin/docs" // Swagger docs
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -32,6 +32,7 @@ type Container struct {
|
||||
TextureRepo repository.TextureRepository
|
||||
ClientRepo repository.ClientRepository
|
||||
YggdrasilRepo repository.YggdrasilRepository
|
||||
ReportRepo repository.ReportRepository
|
||||
|
||||
// Service层
|
||||
UserService service.UserService
|
||||
@@ -43,6 +44,7 @@ type Container struct {
|
||||
SecurityService service.SecurityService
|
||||
CaptchaService service.CaptchaService
|
||||
SignatureService *service.SignatureService
|
||||
ReportService service.ReportService
|
||||
}
|
||||
|
||||
// NewContainer 创建依赖容器
|
||||
@@ -86,6 +88,7 @@ func NewContainer(
|
||||
c.TextureRepo = repository.NewTextureRepository(db)
|
||||
c.ClientRepo = repository.NewClientRepository(db)
|
||||
c.YggdrasilRepo = repository.NewYggdrasilRepository(db)
|
||||
c.ReportRepo = repository.NewReportRepository(db)
|
||||
|
||||
// 初始化SignatureService(作为依赖注入,避免在容器中创建并立即调用)
|
||||
// 将SignatureService添加到容器中,供其他服务使用
|
||||
@@ -95,6 +98,7 @@ func NewContainer(
|
||||
c.UserService = service.NewUserService(c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger)
|
||||
c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, 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需要)
|
||||
// 注意:这里仍然需要预先初始化,因为TokenService在创建时需要YggdrasilJWT
|
||||
|
||||
@@ -62,6 +62,19 @@ func UserToUserInfo(user *model.User) *types.UserInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// UserToPublicUserInfo 将 User 模型转换为 PublicUserInfo 响应
|
||||
func UserToPublicUserInfo(user *model.User) *types.PublicUserInfo {
|
||||
return &types.PublicUserInfo{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Avatar: user.Avatar,
|
||||
Points: user.Points,
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
CreatedAt: user.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ProfileToProfileInfo 将 Profile 模型转换为 ProfileInfo 响应
|
||||
func ProfileToProfileInfo(profile *model.Profile) *types.ProfileInfo {
|
||||
return &types.ProfileInfo{
|
||||
@@ -87,22 +100,28 @@ func ProfilesToProfileInfos(profiles []*model.Profile) []*types.ProfileInfo {
|
||||
|
||||
// TextureToTextureInfo 将 Texture 模型转换为 TextureInfo 响应
|
||||
func TextureToTextureInfo(texture *model.Texture) *types.TextureInfo {
|
||||
uploaderUsername := ""
|
||||
if texture.Uploader != nil {
|
||||
uploaderUsername = texture.Uploader.Username
|
||||
}
|
||||
|
||||
return &types.TextureInfo{
|
||||
ID: texture.ID,
|
||||
UploaderID: texture.UploaderID,
|
||||
Name: texture.Name,
|
||||
Description: texture.Description,
|
||||
Type: types.TextureType(texture.Type),
|
||||
URL: texture.URL,
|
||||
Hash: texture.Hash,
|
||||
Size: texture.Size,
|
||||
IsPublic: texture.IsPublic,
|
||||
DownloadCount: texture.DownloadCount,
|
||||
FavoriteCount: texture.FavoriteCount,
|
||||
IsSlim: texture.IsSlim,
|
||||
Status: texture.Status,
|
||||
CreatedAt: texture.CreatedAt,
|
||||
UpdatedAt: texture.UpdatedAt,
|
||||
ID: texture.ID,
|
||||
UploaderID: texture.UploaderID,
|
||||
UploaderUsername: uploaderUsername,
|
||||
Name: texture.Name,
|
||||
Description: texture.Description,
|
||||
Type: types.TextureType(texture.Type),
|
||||
URL: texture.URL,
|
||||
Hash: texture.Hash,
|
||||
Size: texture.Size,
|
||||
IsPublic: texture.IsPublic,
|
||||
DownloadCount: texture.DownloadCount,
|
||||
FavoriteCount: texture.FavoriteCount,
|
||||
IsSlim: texture.IsSlim,
|
||||
Status: texture.Status,
|
||||
CreatedAt: texture.CreatedAt,
|
||||
UpdatedAt: texture.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
CustomSkin *CustomSkinHandler
|
||||
Admin *AdminHandler
|
||||
Report *ReportHandler
|
||||
}
|
||||
|
||||
// NewHandlers 创建所有Handler实例
|
||||
@@ -34,6 +35,7 @@ func NewHandlers(c *container.Container) *Handlers {
|
||||
Yggdrasil: NewYggdrasilHandler(c),
|
||||
CustomSkin: NewCustomSkinHandler(c),
|
||||
Admin: NewAdminHandler(c),
|
||||
Report: NewReportHandler(c),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +79,9 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
|
||||
|
||||
// 管理员路由(需要管理员权限)
|
||||
registerAdminRoutes(v1, c, h.Admin)
|
||||
|
||||
// 举报路由
|
||||
registerReportRoutes(v1, h.Report, c.JWT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +98,10 @@ func registerAuthRoutes(v1 *gin.RouterGroup, h *AuthHandler) {
|
||||
|
||||
// registerUserRoutes 注册用户路由
|
||||
func registerUserRoutes(v1 *gin.RouterGroup, h *UserHandler, jwtService *auth.JWTService) {
|
||||
// 公开用户信息路由(无需认证)
|
||||
v1.GET("/users/public", h.GetPublicInfo)
|
||||
|
||||
// 需要认证的用户路由
|
||||
userGroup := v1.Group("/user")
|
||||
userGroup.Use(middleware.AuthMiddleware(jwtService))
|
||||
{
|
||||
@@ -232,3 +241,28 @@ func registerCustomSkinRoutes(v1 *gin.RouterGroup, h *CustomSkinHandler) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"carrotskin/internal/container"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/service"
|
||||
"carrotskin/internal/types"
|
||||
|
||||
@@ -315,3 +316,55 @@ func (h *UserHandler) ResetYggdrasilPassword(c *gin.Context) {
|
||||
h.logger.Info("Yggdrasil密码重置成功", zap.Int64("userId", userID))
|
||||
RespondSuccess(c, gin.H{"password": newPassword})
|
||||
}
|
||||
|
||||
// GetPublicInfo 获取用户公开信息
|
||||
// @Summary 获取用户公开信息
|
||||
// @Description 根据用户名或用户ID获取用户的公开信息(不包含敏感信息如邮箱)
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param username query string false "用户名"
|
||||
// @Param id query int false "用户ID"
|
||||
// @Success 200 {object} model.Response{data=types.PublicUserInfo} "获取成功"
|
||||
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
||||
// @Failure 404 {object} model.ErrorResponse "用户不存在"
|
||||
// @Router /api/v1/users/public [get]
|
||||
func (h *UserHandler) GetPublicInfo(c *gin.Context) {
|
||||
username := c.Query("username")
|
||||
idStr := c.Query("id")
|
||||
|
||||
// 至少需要提供一个参数
|
||||
if username == "" && idStr == "" {
|
||||
RespondBadRequest(c, "必须提供用户名或用户ID", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var user *model.User
|
||||
var err error
|
||||
|
||||
// 优先使用用户名查询
|
||||
if username != "" {
|
||||
user, err = h.container.UserService.GetByUsername(c.Request.Context(), username)
|
||||
} else {
|
||||
// 使用用户ID查询
|
||||
id := parseIntWithDefault(idStr, 0)
|
||||
if id == 0 {
|
||||
RespondBadRequest(c, "无效的用户ID", nil)
|
||||
return
|
||||
}
|
||||
user, err = h.container.UserService.GetByID(c.Request.Context(), int64(id))
|
||||
}
|
||||
|
||||
if err != nil || user == nil {
|
||||
RespondNotFound(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if user.Status != 1 {
|
||||
RespondNotFound(c, "用户不可用")
|
||||
return
|
||||
}
|
||||
|
||||
RespondSuccess(c, UserToPublicUserInfo(user))
|
||||
}
|
||||
|
||||
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
|
||||
BatchDelete(ctx context.Context, ids []int64) (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
|
||||
IsFavorited(ctx context.Context, userID, textureID int64) (bool, error)
|
||||
AddFavorite(ctx context.Context, userID, textureID int64) error
|
||||
RemoveFavorite(ctx context.Context, userID, textureID int64) error
|
||||
ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error)
|
||||
GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
||||
CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error)
|
||||
}
|
||||
|
||||
|
||||
// YggdrasilRepository Yggdrasil仓储接口
|
||||
type YggdrasilRepository interface {
|
||||
GetPasswordByID(ctx context.Context, id int64) (string, error)
|
||||
@@ -84,3 +79,21 @@ type ClientRepository interface {
|
||||
DeleteByClientToken(ctx context.Context, clientToken string) 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)
|
||||
}
|
||||
|
||||
|
||||
if err := profileRepo.UpdateLastUsedAt(ctx, "p-uuid"); err != nil {
|
||||
t.Fatalf("UpdateLastUsedAt err: %v", err)
|
||||
}
|
||||
@@ -150,22 +149,20 @@ func TestTextureRepository_Basic(t *testing.T) {
|
||||
t.Fatalf("FindByHashAndUploaderID mismatch")
|
||||
}
|
||||
|
||||
_ = textureRepo.IncrementFavoriteCount(ctx, tex.ID)
|
||||
_ = textureRepo.DecrementFavoriteCount(ctx, tex.ID)
|
||||
_, _ = textureRepo.ToggleFavorite(ctx, u.ID, 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.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 {
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
func TestClientRepository_Basic(t *testing.T) {
|
||||
db := testutil.NewTestDB(t)
|
||||
repo := NewClientRepository(db)
|
||||
|
||||
@@ -29,13 +29,13 @@ func (r *textureRepository) FindByID(ctx context.Context, id int64) (*model.Text
|
||||
|
||||
func (r *textureRepository) FindByHash(ctx context.Context, hash string) (*model.Texture, error) {
|
||||
var texture model.Texture
|
||||
err := r.db.WithContext(ctx).Where("hash = ?", hash).First(&texture).Error
|
||||
err := r.db.WithContext(ctx).Preload("Uploader").Where("hash = ?", hash).First(&texture).Error
|
||||
return handleNotFoundResult(&texture, err)
|
||||
}
|
||||
|
||||
func (r *textureRepository) FindByHashAndUploaderID(ctx context.Context, hash string, uploaderID int64) (*model.Texture, error) {
|
||||
var texture model.Texture
|
||||
err := r.db.WithContext(ctx).Where("hash = ? AND uploader_id = ?", hash, uploaderID).First(&texture).Error
|
||||
err := r.db.WithContext(ctx).Preload("Uploader").Where("hash = ? AND uploader_id = ?", hash, uploaderID).First(&texture).Error
|
||||
return handleNotFoundResult(&texture, err)
|
||||
}
|
||||
|
||||
@@ -138,42 +138,52 @@ func (r *textureRepository) IncrementDownloadCount(ctx context.Context, id int64
|
||||
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 {
|
||||
return r.db.WithContext(ctx).Create(log).Error
|
||||
}
|
||||
|
||||
func (r *textureRepository) IsFavorited(ctx context.Context, userID, textureID int64) (bool, error) {
|
||||
var count int64
|
||||
// 使用 Select("1") 优化,只查询是否存在,不需要查询所有字段
|
||||
err := r.db.WithContext(ctx).Model(&model.UserTextureFavorite{}).
|
||||
Select("1").
|
||||
Where("user_id = ? AND texture_id = ?", userID, textureID).
|
||||
Limit(1).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
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
|
||||
err := tx.Model(&model.UserTextureFavorite{}).
|
||||
Where("user_id = ? AND texture_id = ?", userID, textureID).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *textureRepository) AddFavorite(ctx context.Context, userID, textureID int64) error {
|
||||
favorite := &model.UserTextureFavorite{
|
||||
UserID: userID,
|
||||
TextureID: textureID,
|
||||
}
|
||||
return r.db.WithContext(ctx).Create(favorite).Error
|
||||
}
|
||||
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) RemoveFavorite(ctx context.Context, userID, textureID int64) error {
|
||||
return r.db.WithContext(ctx).Where("user_id = ? AND texture_id = ?", userID, textureID).
|
||||
Delete(&model.UserTextureFavorite{}).Error
|
||||
favorite := &model.UserTextureFavorite{
|
||||
UserID: userID,
|
||||
TextureID: textureID,
|
||||
}
|
||||
if err := tx.Create(favorite).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.Texture{}).Where("id = ?", textureID).
|
||||
UpdateColumn("favorite_count", gorm.Expr("favorite_count + 1")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
isAdded = true
|
||||
return nil
|
||||
})
|
||||
return isAdded, err
|
||||
}
|
||||
|
||||
func (r *textureRepository) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||
|
||||
@@ -19,6 +19,7 @@ type UserService interface {
|
||||
// 用户查询
|
||||
GetByID(ctx context.Context, id int64) (*model.User, error)
|
||||
GetByEmail(ctx context.Context, email string) (*model.User, error)
|
||||
GetByUsername(ctx context.Context, username string) (*model.User, error)
|
||||
|
||||
// 用户更新
|
||||
UpdateInfo(ctx context.Context, user *model.User) error
|
||||
@@ -136,6 +137,30 @@ type SecurityService interface {
|
||||
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 服务集合
|
||||
type Services struct {
|
||||
User UserService
|
||||
@@ -146,6 +171,7 @@ type Services struct {
|
||||
Captcha CaptchaService
|
||||
Yggdrasil YggdrasilService
|
||||
Security SecurityService
|
||||
Report ReportService
|
||||
}
|
||||
|
||||
// ServiceDeps 服务依赖
|
||||
|
||||
@@ -391,37 +391,24 @@ func (m *MockTextureRepository) IncrementFavoriteCount(ctx context.Context, id i
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockTextureRepository) IsFavorited(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 {
|
||||
func (m *MockTextureRepository) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
|
||||
if m.favorites[userID] == nil {
|
||||
m.favorites[userID] = make(map[int64]bool)
|
||||
}
|
||||
m.favorites[userID][textureID] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockTextureRepository) RemoveFavorite(ctx context.Context, userID, textureID int64) error {
|
||||
if userFavs, ok := m.favorites[userID]; ok {
|
||||
delete(userFavs, textureID)
|
||||
isFavorited := m.favorites[userID][textureID]
|
||||
m.favorites[userID][textureID] = !isFavorited
|
||||
if texture, ok := m.textures[textureID]; ok {
|
||||
if !isFavorited {
|
||||
texture.FavoriteCount++
|
||||
} else if texture.FavoriteCount > 0 {
|
||||
texture.FavoriteCount--
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return !isFavorited, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// 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
|
||||
}
|
||||
@@ -55,6 +55,22 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture,
|
||||
if texture.Status == -1 {
|
||||
return nil, errors.New("材质已删除")
|
||||
}
|
||||
// 如果缓存中没有 Uploader 信息,重新查询数据库
|
||||
if texture.Uploader == nil {
|
||||
texture2, err := s.textureRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if texture2 == nil {
|
||||
return nil, ErrTextureNotFound
|
||||
}
|
||||
if texture2.Status == -1 {
|
||||
return nil, errors.New("材质已删除")
|
||||
}
|
||||
// 更新缓存
|
||||
s.cache.SetAsync(context.Background(), cacheKey, texture2, s.cache.Policy.TextureTTL)
|
||||
return texture2, nil
|
||||
}
|
||||
return &texture, nil
|
||||
}
|
||||
|
||||
@@ -71,9 +87,7 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture,
|
||||
}
|
||||
|
||||
// 存入缓存(异步)
|
||||
if texture2 != nil {
|
||||
s.cache.SetAsync(context.Background(), cacheKey, texture2, s.cache.Policy.TextureTTL)
|
||||
}
|
||||
s.cache.SetAsync(context.Background(), cacheKey, texture2, s.cache.Policy.TextureTTL)
|
||||
|
||||
return texture2, nil
|
||||
}
|
||||
@@ -205,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) {
|
||||
// 确保材质存在
|
||||
texture, err := s.textureRepo.FindByID(ctx, textureID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if texture == nil {
|
||||
if texture == nil || texture.Status != 1 || !texture.IsPublic {
|
||||
return false, ErrTextureNotFound
|
||||
}
|
||||
|
||||
isFavorited, err := s.textureRepo.IsFavorited(ctx, userID, textureID)
|
||||
isAdded, err := s.textureRepo.ToggleFavorite(ctx, userID, textureID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if isFavorited {
|
||||
// 已收藏 -> 取消收藏
|
||||
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
|
||||
}
|
||||
s.cacheInv.BatchInvalidate(ctx, s.cacheKeys.UserFavoritesPattern(userID))
|
||||
|
||||
// 未收藏 -> 添加收藏
|
||||
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
|
||||
return isAdded, nil
|
||||
}
|
||||
|
||||
func (s *textureService) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||
@@ -365,7 +362,17 @@ func (s *textureService) UploadTexture(ctx context.Context, uploaderID int64, na
|
||||
// 清除用户的 texture 列表缓存(所有分页)
|
||||
s.cacheInv.BatchInvalidate(ctx, fmt.Sprintf("texture:user:%d:*", uploaderID))
|
||||
|
||||
return texture, nil
|
||||
// 重新查询以预加载 Uploader 关联
|
||||
textureWithUploader, err := s.textureRepo.FindByID(ctx, texture.ID)
|
||||
if err != nil {
|
||||
// 如果查询失败,返回原始创建的 texture 对象(虽然可能没有 Uploader 信息)
|
||||
return texture, nil
|
||||
}
|
||||
if textureWithUploader == nil {
|
||||
// 如果查询返回 nil(极端情况,如数据库复制延迟),返回原始创建的 texture 对象
|
||||
return texture, nil
|
||||
}
|
||||
return textureWithUploader, nil
|
||||
}
|
||||
|
||||
// parseTextureTypeInternal 解析材质类型
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"carrotskin/internal/model"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -564,7 +565,7 @@ func TestTextureServiceImpl_Create(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
// UploadTexture需要文件数据,这里创建一个简单的测试数据
|
||||
fileData := []byte("fake png data for testing")
|
||||
fileData := []byte(strings.Repeat("x", 512))
|
||||
texture, err := textureService.UploadTexture(
|
||||
ctx,
|
||||
tt.uploaderID,
|
||||
@@ -760,7 +761,7 @@ func TestTextureServiceImpl_FavoritesAndLimit(t *testing.T) {
|
||||
UploaderID: 1,
|
||||
Name: "T",
|
||||
})
|
||||
_ = textureRepo.AddFavorite(context.Background(), 1, i)
|
||||
_, _ = textureRepo.ToggleFavorite(context.Background(), 1, i)
|
||||
}
|
||||
|
||||
cacheManager := NewMockCacheManager()
|
||||
|
||||
@@ -199,6 +199,14 @@ func (s *userService) GetByEmail(ctx context.Context, email string) (*model.User
|
||||
}, s.cache.Policy.UserEmailTTL)
|
||||
}
|
||||
|
||||
func (s *userService) GetByUsername(ctx context.Context, username string) (*model.User, error) {
|
||||
// 使用 Cached 装饰器自动处理缓存
|
||||
cacheKey := s.cacheKeys.UserByUsername(username)
|
||||
return database.Cached(ctx, s.cache, cacheKey, func() (*model.User, error) {
|
||||
return s.userRepo.FindByUsername(ctx, username)
|
||||
}, s.cache.Policy.UserTTL)
|
||||
}
|
||||
|
||||
func (s *userService) UpdateInfo(ctx context.Context, user *model.User) error {
|
||||
err := s.userRepo.Update(ctx, user)
|
||||
if err != nil {
|
||||
|
||||
@@ -110,6 +110,18 @@ type UserInfo struct {
|
||||
UpdatedAt time.Time `json:"updated_at" example:"2025-10-01T10:00:00Z"`
|
||||
}
|
||||
|
||||
// PublicUserInfo 用户公开信息
|
||||
// @Description 用户公开信息(不包含敏感信息如邮箱)
|
||||
type PublicUserInfo struct {
|
||||
ID int64 `json:"id" example:"1"`
|
||||
Username string `json:"username" example:"testuser"`
|
||||
Avatar string `json:"avatar" example:"https://example.com/avatar.png"`
|
||||
Points int `json:"points" example:"100"`
|
||||
Role string `json:"role" example:"user"`
|
||||
Status int16 `json:"status" example:"1"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2025-10-01T10:00:00Z"`
|
||||
}
|
||||
|
||||
// TextureType 材质类型
|
||||
type TextureType string
|
||||
|
||||
@@ -121,21 +133,22 @@ const (
|
||||
// TextureInfo 材质信息
|
||||
// @Description 材质详细信息
|
||||
type TextureInfo struct {
|
||||
ID int64 `json:"id" example:"1"`
|
||||
UploaderID int64 `json:"uploader_id" example:"1"`
|
||||
Name string `json:"name" example:"My Skin"`
|
||||
Description string `json:"description,omitempty" example:"A cool skin"`
|
||||
Type TextureType `json:"type" example:"SKIN"`
|
||||
URL string `json:"url" example:"https://rustfs.example.com/textures/xxx.png"`
|
||||
Hash string `json:"hash" example:"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"`
|
||||
Size int `json:"size" example:"2048"`
|
||||
IsPublic bool `json:"is_public" example:"true"`
|
||||
DownloadCount int `json:"download_count" example:"100"`
|
||||
FavoriteCount int `json:"favorite_count" example:"50"`
|
||||
IsSlim bool `json:"is_slim" example:"false"`
|
||||
Status int16 `json:"status" example:"1"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2025-10-01T10:00:00Z"`
|
||||
UpdatedAt time.Time `json:"updated_at" example:"2025-10-01T10:00:00Z"`
|
||||
ID int64 `json:"id" example:"1"`
|
||||
UploaderID int64 `json:"uploader_id" example:"1"`
|
||||
UploaderUsername string `json:"uploader_username" example:"testuser"`
|
||||
Name string `json:"name" example:"My Skin"`
|
||||
Description string `json:"description,omitempty" example:"A cool skin"`
|
||||
Type TextureType `json:"type" example:"SKIN"`
|
||||
URL string `json:"url" example:"https://rustfs.example.com/textures/xxx.png"`
|
||||
Hash string `json:"hash" example:"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"`
|
||||
Size int `json:"size" example:"2048"`
|
||||
IsPublic bool `json:"is_public" example:"true"`
|
||||
DownloadCount int `json:"download_count" example:"100"`
|
||||
FavoriteCount int `json:"favorite_count" example:"50"`
|
||||
IsSlim bool `json:"is_slim" example:"false"`
|
||||
Status int16 `json:"status" example:"1"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2025-10-01T10:00:00Z"`
|
||||
UpdatedAt time.Time `json:"updated_at" example:"2025-10-01T10:00:00Z"`
|
||||
}
|
||||
|
||||
// ProfileInfo 角色信息
|
||||
|
||||
@@ -140,8 +140,7 @@ func Load() (*Config, error) {
|
||||
// 设置默认值
|
||||
setDefaults()
|
||||
|
||||
// 设置环境变量前缀
|
||||
viper.SetEnvPrefix("CARROTSKIN")
|
||||
// 自动读取环境变量(不设置前缀,因为 BindEnv 已经明确指定了变量名)
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// 手动设置环境变量映射
|
||||
@@ -320,6 +319,7 @@ func setupEnvMappings() {
|
||||
|
||||
// overrideFromEnv 从环境变量中覆盖配置
|
||||
func overrideFromEnv(config *Config) {
|
||||
|
||||
// 处理RustFS存储桶配置
|
||||
if texturesBucket := os.Getenv("RUSTFS_BUCKET_TEXTURES"); texturesBucket != "" {
|
||||
if config.RustFS.Buckets == nil {
|
||||
@@ -360,6 +360,24 @@ func overrideFromEnv(config *Config) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理Redis基本配置
|
||||
if host := os.Getenv("REDIS_HOST"); host != "" {
|
||||
config.Redis.Host = host
|
||||
}
|
||||
if port := os.Getenv("REDIS_PORT"); port != "" {
|
||||
if val, err := strconv.Atoi(port); err == nil {
|
||||
config.Redis.Port = val
|
||||
}
|
||||
}
|
||||
if password := os.Getenv("REDIS_PASSWORD"); password != "" {
|
||||
config.Redis.Password = password
|
||||
}
|
||||
if database := os.Getenv("REDIS_DATABASE"); database != "" {
|
||||
if val, err := strconv.Atoi(database); err == nil {
|
||||
config.Redis.Database = val
|
||||
}
|
||||
}
|
||||
|
||||
// 处理Redis连接池配置
|
||||
if poolSize := os.Getenv("REDIS_POOL_SIZE"); poolSize != "" {
|
||||
if val, err := strconv.Atoi(poolSize); err == nil {
|
||||
|
||||
@@ -369,6 +369,11 @@ func (b *CacheKeyBuilder) ProfilePattern(userID int64) string {
|
||||
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 检查缓存键是否存在
|
||||
func (cm *CacheManager) Exists(ctx context.Context, key string) (bool, error) {
|
||||
if !cm.config.Enabled || cm.redis == nil {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
// "context"
|
||||
"testing"
|
||||
// "time"
|
||||
|
||||
"carrotskin/pkg/config"
|
||||
|
||||
@@ -41,4 +39,3 @@ func TestNewStorage_SkipConnectWhenNoCreds(t *testing.T) {
|
||||
t.Fatalf("NewStorage should not error when creds empty: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user