From 61e5cbf80dd493479f9814bac03270354544ca2d Mon Sep 17 00:00:00 2001 From: ctwj <908504609@qq.com> Date: Wed, 19 Nov 2025 02:22:04 +0800 Subject: [PATCH] add report --- db/connection.go | 2 + db/converter/copyright_claim_converter.go | 40 ++ db/converter/report_converter.go | 37 ++ db/dto/copyright_claim.go | 45 ++ db/dto/report.go | 39 ++ db/entity/copyright_claim.go | 32 ++ db/entity/report.go | 29 ++ db/repo/copyright_claim_repository.go | 87 ++++ db/repo/manager.go | 4 + db/repo/report_repository.go | 87 ++++ go.mod | 2 +- handlers/copyright_claim_handler.go | 278 ++++++++++++ handlers/report_handler.go | 276 ++++++++++++ main.go | 19 + web/components/CopyrightModal.vue | 52 ++- web/components/ReportModal.vue | 38 +- web/components/ShareButtons.vue | 234 +++++++--- web/composables/useApi.ts | 49 ++- web/layouts/admin.vue | 16 +- web/middleware/admin.ts | 31 ++ web/pages/admin/copyright-claims.vue | 510 ++++++++++++++++++++++ web/pages/admin/reports.vue | 483 ++++++++++++++++++++ web/pages/r/[key].vue | 65 ++- 23 files changed, 2334 insertions(+), 121 deletions(-) create mode 100644 db/converter/copyright_claim_converter.go create mode 100644 db/converter/report_converter.go create mode 100644 db/dto/copyright_claim.go create mode 100644 db/dto/report.go create mode 100644 db/entity/copyright_claim.go create mode 100644 db/entity/report.go create mode 100644 db/repo/copyright_claim_repository.go create mode 100644 db/repo/report_repository.go create mode 100644 handlers/copyright_claim_handler.go create mode 100644 handlers/report_handler.go create mode 100644 web/middleware/admin.ts create mode 100644 web/pages/admin/copyright-claims.vue create mode 100644 web/pages/admin/reports.vue diff --git a/db/connection.go b/db/connection.go index 2cdcec5..739c348 100644 --- a/db/connection.go +++ b/db/connection.go @@ -109,6 +109,8 @@ func InitDB() error { &entity.APIAccessLog{}, &entity.APIAccessLogStats{}, &entity.APIAccessLogSummary{}, + &entity.Report{}, + &entity.CopyrightClaim{}, ) if err != nil { utils.Fatal("数据库迁移失败: %v", err) diff --git a/db/converter/copyright_claim_converter.go b/db/converter/copyright_claim_converter.go new file mode 100644 index 0000000..6a457cb --- /dev/null +++ b/db/converter/copyright_claim_converter.go @@ -0,0 +1,40 @@ +package converter + +import ( + "github.com/ctwj/urldb/db/dto" + "github.com/ctwj/urldb/db/entity" + "time" +) + +// CopyrightClaimToResponse 将版权申述实体转换为响应对象 +func CopyrightClaimToResponse(claim *entity.CopyrightClaim) *dto.CopyrightClaimResponse { + if claim == nil { + return nil + } + + return &dto.CopyrightClaimResponse{ + ID: claim.ID, + ResourceKey: claim.ResourceKey, + Identity: claim.Identity, + ProofType: claim.ProofType, + Reason: claim.Reason, + ContactInfo: claim.ContactInfo, + ClaimantName: claim.ClaimantName, + ProofFiles: claim.ProofFiles, + UserAgent: claim.UserAgent, + IPAddress: claim.IPAddress, + Status: claim.Status, + Note: claim.Note, + CreatedAt: claim.CreatedAt.Format(time.RFC3339), + UpdatedAt: claim.UpdatedAt.Format(time.RFC3339), + } +} + +// CopyrightClaimsToResponse 将版权申述实体列表转换为响应对象列表 +func CopyrightClaimsToResponse(claims []*entity.CopyrightClaim) []*dto.CopyrightClaimResponse { + var responses []*dto.CopyrightClaimResponse + for _, claim := range claims { + responses = append(responses, CopyrightClaimToResponse(claim)) + } + return responses +} \ No newline at end of file diff --git a/db/converter/report_converter.go b/db/converter/report_converter.go new file mode 100644 index 0000000..091fd0f --- /dev/null +++ b/db/converter/report_converter.go @@ -0,0 +1,37 @@ +package converter + +import ( + "github.com/ctwj/urldb/db/dto" + "github.com/ctwj/urldb/db/entity" + "time" +) + +// ReportToResponse 将举报实体转换为响应对象 +func ReportToResponse(report *entity.Report) *dto.ReportResponse { + if report == nil { + return nil + } + + return &dto.ReportResponse{ + ID: report.ID, + ResourceKey: report.ResourceKey, + Reason: report.Reason, + Description: report.Description, + Contact: report.Contact, + UserAgent: report.UserAgent, + IPAddress: report.IPAddress, + Status: report.Status, + Note: report.Note, + CreatedAt: report.CreatedAt.Format(time.RFC3339), + UpdatedAt: report.UpdatedAt.Format(time.RFC3339), + } +} + +// ReportsToResponse 将举报实体列表转换为响应对象列表 +func ReportsToResponse(reports []*entity.Report) []*dto.ReportResponse { + var responses []*dto.ReportResponse + for _, report := range reports { + responses = append(responses, ReportToResponse(report)) + } + return responses +} \ No newline at end of file diff --git a/db/dto/copyright_claim.go b/db/dto/copyright_claim.go new file mode 100644 index 0000000..19f28be --- /dev/null +++ b/db/dto/copyright_claim.go @@ -0,0 +1,45 @@ +package dto + +// CopyrightClaimCreateRequest 版权申述创建请求 +type CopyrightClaimCreateRequest struct { + ResourceKey string `json:"resource_key" validate:"required,max=255"` + Identity string `json:"identity" validate:"required,max=50"` + ProofType string `json:"proof_type" validate:"required,max=50"` + Reason string `json:"reason" validate:"required,max=2000"` + ContactInfo string `json:"contact_info" validate:"required,max=255"` + ClaimantName string `json:"claimant_name" validate:"required,max=100"` + ProofFiles string `json:"proof_files" validate:"omitempty,max=2000"` + UserAgent string `json:"user_agent" validate:"omitempty,max=1000"` + IPAddress string `json:"ip_address" validate:"omitempty,max=45"` +} + +// CopyrightClaimUpdateRequest 版权申述更新请求 +type CopyrightClaimUpdateRequest struct { + Status string `json:"status" validate:"required,oneof=pending approved rejected"` + Note string `json:"note" validate:"omitempty,max=1000"` +} + +// CopyrightClaimResponse 版权申述响应 +type CopyrightClaimResponse struct { + ID uint `json:"id"` + ResourceKey string `json:"resource_key"` + Identity string `json:"identity"` + ProofType string `json:"proof_type"` + Reason string `json:"reason"` + ContactInfo string `json:"contact_info"` + ClaimantName string `json:"claimant_name"` + ProofFiles string `json:"proof_files"` + UserAgent string `json:"user_agent"` + IPAddress string `json:"ip_address"` + Status string `json:"status"` + Note string `json:"note"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// CopyrightClaimListRequest 版权申述列表请求 +type CopyrightClaimListRequest struct { + Page int `query:"page" validate:"omitempty,min=1"` + PageSize int `query:"page_size" validate:"omitempty,min=1,max=100"` + Status string `query:"status" validate:"omitempty,oneof=pending approved rejected"` +} \ No newline at end of file diff --git a/db/dto/report.go b/db/dto/report.go new file mode 100644 index 0000000..782dfc2 --- /dev/null +++ b/db/dto/report.go @@ -0,0 +1,39 @@ +package dto + +// ReportCreateRequest 举报创建请求 +type ReportCreateRequest struct { + ResourceKey string `json:"resource_key" validate:"required,max=255"` + Reason string `json:"reason" validate:"required,max=100"` + Description string `json:"description" validate:"required,max=1000"` + Contact string `json:"contact" validate:"omitempty,max=255"` + UserAgent string `json:"user_agent" validate:"omitempty,max=1000"` + IPAddress string `json:"ip_address" validate:"omitempty,max=45"` +} + +// ReportUpdateRequest 举报更新请求 +type ReportUpdateRequest struct { + Status string `json:"status" validate:"required,oneof=pending approved rejected"` + Note string `json:"note" validate:"omitempty,max=1000"` +} + +// ReportResponse 举报响应 +type ReportResponse struct { + ID uint `json:"id"` + ResourceKey string `json:"resource_key"` + Reason string `json:"reason"` + Description string `json:"description"` + Contact string `json:"contact"` + UserAgent string `json:"user_agent"` + IPAddress string `json:"ip_address"` + Status string `json:"status"` + Note string `json:"note"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// ReportListRequest 举报列表请求 +type ReportListRequest struct { + Page int `query:"page" validate:"omitempty,min=1"` + PageSize int `query:"page_size" validate:"omitempty,min=1,max=100"` + Status string `query:"status" validate:"omitempty,oneof=pending approved rejected"` +} \ No newline at end of file diff --git a/db/entity/copyright_claim.go b/db/entity/copyright_claim.go new file mode 100644 index 0000000..e93d5b1 --- /dev/null +++ b/db/entity/copyright_claim.go @@ -0,0 +1,32 @@ +package entity + +import ( + "gorm.io/gorm" + "time" +) + +// CopyrightClaim 版权申述实体 +type CopyrightClaim struct { + ID uint `gorm:"primaryKey" json:"id"` + ResourceKey string `gorm:"type:varchar(255);not null;index" json:"resource_key"` // 资源唯一标识 + Identity string `gorm:"type:varchar(50);not null" json:"identity"` // 申述人身份 + ProofType string `gorm:"type:varchar(50);not null" json:"proof_type"` // 证明类型 + Reason string `gorm:"type:text;not null" json:"reason"` // 申述理由 + ContactInfo string `gorm:"type:varchar(255);not null" json:"contact_info"` // 联系信息 + ClaimantName string `gorm:"type:varchar(100);not null" json:"claimant_name"` // 申述人姓名 + ProofFiles string `gorm:"type:text" json:"proof_files"` // 证明文件(JSON格式) + UserAgent string `gorm:"type:text" json:"user_agent"` // 用户代理 + IPAddress string `gorm:"type:varchar(45)" json:"ip_address"` // IP地址 + Status string `gorm:"type:varchar(20);default:'pending'" json:"status"` // 处理状态: pending, approved, rejected + ProcessedAt *time.Time `json:"processed_at"` // 处理时间 + ProcessedBy *uint `json:"processed_by"` // 处理人ID + Note string `gorm:"type:text" json:"note"` // 处理备注 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"deleted_at"` +} + +// TableName 表名 +func (CopyrightClaim) TableName() string { + return "copyright_claims" +} \ No newline at end of file diff --git a/db/entity/report.go b/db/entity/report.go new file mode 100644 index 0000000..2559467 --- /dev/null +++ b/db/entity/report.go @@ -0,0 +1,29 @@ +package entity + +import ( + "gorm.io/gorm" + "time" +) + +// Report 举报实体 +type Report struct { + ID uint `gorm:"primaryKey" json:"id"` + ResourceKey string `gorm:"type:varchar(255);not null;index" json:"resource_key"` // 资源唯一标识 + Reason string `gorm:"type:varchar(100);not null" json:"reason"` // 举报原因 + Description string `gorm:"type:text" json:"description"` // 详细描述 + Contact string `gorm:"type:varchar(255)" json:"contact"` // 联系方式 + UserAgent string `gorm:"type:text" json:"user_agent"` // 用户代理 + IPAddress string `gorm:"type:varchar(45)" json:"ip_address"` // IP地址 + Status string `gorm:"type:varchar(20);default:'pending'" json:"status"` // 处理状态: pending, approved, rejected + ProcessedAt *time.Time `json:"processed_at"` // 处理时间 + ProcessedBy *uint `json:"processed_by"` // 处理人ID + Note string `gorm:"type:text" json:"note"` // 处理备注 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"deleted_at"` +} + +// TableName 表名 +func (Report) TableName() string { + return "reports" +} \ No newline at end of file diff --git a/db/repo/copyright_claim_repository.go b/db/repo/copyright_claim_repository.go new file mode 100644 index 0000000..93d808b --- /dev/null +++ b/db/repo/copyright_claim_repository.go @@ -0,0 +1,87 @@ +package repo + +import ( + "gorm.io/gorm" + "github.com/ctwj/urldb/db/entity" +) + +// CopyrightClaimRepository 版权申述Repository接口 +type CopyrightClaimRepository interface { + BaseRepository[entity.CopyrightClaim] + GetByResourceKey(resourceKey string) ([]*entity.CopyrightClaim, error) + List(status string, page, pageSize int) ([]*entity.CopyrightClaim, int64, error) + UpdateStatus(id uint, status string, processedBy *uint, note string) error + // 兼容原有方法名 + GetByID(id uint) (*entity.CopyrightClaim, error) +} + +// CopyrightClaimRepositoryImpl 版权申述Repository实现 +type CopyrightClaimRepositoryImpl struct { + BaseRepositoryImpl[entity.CopyrightClaim] +} + +// NewCopyrightClaimRepository 创建版权申述Repository +func NewCopyrightClaimRepository(db *gorm.DB) CopyrightClaimRepository { + return &CopyrightClaimRepositoryImpl{ + BaseRepositoryImpl: BaseRepositoryImpl[entity.CopyrightClaim]{db: db}, + } +} + +// Create 创建版权申述 +func (r *CopyrightClaimRepositoryImpl) Create(claim *entity.CopyrightClaim) error { + return r.GetDB().Create(claim).Error +} + +// GetByID 根据ID获取版权申述 +func (r *CopyrightClaimRepositoryImpl) GetByID(id uint) (*entity.CopyrightClaim, error) { + var claim entity.CopyrightClaim + err := r.GetDB().Where("id = ?", id).First(&claim).Error + return &claim, err +} + +// GetByResourceKey 获取某个资源的所有版权申述 +func (r *CopyrightClaimRepositoryImpl) GetByResourceKey(resourceKey string) ([]*entity.CopyrightClaim, error) { + var claims []*entity.CopyrightClaim + err := r.GetDB().Where("resource_key = ?", resourceKey).Find(&claims).Error + return claims, err +} + +// List 获取版权申述列表 +func (r *CopyrightClaimRepositoryImpl) List(status string, page, pageSize int) ([]*entity.CopyrightClaim, int64, error) { + var claims []*entity.CopyrightClaim + var total int64 + + query := r.GetDB().Model(&entity.CopyrightClaim{}) + + if status != "" { + query = query.Where("status = ?", status) + } + + // 获取总数 + query.Count(&total) + + // 分页查询 + offset := (page - 1) * pageSize + err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&claims).Error + return claims, total, err +} + +// Update 更新版权申述 +func (r *CopyrightClaimRepositoryImpl) Update(claim *entity.CopyrightClaim) error { + return r.GetDB().Save(claim).Error +} + +// UpdateStatus 更新版权申述状态 +func (r *CopyrightClaimRepositoryImpl) UpdateStatus(id uint, status string, processedBy *uint, note string) error { + return r.GetDB().Model(&entity.CopyrightClaim{}).Where("id = ?", id).Updates(map[string]interface{}{ + "status": status, + "processed_at": gorm.Expr("NOW()"), + "processed_by": processedBy, + "note": note, + }).Error +} + +// Delete 删除版权申述 +func (r *CopyrightClaimRepositoryImpl) Delete(id uint) error { + return r.GetDB().Delete(&entity.CopyrightClaim{}, id).Error +} \ No newline at end of file diff --git a/db/repo/manager.go b/db/repo/manager.go index 2aeb145..fddd530 100644 --- a/db/repo/manager.go +++ b/db/repo/manager.go @@ -22,6 +22,8 @@ type RepositoryManager struct { FileRepository FileRepository TelegramChannelRepository TelegramChannelRepository APIAccessLogRepository APIAccessLogRepository + ReportRepository ReportRepository + CopyrightClaimRepository CopyrightClaimRepository } // NewRepositoryManager 创建Repository管理器 @@ -43,5 +45,7 @@ func NewRepositoryManager(db *gorm.DB) *RepositoryManager { FileRepository: NewFileRepository(db), TelegramChannelRepository: NewTelegramChannelRepository(db), APIAccessLogRepository: NewAPIAccessLogRepository(db), + ReportRepository: NewReportRepository(db), + CopyrightClaimRepository: NewCopyrightClaimRepository(db), } } diff --git a/db/repo/report_repository.go b/db/repo/report_repository.go new file mode 100644 index 0000000..85cf4c9 --- /dev/null +++ b/db/repo/report_repository.go @@ -0,0 +1,87 @@ +package repo + +import ( + "gorm.io/gorm" + "github.com/ctwj/urldb/db/entity" +) + +// ReportRepository 举报Repository接口 +type ReportRepository interface { + BaseRepository[entity.Report] + GetByResourceKey(resourceKey string) ([]*entity.Report, error) + List(status string, page, pageSize int) ([]*entity.Report, int64, error) + UpdateStatus(id uint, status string, processedBy *uint, note string) error + // 兼容原有方法名 + GetByID(id uint) (*entity.Report, error) +} + +// ReportRepositoryImpl 举报Repository实现 +type ReportRepositoryImpl struct { + BaseRepositoryImpl[entity.Report] +} + +// NewReportRepository 创建举报Repository +func NewReportRepository(db *gorm.DB) ReportRepository { + return &ReportRepositoryImpl{ + BaseRepositoryImpl: BaseRepositoryImpl[entity.Report]{db: db}, + } +} + +// Create 创建举报 +func (r *ReportRepositoryImpl) Create(report *entity.Report) error { + return r.GetDB().Create(report).Error +} + +// GetByID 根据ID获取举报 +func (r *ReportRepositoryImpl) GetByID(id uint) (*entity.Report, error) { + var report entity.Report + err := r.GetDB().Where("id = ?", id).First(&report).Error + return &report, err +} + +// GetByResourceKey 获取某个资源的所有举报 +func (r *ReportRepositoryImpl) GetByResourceKey(resourceKey string) ([]*entity.Report, error) { + var reports []*entity.Report + err := r.GetDB().Where("resource_key = ?", resourceKey).Find(&reports).Error + return reports, err +} + +// List 获取举报列表 +func (r *ReportRepositoryImpl) List(status string, page, pageSize int) ([]*entity.Report, int64, error) { + var reports []*entity.Report + var total int64 + + query := r.GetDB().Model(&entity.Report{}) + + if status != "" { + query = query.Where("status = ?", status) + } + + // 获取总数 + query.Count(&total) + + // 分页查询 + offset := (page - 1) * pageSize + err := query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&reports).Error + return reports, total, err +} + +// Update 更新举报 +func (r *ReportRepositoryImpl) Update(report *entity.Report) error { + return r.GetDB().Save(report).Error +} + +// UpdateStatus 更新举报状态 +func (r *ReportRepositoryImpl) UpdateStatus(id uint, status string, processedBy *uint, note string) error { + return r.GetDB().Model(&entity.Report{}).Where("id = ?", id).Updates(map[string]interface{}{ + "status": status, + "processed_at": gorm.Expr("NOW()"), + "processed_by": processedBy, + "note": note, + }).Error +} + +// Delete 删除举报 +func (r *ReportRepositoryImpl) Delete(id uint) error { + return r.GetDB().Delete(&entity.Report{}, id).Error +} \ No newline at end of file diff --git a/go.mod b/go.mod index 3159425..d1e065f 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-playground/validator/v10 v10.27.0 github.com/goccy/go-json v0.10.5 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect diff --git a/handlers/copyright_claim_handler.go b/handlers/copyright_claim_handler.go new file mode 100644 index 0000000..6fc6b7c --- /dev/null +++ b/handlers/copyright_claim_handler.go @@ -0,0 +1,278 @@ +package handlers + +import ( + "net/http" + "strconv" + "github.com/ctwj/urldb/db/converter" + "github.com/ctwj/urldb/db/dto" + "github.com/ctwj/urldb/db/entity" + "github.com/ctwj/urldb/db/repo" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "gorm.io/gorm" +) + +type CopyrightClaimHandler struct { + copyrightClaimRepo repo.CopyrightClaimRepository + validate *validator.Validate +} + +func NewCopyrightClaimHandler(copyrightClaimRepo repo.CopyrightClaimRepository) *CopyrightClaimHandler { + return &CopyrightClaimHandler{ + copyrightClaimRepo: copyrightClaimRepo, + validate: validator.New(), + } +} + +// CreateCopyrightClaim 创建版权申述 +// @Summary 创建版权申述 +// @Description 提交资源版权申述 +// @Tags CopyrightClaim +// @Accept json +// @Produce json +// @Param request body dto.CopyrightClaimCreateRequest true "版权申述信息" +// @Success 200 {object} Response{data=dto.CopyrightClaimResponse} +// @Failure 400 {object} Response +// @Failure 500 {object} Response +// @Router /copyright-claims [post] +func (h *CopyrightClaimHandler) CreateCopyrightClaim(c *gin.Context) { + var req dto.CopyrightClaimCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest) + return + } + + // 验证参数 + if err := h.validate.Struct(req); err != nil { + ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest) + return + } + + // 创建版权申述实体 + claim := &entity.CopyrightClaim{ + ResourceKey: req.ResourceKey, + Identity: req.Identity, + ProofType: req.ProofType, + Reason: req.Reason, + ContactInfo: req.ContactInfo, + ClaimantName: req.ClaimantName, + ProofFiles: req.ProofFiles, + UserAgent: req.UserAgent, + IPAddress: req.IPAddress, + Status: "pending", // 默认为待处理 + } + + // 保存到数据库 + if err := h.copyrightClaimRepo.Create(claim); err != nil { + ErrorResponse(c, "创建版权申述失败: "+err.Error(), http.StatusInternalServerError) + return + } + + // 返回响应 + response := converter.CopyrightClaimToResponse(claim) + SuccessResponse(c, response) +} + +// GetCopyrightClaim 获取版权申述详情 +// @Summary 获取版权申述详情 +// @Description 根据ID获取版权申述详情 +// @Tags CopyrightClaim +// @Produce json +// @Param id path int true "版权申述ID" +// @Success 200 {object} Response{data=dto.CopyrightClaimResponse} +// @Failure 400 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /copyright-claims/{id} [get] +func (h *CopyrightClaimHandler) GetCopyrightClaim(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + ErrorResponse(c, "无效的ID", http.StatusBadRequest) + return + } + + claim, err := h.copyrightClaimRepo.GetByID(uint(id)) + if err != nil { + if err == gorm.ErrRecordNotFound { + ErrorResponse(c, "版权申述不存在", http.StatusNotFound) + return + } + ErrorResponse(c, "获取版权申述失败: "+err.Error(), http.StatusInternalServerError) + return + } + + SuccessResponse(c, converter.CopyrightClaimToResponse(claim)) +} + +// ListCopyrightClaims 获取版权申述列表 +// @Summary 获取版权申述列表 +// @Description 获取版权申述列表(支持分页和状态筛选) +// @Tags CopyrightClaim +// @Produce json +// @Param page query int false "页码" default(1) +// @Param page_size query int false "每页数量" default(10) +// @Param status query string false "处理状态" +// @Success 200 {object} Response{data=object{items=[]dto.CopyrightClaimResponse,total=int}} +// @Failure 400 {object} Response +// @Failure 500 {object} Response +// @Router /copyright-claims [get] +func (h *CopyrightClaimHandler) ListCopyrightClaims(c *gin.Context) { + var req dto.CopyrightClaimListRequest + if err := c.ShouldBindQuery(&req); err != nil { + ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest) + return + } + + // 设置默认值 + if req.Page == 0 { + req.Page = 1 + } + if req.PageSize == 0 { + req.PageSize = 10 + } + + // 验证参数 + if err := h.validate.Struct(req); err != nil { + ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest) + return + } + + claims, total, err := h.copyrightClaimRepo.List(req.Status, req.Page, req.PageSize) + if err != nil { + ErrorResponse(c, "获取版权申述列表失败: "+err.Error(), http.StatusInternalServerError) + return + } + + PageResponse(c, converter.CopyrightClaimsToResponse(claims), total, req.Page, req.PageSize) +} + +// UpdateCopyrightClaim 更新版权申述状态 +// @Summary 更新版权申述状态 +// @Description 更新版权申述处理状态 +// @Tags CopyrightClaim +// @Accept json +// @Produce json +// @Param id path int true "版权申述ID" +// @Param request body dto.CopyrightClaimUpdateRequest true "更新信息" +// @Success 200 {object} Response{data=dto.CopyrightClaimResponse} +// @Failure 400 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /copyright-claims/{id} [put] +func (h *CopyrightClaimHandler) UpdateCopyrightClaim(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + ErrorResponse(c, "无效的ID", http.StatusBadRequest) + return + } + + var req dto.CopyrightClaimUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest) + return + } + + // 验证参数 + if err := h.validate.Struct(req); err != nil { + ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest) + return + } + + // 获取当前版权申述 + _, err = h.copyrightClaimRepo.GetByID(uint(id)) + if err != nil { + if err == gorm.ErrRecordNotFound { + ErrorResponse(c, "版权申述不存在", http.StatusNotFound) + return + } + ErrorResponse(c, "获取版权申述失败: "+err.Error(), http.StatusInternalServerError) + return + } + + // 更新状态 + processedBy := uint(0) // 从上下文获取当前用户ID,如果存在的话 + if currentUser := c.GetUint("user_id"); currentUser > 0 { + processedBy = currentUser + } + + if err := h.copyrightClaimRepo.UpdateStatus(uint(id), req.Status, &processedBy, req.Note); err != nil { + ErrorResponse(c, "更新版权申述状态失败: "+err.Error(), http.StatusInternalServerError) + return + } + + // 获取更新后的版权申述信息 + updatedClaim, err := h.copyrightClaimRepo.GetByID(uint(id)) + if err != nil { + ErrorResponse(c, "获取更新后版权申述信息失败: "+err.Error(), http.StatusInternalServerError) + return + } + + SuccessResponse(c, converter.CopyrightClaimToResponse(updatedClaim)) +} + +// DeleteCopyrightClaim 删除版权申述 +// @Summary 删除版权申述 +// @Description 删除版权申述记录 +// @Tags CopyrightClaim +// @Produce json +// @Param id path int true "版权申述ID" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 500 {object} Response +// @Router /copyright-claims/{id} [delete] +func (h *CopyrightClaimHandler) DeleteCopyrightClaim(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + ErrorResponse(c, "无效的ID", http.StatusBadRequest) + return + } + + if err := h.copyrightClaimRepo.Delete(uint(id)); err != nil { + ErrorResponse(c, "删除版权申述失败: "+err.Error(), http.StatusInternalServerError) + return + } + + SuccessResponse(c, nil) +} + +// GetCopyrightClaimByResource 获取某个资源的版权申述列表 +// @Summary 获取资源版权申述列表 +// @Description 获取某个资源的所有版权申述记录 +// @Tags CopyrightClaim +// @Produce json +// @Param resource_key path string true "资源Key" +// @Success 200 {object} Response{data=[]dto.CopyrightClaimResponse} +// @Failure 400 {object} Response +// @Failure 500 {object} Response +// @Router /copyright-claims/resource/{resource_key} [get] +func (h *CopyrightClaimHandler) GetCopyrightClaimByResource(c *gin.Context) { + resourceKey := c.Param("resource_key") + if resourceKey == "" { + ErrorResponse(c, "资源Key不能为空", http.StatusBadRequest) + return + } + + claims, err := h.copyrightClaimRepo.GetByResourceKey(resourceKey) + if err != nil { + ErrorResponse(c, "获取资源版权申述失败: "+err.Error(), http.StatusInternalServerError) + return + } + + SuccessResponse(c, converter.CopyrightClaimsToResponse(claims)) +} + +// RegisterCopyrightClaimRoutes 注册版权申述相关路由 +func RegisterCopyrightClaimRoutes(router *gin.RouterGroup, copyrightClaimRepo repo.CopyrightClaimRepository) { + handler := NewCopyrightClaimHandler(copyrightClaimRepo) + + claims := router.Group("/copyright-claims") + { + claims.POST("", handler.CreateCopyrightClaim) // 创建版权申述 + claims.GET("/:id", handler.GetCopyrightClaim) // 获取版权申述详情 + claims.GET("", handler.ListCopyrightClaims) // 获取版权申述列表 + claims.PUT("/:id", handler.UpdateCopyrightClaim) // 更新版权申述状态 + claims.DELETE("/:id", handler.DeleteCopyrightClaim) // 删除版权申述 + claims.GET("/resource/:resource_key", handler.GetCopyrightClaimByResource) // 获取资源版权申述列表 + } +} \ No newline at end of file diff --git a/handlers/report_handler.go b/handlers/report_handler.go new file mode 100644 index 0000000..322f5b0 --- /dev/null +++ b/handlers/report_handler.go @@ -0,0 +1,276 @@ +package handlers + +import ( + "net/http" + "strconv" + "github.com/ctwj/urldb/db/converter" + "github.com/ctwj/urldb/db/dto" + "github.com/ctwj/urldb/db/entity" + "github.com/ctwj/urldb/db/repo" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "gorm.io/gorm" +) + +type ReportHandler struct { + reportRepo repo.ReportRepository + validate *validator.Validate +} + +func NewReportHandler(reportRepo repo.ReportRepository) *ReportHandler { + return &ReportHandler{ + reportRepo: reportRepo, + validate: validator.New(), + } +} + +// CreateReport 创建举报 +// @Summary 创建举报 +// @Description 提交资源举报 +// @Tags Report +// @Accept json +// @Produce json +// @Param request body dto.ReportCreateRequest true "举报信息" +// @Success 200 {object} Response{data=dto.ReportResponse} +// @Failure 400 {object} Response +// @Failure 500 {object} Response +// @Router /reports [post] +func (h *ReportHandler) CreateReport(c *gin.Context) { + var req dto.ReportCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest) + return + } + + // 验证参数 + if err := h.validate.Struct(req); err != nil { + ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest) + return + } + + // 创建举报实体 + report := &entity.Report{ + ResourceKey: req.ResourceKey, + Reason: req.Reason, + Description: req.Description, + Contact: req.Contact, + UserAgent: req.UserAgent, + IPAddress: req.IPAddress, + Status: "pending", // 默认为待处理 + } + + // 保存到数据库 + if err := h.reportRepo.Create(report); err != nil { + ErrorResponse(c, "创建举报失败: "+err.Error(), http.StatusInternalServerError) + return + } + + // 返回响应 + response := converter.ReportToResponse(report) + SuccessResponse(c, response) +} + +// GetReport 获取举报详情 +// @Summary 获取举报详情 +// @Description 根据ID获取举报详情 +// @Tags Report +// @Produce json +// @Param id path int true "举报ID" +// @Success 200 {object} Response{data=dto.ReportResponse} +// @Failure 400 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /reports/{id} [get] +func (h *ReportHandler) GetReport(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + ErrorResponse(c, "无效的ID", http.StatusBadRequest) + return + } + + report, err := h.reportRepo.GetByID(uint(id)) + if err != nil { + if err == gorm.ErrRecordNotFound { + ErrorResponse(c, "举报不存在", http.StatusNotFound) + return + } + ErrorResponse(c, "获取举报失败: "+err.Error(), http.StatusInternalServerError) + return + } + + response := converter.ReportToResponse(report) + SuccessResponse(c, response) +} + +// ListReports 获取举报列表 +// @Summary 获取举报列表 +// @Description 获取举报列表(支持分页和状态筛选) +// @Tags Report +// @Produce json +// @Param page query int false "页码" default(1) +// @Param page_size query int false "每页数量" default(10) +// @Param status query string false "处理状态" +// @Success 200 {object} Response{data=object{items=[]dto.ReportResponse,total=int}} +// @Failure 400 {object} Response +// @Failure 500 {object} Response +// @Router /reports [get] +func (h *ReportHandler) ListReports(c *gin.Context) { + var req dto.ReportListRequest + if err := c.ShouldBindQuery(&req); err != nil { + ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest) + return + } + + // 设置默认值 + if req.Page == 0 { + req.Page = 1 + } + if req.PageSize == 0 { + req.PageSize = 10 + } + + // 验证参数 + if err := h.validate.Struct(req); err != nil { + ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest) + return + } + + reports, total, err := h.reportRepo.List(req.Status, req.Page, req.PageSize) + if err != nil { + ErrorResponse(c, "获取举报列表失败: "+err.Error(), http.StatusInternalServerError) + return + } + + PageResponse(c, converter.ReportsToResponse(reports), total, req.Page, req.PageSize) +} + +// UpdateReport 更新举报状态 +// @Summary 更新举报状态 +// @Description 更新举报处理状态 +// @Tags Report +// @Accept json +// @Produce json +// @Param id path int true "举报ID" +// @Param request body dto.ReportUpdateRequest true "更新信息" +// @Success 200 {object} Response{data=dto.ReportResponse} +// @Failure 400 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /reports/{id} [put] +func (h *ReportHandler) UpdateReport(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + ErrorResponse(c, "无效的ID", http.StatusBadRequest) + return + } + + var req dto.ReportUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest) + return + } + + // 验证参数 + if err := h.validate.Struct(req); err != nil { + ErrorResponse(c, "参数验证失败: "+err.Error(), http.StatusBadRequest) + return + } + + // 获取当前举报 + _, err = h.reportRepo.GetByID(uint(id)) + if err != nil { + if err == gorm.ErrRecordNotFound { + ErrorResponse(c, "举报不存在", http.StatusNotFound) + return + } + ErrorResponse(c, "获取举报失败: "+err.Error(), http.StatusInternalServerError) + return + } + + // 更新状态 + processedBy := uint(0) // 从上下文获取当前用户ID,如果存在的话 + if currentUser := c.GetUint("user_id"); currentUser > 0 { + processedBy = currentUser + } + + if err := h.reportRepo.UpdateStatus(uint(id), req.Status, &processedBy, req.Note); err != nil { + ErrorResponse(c, "更新举报状态失败: "+err.Error(), http.StatusInternalServerError) + return + } + + // 获取更新后的举报信息 + updatedReport, err := h.reportRepo.GetByID(uint(id)) + if err != nil { + ErrorResponse(c, "获取更新后举报信息失败: "+err.Error(), http.StatusInternalServerError) + return + } + + SuccessResponse(c, converter.ReportToResponse(updatedReport)) +} + +// DeleteReport 删除举报 +// @Summary 删除举报 +// @Description 删除举报记录 +// @Tags Report +// @Produce json +// @Param id path int true "举报ID" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 500 {object} Response +// @Router /reports/{id} [delete] +func (h *ReportHandler) DeleteReport(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + ErrorResponse(c, "无效的ID", http.StatusBadRequest) + return + } + + if err := h.reportRepo.Delete(uint(id)); err != nil { + ErrorResponse(c, "删除举报失败: "+err.Error(), http.StatusInternalServerError) + return + } + + SuccessResponse(c, nil) +} + +// GetReportByResource 获取某个资源的举报列表 +// @Summary 获取资源举报列表 +// @Description 获取某个资源的所有举报记录 +// @Tags Report +// @Produce json +// @Param resource_key path string true "资源Key" +// @Success 200 {object} Response{data=[]dto.ReportResponse} +// @Failure 400 {object} Response +// @Failure 500 {object} Response +// @Router /reports/resource/{resource_key} [get] +func (h *ReportHandler) GetReportByResource(c *gin.Context) { + resourceKey := c.Param("resource_key") + if resourceKey == "" { + ErrorResponse(c, "资源Key不能为空", http.StatusBadRequest) + return + } + + reports, err := h.reportRepo.GetByResourceKey(resourceKey) + if err != nil { + ErrorResponse(c, "获取资源举报失败: "+err.Error(), http.StatusInternalServerError) + return + } + + SuccessResponse(c, converter.ReportsToResponse(reports)) +} + +// RegisterReportRoutes 注册举报相关路由 +func RegisterReportRoutes(router *gin.RouterGroup, reportRepo repo.ReportRepository) { + handler := NewReportHandler(reportRepo) + + reports := router.Group("/reports") + { + reports.POST("", handler.CreateReport) // 创建举报 + reports.GET("/:id", handler.GetReport) // 获取举报详情 + reports.GET("", handler.ListReports) // 获取举报列表 + reports.PUT("/:id", handler.UpdateReport) // 更新举报状态 + reports.DELETE("/:id", handler.DeleteReport) // 删除举报 + reports.GET("/resource/:resource_key", handler.GetReportByResource) // 获取资源举报列表 + } +} \ No newline at end of file diff --git a/main.go b/main.go index 7caa944..121249d 100644 --- a/main.go +++ b/main.go @@ -211,6 +211,10 @@ func main() { // 创建OG图片处理器 ogImageHandler := handlers.NewOGImageHandler() + // 创建举报和版权申述处理器 + reportHandler := handlers.NewReportHandler(repoManager.ReportRepository) + copyrightClaimHandler := handlers.NewCopyrightClaimHandler(repoManager.CopyrightClaimRepository) + // API路由 api := r.Group("/api") { @@ -443,6 +447,21 @@ func main() { // OG图片生成路由 api.GET("/og-image", ogImageHandler.GenerateOGImage) + + // 举报和版权申述路由 + api.POST("/reports", reportHandler.CreateReport) + api.GET("/reports/:id", reportHandler.GetReport) + api.GET("/reports", middleware.AuthMiddleware(), middleware.AdminMiddleware(), reportHandler.ListReports) + api.PUT("/reports/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), reportHandler.UpdateReport) + api.DELETE("/reports/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), reportHandler.DeleteReport) + api.GET("/reports/resource/:resource_key", reportHandler.GetReportByResource) + + api.POST("/copyright-claims", copyrightClaimHandler.CreateCopyrightClaim) + api.GET("/copyright-claims/:id", copyrightClaimHandler.GetCopyrightClaim) + api.GET("/copyright-claims", middleware.AuthMiddleware(), middleware.AdminMiddleware(), copyrightClaimHandler.ListCopyrightClaims) + api.PUT("/copyright-claims/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), copyrightClaimHandler.UpdateCopyrightClaim) + api.DELETE("/copyright-claims/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), copyrightClaimHandler.DeleteCopyrightClaim) + api.GET("/copyright-claims/resource/:resource_key", copyrightClaimHandler.GetCopyrightClaimByResource) } // 设置监控系统 diff --git a/web/components/CopyrightModal.vue b/web/components/CopyrightModal.vue index 873fd8f..cadf573 100644 --- a/web/components/CopyrightModal.vue +++ b/web/components/CopyrightModal.vue @@ -129,6 +129,9 @@ \ No newline at end of file diff --git a/web/pages/admin/reports.vue b/web/pages/admin/reports.vue new file mode 100644 index 0000000..88566e7 --- /dev/null +++ b/web/pages/admin/reports.vue @@ -0,0 +1,483 @@ + + + \ No newline at end of file diff --git a/web/pages/r/[key].vue b/web/pages/r/[key].vue index 5f9e524..16150ba 100644 --- a/web/pages/r/[key].vue +++ b/web/pages/r/[key].vue @@ -137,7 +137,7 @@ @@ -494,6 +494,27 @@ const mainResource = computed(() => { return resources && resources.length > 0 ? resources[0] : null }) +// 生成完整的资源URL +const getResourceUrl = computed(() => { + const config = useRuntimeConfig() + const key = mainResource.value?.key + if (!key) return '' + + // 优先使用配置中的站点URL,如果未设置则使用当前页面的origin + let siteUrl = config.public.siteUrl + if (!siteUrl || siteUrl === 'https://yourdomain.com') { + // 在客户端使用当前页面的origin + if (typeof window !== 'undefined') { + siteUrl = window.location.origin + } else { + // 在服务端渲染时,使用默认值(这应该在部署时被环境变量覆盖) + siteUrl = process.env.NUXT_PUBLIC_SITE_URL || 'https://yourdomain.com' + } + } + + return `${siteUrl}/r/${key}` +}) + // 服务端相关资源处理(去重) const serverRelatedResources = computed(() => { const resources = Array.isArray(relatedResourcesData.value?.data) ? relatedResourcesData.value.data : [] @@ -690,11 +711,15 @@ const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text) // 显示复制成功提示 - const notification = useNotification() - notification.success({ - content: '已复制到剪贴板', - duration: 2000 - }) + if (process.client) { + const notification = useNotification() + if (notification) { + notification.success({ + content: '已复制到剪贴板', + duration: 2000 + }) + } + } } catch (error) { console.error('复制失败:', error) } @@ -703,21 +728,29 @@ const copyToClipboard = async (text: string) => { // 处理举报提交 const handleReportSubmitted = () => { showReportModal.value = false - const notification = useNotification() - notification.success({ - content: '举报已提交,感谢您的反馈', - duration: 3000 - }) + if (process.client) { + const notification = useNotification() + if (notification) { + notification.success({ + content: '举报已提交,感谢您的反馈', + duration: 3000 + }) + } + } } // 处理版权申述提交 const handleCopyrightSubmitted = () => { showCopyrightModal.value = false - const notification = useNotification() - notification.success({ - content: '版权申述已提交,我们会尽快处理', - duration: 3000 - }) + if (process.client) { + const notification = useNotification() + if (notification) { + notification.success({ + content: '版权申述已提交,我们会尽快处理', + duration: 3000 + }) + } + } } // 获取相关资源(客户端更新,用于交互优化)