feat(material): add material handling and routing functionality
- Introduced MaterialHandler for managing learning materials and subjects. - Updated router to include routes for public and admin material management. - Enhanced database initialization with material subject and file models. - Implemented seeding logic for material subjects and files. - Updated wire generation to include MaterialHandler and related services.
This commit is contained in:
490
internal/handler/material_handler.go
Normal file
490
internal/handler/material_handler.go
Normal file
@@ -0,0 +1,490 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/response"
|
||||
"carrot_bbs/internal/repository"
|
||||
"carrot_bbs/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 := repository.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 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 := repository.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})
|
||||
}
|
||||
Reference in New Issue
Block a user