Files
backend/internal/handler/material_handler.go
lafay 2f584c1f2c
All checks were successful
Build Backend / build (push) Successful in 5m10s
Build Backend / build-docker (push) Successful in 2m22s
This is a breaking change that renames the entire project from "Carrot BBS" to "WithYou".
The Go module name has been changed from `carrot_bbs` to `with_you`, which requires
all import paths to be updated throughout the codebase and by external consumers.

BREAKING CHANGE: Module name changed from carrot_bbs to with_you. All imports
and dependencies must be updated from carrot_bbs/* to with_you/*.
2026-04-22 16:01:59 +08:00

492 lines
14 KiB
Go

package handler
import (
"errors"
"strconv"
"with_you/internal/dto"
"with_you/internal/model"
"with_you/internal/pkg/response"
"with_you/internal/service"
"github.com/gin-gonic/gin"
)
// MaterialHandler 学习资料处理器
type MaterialHandler struct {
materialService service.MaterialService
}
// NewMaterialHandler 创建学习资料处理器
func NewMaterialHandler(materialService service.MaterialService) *MaterialHandler {
return &MaterialHandler{materialService: materialService}
}
// =====================================================
// 响应结构体
// =====================================================
type subjectResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
Color string `json:"color"`
Description string `json:"description,omitempty"`
SortOrder int `json:"sort_order"`
IsActive bool `json:"is_active"`
MaterialCount int `json:"material_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type materialResponse struct {
ID string `json:"id"`
SubjectID string `json:"subject_id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
FileType string `json:"file_type"`
FileSize int64 `json:"file_size"`
FileURL string `json:"file_url"`
FileName string `json:"file_name"`
DownloadCount int `json:"download_count"`
AuthorID string `json:"author_id,omitempty"`
Tags []string `json:"tags,omitempty"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Subject *subjectResponse `json:"subject,omitempty"`
}
// =====================================================
// 转换函数
// =====================================================
func toSubjectResponse(s *model.MaterialSubject, materialCount int64) *subjectResponse {
if s == nil {
return nil
}
return &subjectResponse{
ID: s.ID,
Name: s.Name,
Icon: s.Icon,
Color: s.Color,
Description: s.Description,
SortOrder: s.SortOrder,
IsActive: s.IsActive,
MaterialCount: int(materialCount),
CreatedAt: s.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: s.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
func toMaterialResponse(f *model.MaterialFile) *materialResponse {
if f == nil {
return nil
}
var subjectResp *subjectResponse
if f.Subject != nil {
subjectResp = toSubjectResponse(f.Subject, 0)
}
return &materialResponse{
ID: f.ID,
SubjectID: f.SubjectID,
Title: f.Title,
Description: f.Description,
FileType: string(f.FileType),
FileSize: f.FileSize,
FileURL: f.FileURL,
FileName: f.FileName,
DownloadCount: f.DownloadCount,
AuthorID: f.AuthorID,
Tags: f.Tags,
Status: string(f.Status),
CreatedAt: f.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: f.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
Subject: subjectResp,
}
}
// =====================================================
// 公开API - 学科相关
// =====================================================
// ListSubjects 公开学科列表(仅启用)
func (h *MaterialHandler) ListSubjects(c *gin.Context) {
items, err := h.materialService.ListActiveSubjects()
if err != nil {
response.InternalServerError(c, "failed to get subjects")
return
}
result := make([]*subjectResponse, 0, len(items))
for _, item := range items {
_, count, _ := h.materialService.GetSubjectByIDWithMaterialCount(item.ID)
result = append(result, toSubjectResponse(item, count))
}
response.Success(c, gin.H{"list": result})
}
// GetSubject 获取学科详情
func (h *MaterialHandler) GetSubject(c *gin.Context) {
id := c.Param("id")
item, count, err := h.materialService.GetSubjectByIDWithMaterialCount(id)
if err != nil {
response.NotFound(c, "subject not found")
return
}
response.Success(c, toSubjectResponse(item, count))
}
// =====================================================
// 公开API - 资料相关
// =====================================================
// ListMaterials 资料列表
func (h *MaterialHandler) ListMaterials(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
subjectID := c.Query("subject_id")
fileType := c.Query("file_type")
keyword := c.Query("keyword")
params := dto.MaterialFileQueryParams{
SubjectID: subjectID,
FileType: fileType,
Keyword: keyword,
Status: string(model.MaterialStatusActive),
Page: page,
PageSize: pageSize,
SortBy: "created_at",
SortOrder: "DESC",
}
items, total, err := h.materialService.ListMaterials(params)
if err != nil {
response.InternalServerError(c, "failed to get materials")
return
}
result := make([]*materialResponse, 0, len(items))
for _, item := range items {
result = append(result, toMaterialResponse(item))
}
response.Success(c, gin.H{
"list": result,
"total": total,
"page": page,
"page_size": pageSize,
"has_more": int64(page*pageSize) < total,
})
}
// GetMaterial 获取资料详情
func (h *MaterialHandler) GetMaterial(c *gin.Context) {
id := c.Param("id")
item, err := h.materialService.GetMaterialByIDWithSubject(id)
if err != nil {
response.NotFound(c, "material not found")
return
}
response.Success(c, toMaterialResponse(item))
}
// SearchMaterials 搜索资料
func (h *MaterialHandler) SearchMaterials(c *gin.Context) {
keyword := c.Query("keyword")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
items, total, err := h.materialService.SearchMaterials(keyword, page, pageSize)
if err != nil {
response.InternalServerError(c, "failed to search materials")
return
}
result := make([]*materialResponse, 0, len(items))
for _, item := range items {
result = append(result, toMaterialResponse(item))
}
response.Success(c, gin.H{
"list": result,
"total": total,
"page": page,
"page_size": pageSize,
"has_more": int64(page*pageSize) < total,
})
}
// DownloadMaterial 下载资料(增加下载次数)
func (h *MaterialHandler) DownloadMaterial(c *gin.Context) {
id := c.Param("id")
item, err := h.materialService.GetMaterialByID(id)
if err != nil {
response.NotFound(c, "material not found")
return
}
// 增加下载次数
_ = h.materialService.IncrementDownloadCount(id)
// 返回下载链接
response.Success(c, gin.H{
"id": item.ID,
"file_url": item.FileURL,
"file_name": item.FileName,
})
}
// =====================================================
// 管理API - 学科相关
// =====================================================
// AdminListSubjects 管理端学科列表(全部)
func (h *MaterialHandler) AdminListSubjects(c *gin.Context) {
items, err := h.materialService.ListAllSubjects()
if err != nil {
response.InternalServerError(c, "failed to get subjects")
return
}
result := make([]*subjectResponse, 0, len(items))
for _, item := range items {
_, count, _ := h.materialService.GetSubjectByIDWithMaterialCount(item.ID)
result = append(result, toSubjectResponse(item, count))
}
response.Success(c, gin.H{"list": result})
}
type adminSubjectUpsertRequest struct {
Name string `json:"name" binding:"required,max=100"`
Icon string `json:"icon" binding:"max=50"`
Color string `json:"color" binding:"max=20"`
Description string `json:"description" binding:"max=255"`
SortOrder int `json:"sort_order"`
IsActive *bool `json:"is_active"`
}
// AdminCreateSubject 创建学科
func (h *MaterialHandler) AdminCreateSubject(c *gin.Context) {
var req adminSubjectUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
item, err := h.materialService.CreateSubject(req.Name, req.Icon, req.Color, req.Description, req.SortOrder, isActive)
if err != nil {
response.InternalServerError(c, "failed to create subject")
return
}
response.Success(c, toSubjectResponse(item, 0))
}
// AdminUpdateSubject 更新学科
func (h *MaterialHandler) AdminUpdateSubject(c *gin.Context) {
id := c.Param("id")
var req adminSubjectUpsertRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
item, err := h.materialService.UpdateSubject(id, req.Name, req.Icon, req.Color, req.Description, req.SortOrder, isActive)
if err != nil {
response.InternalServerError(c, "failed to update subject")
return
}
_, count, _ := h.materialService.GetSubjectByIDWithMaterialCount(item.ID)
response.Success(c, toSubjectResponse(item, count))
}
// AdminDeleteSubject 删除学科
func (h *MaterialHandler) AdminDeleteSubject(c *gin.Context) {
id := c.Param("id")
if err := h.materialService.DeleteSubject(id); err != nil {
if errors.Is(err, service.ErrSubjectHasMaterials) {
response.BadRequest(c, "学科下存在资料,无法删除")
return
}
response.InternalServerError(c, "failed to delete subject")
return
}
response.Success(c, gin.H{"success": true})
}
// =====================================================
// 管理API - 资料相关
// =====================================================
// AdminListMaterials 管理端资料列表
func (h *MaterialHandler) AdminListMaterials(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
subjectID := c.Query("subject_id")
fileType := c.Query("file_type")
status := c.Query("status")
keyword := c.Query("keyword")
params := dto.MaterialFileQueryParams{
SubjectID: subjectID,
FileType: fileType,
Status: status,
Keyword: keyword,
Page: page,
PageSize: pageSize,
SortBy: "created_at",
SortOrder: "DESC",
}
items, total, err := h.materialService.ListMaterials(params)
if err != nil {
response.InternalServerError(c, "failed to get materials")
return
}
result := make([]*materialResponse, 0, len(items))
for _, item := range items {
result = append(result, toMaterialResponse(item))
}
response.Success(c, gin.H{
"list": result,
"total": total,
"page": page,
"page_size": pageSize,
"total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
})
}
type adminMaterialCreateRequest struct {
SubjectID string `json:"subject_id" binding:"required"`
Title string `json:"title" binding:"required,max=200"`
Description string `json:"description"`
FileType string `json:"file_type" binding:"required,oneof=pdf word ppt"`
FileSize int64 `json:"file_size"`
FileURL string `json:"file_url" binding:"required"`
FileName string `json:"file_name"`
Tags []string `json:"tags"`
}
type adminMaterialUpdateRequest struct {
SubjectID string `json:"subject_id"`
Title string `json:"title" binding:"max=200"`
Description string `json:"description"`
FileType string `json:"file_type" binding:"omitempty,oneof=pdf word ppt"`
FileSize int64 `json:"file_size"`
FileURL string `json:"file_url"`
FileName string `json:"file_name"`
Tags []string `json:"tags"`
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
}
// AdminGetMaterial 管理端获取资料详情
func (h *MaterialHandler) AdminGetMaterial(c *gin.Context) {
id := c.Param("id")
item, err := h.materialService.GetMaterialByIDWithSubject(id)
if err != nil {
response.NotFound(c, "material not found")
return
}
response.Success(c, toMaterialResponse(item))
}
// AdminCreateMaterial 创建资料
func (h *MaterialHandler) AdminCreateMaterial(c *gin.Context) {
var req adminMaterialCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
// 获取当前用户ID作为作者
userID, exists := c.Get("user_id")
var authorID string
if exists {
authorID = userID.(string)
}
file := &model.MaterialFile{
SubjectID: req.SubjectID,
Title: req.Title,
Description: req.Description,
FileType: model.MaterialFileType(req.FileType),
FileSize: req.FileSize,
FileURL: req.FileURL,
FileName: req.FileName,
Tags: req.Tags,
Status: model.MaterialStatusActive,
AuthorID: authorID,
}
if err := h.materialService.CreateMaterial(file); err != nil {
response.InternalServerError(c, "failed to create material")
return
}
response.Success(c, toMaterialResponse(file))
}
// AdminUpdateMaterial 更新资料
func (h *MaterialHandler) AdminUpdateMaterial(c *gin.Context) {
id := c.Param("id")
var req adminMaterialUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
file, err := h.materialService.GetMaterialByID(id)
if err != nil {
response.NotFound(c, "material not found")
return
}
// 更新字段
if req.SubjectID != "" {
file.SubjectID = req.SubjectID
}
if req.Title != "" {
file.Title = req.Title
}
if req.Description != "" {
file.Description = req.Description
}
if req.FileType != "" {
file.FileType = model.MaterialFileType(req.FileType)
}
if req.FileSize > 0 {
file.FileSize = req.FileSize
}
if req.FileURL != "" {
file.FileURL = req.FileURL
}
if req.FileName != "" {
file.FileName = req.FileName
}
if req.Tags != nil {
file.Tags = req.Tags
}
if req.Status != "" {
file.Status = model.MaterialStatus(req.Status)
}
if err := h.materialService.UpdateMaterial(file); err != nil {
response.InternalServerError(c, "failed to update material")
return
}
response.Success(c, toMaterialResponse(file))
}
// AdminDeleteMaterial 删除资料
func (h *MaterialHandler) AdminDeleteMaterial(c *gin.Context) {
id := c.Param("id")
if err := h.materialService.DeleteMaterial(id); err != nil {
response.InternalServerError(c, "failed to delete material")
return
}
response.Success(c, gin.H{"success": true})
}