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:
@@ -46,6 +46,7 @@ func ProvideRouter(
|
|||||||
adminDashboardHandler *handler.AdminDashboardHandler,
|
adminDashboardHandler *handler.AdminDashboardHandler,
|
||||||
adminLogHandler *handler.AdminLogHandler,
|
adminLogHandler *handler.AdminLogHandler,
|
||||||
qrcodeHandler *handler.QRCodeHandler,
|
qrcodeHandler *handler.QRCodeHandler,
|
||||||
|
materialHandler *handler.MaterialHandler,
|
||||||
logService *service.LogService,
|
logService *service.LogService,
|
||||||
activityService service.UserActivityService,
|
activityService service.UserActivityService,
|
||||||
casbinService service.CasbinService,
|
casbinService service.CasbinService,
|
||||||
@@ -73,6 +74,7 @@ func ProvideRouter(
|
|||||||
adminDashboardHandler,
|
adminDashboardHandler,
|
||||||
adminLogHandler,
|
adminLogHandler,
|
||||||
qrcodeHandler,
|
qrcodeHandler,
|
||||||
|
materialHandler,
|
||||||
logService,
|
logService,
|
||||||
activityService,
|
activityService,
|
||||||
casbinService,
|
casbinService,
|
||||||
|
|||||||
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})
|
||||||
|
}
|
||||||
@@ -159,6 +159,10 @@ func autoMigrate(db *gorm.DB) error {
|
|||||||
&Role{},
|
&Role{},
|
||||||
&UserRole{},
|
&UserRole{},
|
||||||
&CasbinRule{},
|
&CasbinRule{},
|
||||||
|
|
||||||
|
// 学习资料相关
|
||||||
|
&MaterialSubject{},
|
||||||
|
&MaterialFile{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -184,6 +188,11 @@ func autoMigrate(db *gorm.DB) error {
|
|||||||
return fmt.Errorf("failed to seed channels: %w", err)
|
return fmt.Errorf("failed to seed channels: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化学习资料学科种子数据
|
||||||
|
if err := seedMaterialSubjects(db); err != nil {
|
||||||
|
return fmt.Errorf("failed to seed material subjects: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,8 +330,18 @@ func seedPermissions(db *gorm.DB) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// seedChannels 初始化频道种子数据(按 slug 幂等)
|
// seedChannels 初始化频道种子数据(仅在表为空时)
|
||||||
func seedChannels(db *gorm.DB) error {
|
func seedChannels(db *gorm.DB) error {
|
||||||
|
// 检查是否已有频道数据
|
||||||
|
var count int64
|
||||||
|
if err := db.Model(&Channel{}).Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
zap.L().Info("Channels already exist, skipping seed", zap.Int64("count", count))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
defaultChannels := []Channel{
|
defaultChannels := []Channel{
|
||||||
{Name: "跳蚤市场", Slug: "market", Description: "二手交易与闲置发布", SortOrder: 10, IsActive: true},
|
{Name: "跳蚤市场", Slug: "market", Description: "二手交易与闲置发布", SortOrder: 10, IsActive: true},
|
||||||
{Name: "课程交流", Slug: "courses", Description: "课程资料、选课与学习讨论", SortOrder: 20, IsActive: true},
|
{Name: "课程交流", Slug: "courses", Description: "课程资料、选课与学习讨论", SortOrder: 20, IsActive: true},
|
||||||
@@ -331,33 +350,45 @@ func seedChannels(db *gorm.DB) error {
|
|||||||
{Name: "保研出国", Slug: "graduate-abroad", Description: "保研、留学申请与经验分享", SortOrder: 50, IsActive: true},
|
{Name: "保研出国", Slug: "graduate-abroad", Description: "保研、留学申请与经验分享", SortOrder: 50, IsActive: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range defaultChannels {
|
if err := db.Create(&defaultChannels).Error; err != nil {
|
||||||
var existing Channel
|
return err
|
||||||
err := db.Where("slug = ?", item.Slug).First(&existing).Error
|
|
||||||
if err == nil {
|
|
||||||
updates := map[string]any{
|
|
||||||
"name": item.Name,
|
|
||||||
"description": item.Description,
|
|
||||||
"sort_order": item.SortOrder,
|
|
||||||
"is_active": item.IsActive,
|
|
||||||
}
|
|
||||||
if uerr := db.Model(&existing).Updates(updates).Error; uerr != nil {
|
|
||||||
return uerr
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if cerr := db.Create(&item).Error; cerr != nil {
|
|
||||||
return cerr
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info("Seeded channels successfully", zap.Int("count", len(defaultChannels)))
|
zap.L().Info("Seeded channels successfully", zap.Int("count", len(defaultChannels)))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// seedMaterialSubjects 初始化学习资料学科种子数据(仅在表为空时)
|
||||||
|
func seedMaterialSubjects(db *gorm.DB) error {
|
||||||
|
// 检查是否已有数据
|
||||||
|
var count int64
|
||||||
|
if err := db.Model(&MaterialSubject{}).Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
zap.L().Info("Material subjects already exist, skipping seed", zap.Int64("count", count))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultSubjects := []MaterialSubject{
|
||||||
|
{Name: "数学", Icon: "calculator-variant", Color: "#FF6B6B", Description: "高等数学、线性代数、概率论等", SortOrder: 10, IsActive: true},
|
||||||
|
{Name: "英语", Icon: "translate", Color: "#4ECDC4", Description: "四六级、考研英语、托福雅思等", SortOrder: 20, IsActive: true},
|
||||||
|
{Name: "物理", Icon: "atom", Color: "#45B7D1", Description: "大学物理、电磁学、力学等", SortOrder: 30, IsActive: true},
|
||||||
|
{Name: "化学", Icon: "flask", Color: "#96CEB4", Description: "有机化学、无机化学、分析化学等", SortOrder: 40, IsActive: true},
|
||||||
|
{Name: "计算机", Icon: "laptop", Color: "#FFEAA7", Description: "数据结构、算法、操作系统等", SortOrder: 50, IsActive: true},
|
||||||
|
{Name: "经济管理", Icon: "chart-line", Color: "#DDA0DD", Description: "微观经济学、宏观经济学、管理学等", SortOrder: 60, IsActive: true},
|
||||||
|
{Name: "法学", Icon: "scale-balance", Color: "#F39C12", Description: "民法、刑法、宪法等", SortOrder: 70, IsActive: true},
|
||||||
|
{Name: "语文文学", Icon: "book-open-page-variant", Color: "#3498DB", Description: "古代文学、现代文学、写作等", SortOrder: 80, IsActive: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&defaultSubjects).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.L().Info("Seeded material subjects successfully", zap.Int("count", len(defaultSubjects)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CloseDB 关闭数据库连接
|
// CloseDB 关闭数据库连接
|
||||||
func CloseDB(db *gorm.DB) {
|
func CloseDB(db *gorm.DB) {
|
||||||
if sqlDB, err := db.DB(); err == nil {
|
if sqlDB, err := db.DB(); err == nil {
|
||||||
|
|||||||
113
internal/model/material.go
Normal file
113
internal/model/material.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaterialFileType 文件类型
|
||||||
|
type MaterialFileType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
MaterialFileTypePDF MaterialFileType = "pdf"
|
||||||
|
MaterialFileTypeWord MaterialFileType = "word"
|
||||||
|
MaterialFileTypePPT MaterialFileType = "ppt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaterialStatus 资料状态
|
||||||
|
type MaterialStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
MaterialStatusActive MaterialStatus = "active"
|
||||||
|
MaterialStatusInactive MaterialStatus = "inactive"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaterialSubject 学科分类
|
||||||
|
type MaterialSubject struct {
|
||||||
|
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||||
|
Name string `json:"name" gorm:"type:varchar(100);not null"`
|
||||||
|
Icon string `json:"icon" gorm:"type:varchar(50)"` // MaterialCommunityIcons name
|
||||||
|
Color string `json:"color" gorm:"type:varchar(20)"` // 主题色
|
||||||
|
Description string `json:"description" gorm:"type:varchar(255)"` // 描述
|
||||||
|
SortOrder int `json:"sort_order" gorm:"default:0;index"` // 排序权重
|
||||||
|
IsActive bool `json:"is_active" gorm:"default:true;index"` // 是否启用
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||||
|
|
||||||
|
// 关联
|
||||||
|
Materials []MaterialFile `json:"materials,omitempty" gorm:"foreignKey:SubjectID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate 创建前生成UUID
|
||||||
|
func (s *MaterialSubject) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if s.ID == "" {
|
||||||
|
s.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MaterialSubject) TableName() string {
|
||||||
|
return "material_subjects"
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringArray 字符串数组类型(用于tags)
|
||||||
|
type StringArray []string
|
||||||
|
|
||||||
|
// Value 实现 driver.Valuer 接口
|
||||||
|
func (a StringArray) Value() (driver.Value, error) {
|
||||||
|
if a == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return json.Marshal(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 实现 sql.Scanner 接口
|
||||||
|
func (a *StringArray) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
*a = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bytes, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return json.Unmarshal(bytes, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaterialFile 文件资料
|
||||||
|
type MaterialFile struct {
|
||||||
|
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||||
|
SubjectID string `json:"subject_id" gorm:"type:varchar(36);index;not null"`
|
||||||
|
Title string `json:"title" gorm:"type:varchar(200);not null"`
|
||||||
|
Description string `json:"description" gorm:"type:text"`
|
||||||
|
FileType MaterialFileType `json:"file_type" gorm:"type:varchar(20);not null;index"`
|
||||||
|
FileSize int64 `json:"file_size" gorm:"default:0"` // 文件大小(字节)
|
||||||
|
FileURL string `json:"file_url" gorm:"type:text;not null"` // 文件URL
|
||||||
|
FileName string `json:"file_name" gorm:"type:varchar(255)"` // 原始文件名
|
||||||
|
DownloadCount int `json:"download_count" gorm:"default:0"` // 下载次数
|
||||||
|
AuthorID string `json:"author_id" gorm:"type:varchar(36);index"` // 上传者ID
|
||||||
|
Tags StringArray `json:"tags" gorm:"type:json"` // 标签
|
||||||
|
Status MaterialStatus `json:"status" gorm:"type:varchar(20);default:active;index"` // 状态
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||||
|
|
||||||
|
// 关联
|
||||||
|
Subject *MaterialSubject `json:"subject,omitempty" gorm:"foreignKey:SubjectID"`
|
||||||
|
Author *User `json:"author,omitempty" gorm:"foreignKey:AuthorID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate 创建前生成UUID
|
||||||
|
func (f *MaterialFile) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if f.ID == "" {
|
||||||
|
f.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MaterialFile) TableName() string {
|
||||||
|
return "material_files"
|
||||||
|
}
|
||||||
247
internal/repository/material_repo.go
Normal file
247
internal/repository/material_repo.go
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrot_bbs/internal/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaterialSubjectRepository 学科分类仓储
|
||||||
|
type MaterialSubjectRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMaterialSubjectRepository(db *gorm.DB) *MaterialSubjectRepository {
|
||||||
|
return &MaterialSubjectRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListActive 获取所有启用的学科分类
|
||||||
|
func (r *MaterialSubjectRepository) ListActive() ([]*model.MaterialSubject, error) {
|
||||||
|
var subjects []*model.MaterialSubject
|
||||||
|
err := r.db.
|
||||||
|
Where("is_active = ?", true).
|
||||||
|
Order("sort_order ASC, created_at ASC").
|
||||||
|
Find(&subjects).Error
|
||||||
|
return subjects, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll 获取所有学科分类(管理端用)
|
||||||
|
func (r *MaterialSubjectRepository) ListAll() ([]*model.MaterialSubject, error) {
|
||||||
|
var subjects []*model.MaterialSubject
|
||||||
|
err := r.db.
|
||||||
|
Order("sort_order ASC, created_at ASC").
|
||||||
|
Find(&subjects).Error
|
||||||
|
return subjects, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID 根据ID获取学科分类
|
||||||
|
func (r *MaterialSubjectRepository) GetByID(id string) (*model.MaterialSubject, error) {
|
||||||
|
var subject model.MaterialSubject
|
||||||
|
if err := r.db.First(&subject, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &subject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建学科分类
|
||||||
|
func (r *MaterialSubjectRepository) Create(subject *model.MaterialSubject) error {
|
||||||
|
return r.db.Create(subject).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新学科分类
|
||||||
|
func (r *MaterialSubjectRepository) Update(subject *model.MaterialSubject) error {
|
||||||
|
return r.db.Save(subject).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除学科分类
|
||||||
|
func (r *MaterialSubjectRepository) Delete(id string) error {
|
||||||
|
return r.db.Delete(&model.MaterialSubject{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountMaterials 统计学科下的资料数量
|
||||||
|
func (r *MaterialSubjectRepository) CountMaterials(subjectID string) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.Model(&model.MaterialFile{}).Where("subject_id = ?", subjectID).Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaterialFileRepository 文件资料仓储
|
||||||
|
type MaterialFileRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMaterialFileRepository(db *gorm.DB) *MaterialFileRepository {
|
||||||
|
return &MaterialFileRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListQueryParams 查询参数
|
||||||
|
type MaterialFileQueryParams struct {
|
||||||
|
SubjectID string
|
||||||
|
FileType string
|
||||||
|
Status string
|
||||||
|
Keyword string
|
||||||
|
Page int
|
||||||
|
PageSize int
|
||||||
|
SortBy string
|
||||||
|
SortOrder string
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取资料列表(支持筛选和分页)
|
||||||
|
func (r *MaterialFileRepository) List(params MaterialFileQueryParams) ([]*model.MaterialFile, int64, error) {
|
||||||
|
var files []*model.MaterialFile
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := r.db.Model(&model.MaterialFile{})
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
if params.SubjectID != "" {
|
||||||
|
query = query.Where("subject_id = ?", params.SubjectID)
|
||||||
|
}
|
||||||
|
if params.FileType != "" {
|
||||||
|
query = query.Where("file_type = ?", params.FileType)
|
||||||
|
}
|
||||||
|
if params.Status != "" {
|
||||||
|
query = query.Where("status = ?", params.Status)
|
||||||
|
}
|
||||||
|
if params.Keyword != "" {
|
||||||
|
keyword := "%" + params.Keyword + "%"
|
||||||
|
query = query.Where("title LIKE ? OR description LIKE ?", keyword, keyword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计总数
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
sortBy := params.SortBy
|
||||||
|
if sortBy == "" {
|
||||||
|
sortBy = "created_at"
|
||||||
|
}
|
||||||
|
sortOrder := params.SortOrder
|
||||||
|
if sortOrder == "" {
|
||||||
|
sortOrder = "DESC"
|
||||||
|
}
|
||||||
|
orderClause := sortBy + " " + sortOrder
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
page := params.Page
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize := params.PageSize
|
||||||
|
if pageSize < 1 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
// 查询
|
||||||
|
if err := query.Order(orderClause).Offset(offset).Limit(pageSize).Find(&files).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID 根据ID获取资料详情
|
||||||
|
func (r *MaterialFileRepository) GetByID(id string) (*model.MaterialFile, error) {
|
||||||
|
var file model.MaterialFile
|
||||||
|
if err := r.db.First(&file, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByIDWithSubject 根据ID获取资料详情(包含学科信息)
|
||||||
|
func (r *MaterialFileRepository) GetByIDWithSubject(id string) (*model.MaterialFile, error) {
|
||||||
|
var file model.MaterialFile
|
||||||
|
if err := r.db.Preload("Subject").First(&file, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建资料
|
||||||
|
func (r *MaterialFileRepository) Create(file *model.MaterialFile) error {
|
||||||
|
return r.db.Create(file).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新资料
|
||||||
|
func (r *MaterialFileRepository) Update(file *model.MaterialFile) error {
|
||||||
|
return r.db.Save(file).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除资料
|
||||||
|
func (r *MaterialFileRepository) Delete(id string) error {
|
||||||
|
return r.db.Delete(&model.MaterialFile{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementDownloadCount 增加下载次数
|
||||||
|
func (r *MaterialFileRepository) IncrementDownloadCount(id string) error {
|
||||||
|
return r.db.Model(&model.MaterialFile{}).Where("id = ?", id).
|
||||||
|
UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBySubjectID 根据学科ID获取资料列表
|
||||||
|
func (r *MaterialFileRepository) GetBySubjectID(subjectID string, page, pageSize int) ([]*model.MaterialFile, int64, error) {
|
||||||
|
var files []*model.MaterialFile
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := r.db.Model(&model.MaterialFile{}).Where("subject_id = ? AND status = ?", subjectID, model.MaterialStatusActive)
|
||||||
|
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&files).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search 搜索资料
|
||||||
|
func (r *MaterialFileRepository) Search(keyword string, page, pageSize int) ([]*model.MaterialFile, int64, error) {
|
||||||
|
var files []*model.MaterialFile
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := r.db.Model(&model.MaterialFile{}).Where("status = ?", model.MaterialStatusActive)
|
||||||
|
if keyword != "" {
|
||||||
|
kw := "%" + keyword + "%"
|
||||||
|
query = query.Where("title LIKE ? OR description LIKE ?", kw, kw)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&files).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHotMaterials 获取热门资料(按下载次数排序)
|
||||||
|
func (r *MaterialFileRepository) GetHotMaterials(limit int) ([]*model.MaterialFile, error) {
|
||||||
|
var files []*model.MaterialFile
|
||||||
|
err := r.db.
|
||||||
|
Where("status = ?", model.MaterialStatusActive).
|
||||||
|
Order("download_count DESC").
|
||||||
|
Limit(limit).
|
||||||
|
Find(&files).Error
|
||||||
|
return files, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestMaterials 获取最新资料
|
||||||
|
func (r *MaterialFileRepository) GetLatestMaterials(limit int) ([]*model.MaterialFile, error) {
|
||||||
|
var files []*model.MaterialFile
|
||||||
|
err := r.db.
|
||||||
|
Where("status = ?", model.MaterialStatusActive).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(limit).
|
||||||
|
Find(&files).Error
|
||||||
|
return files, err
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ type Router struct {
|
|||||||
adminDashboardHandler *handler.AdminDashboardHandler
|
adminDashboardHandler *handler.AdminDashboardHandler
|
||||||
adminLogHandler *handler.AdminLogHandler
|
adminLogHandler *handler.AdminLogHandler
|
||||||
qrcodeHandler *handler.QRCodeHandler
|
qrcodeHandler *handler.QRCodeHandler
|
||||||
|
materialHandler *handler.MaterialHandler
|
||||||
logService *service.LogService
|
logService *service.LogService
|
||||||
jwtService *service.JWTService
|
jwtService *service.JWTService
|
||||||
casbinService service.CasbinService
|
casbinService service.CasbinService
|
||||||
@@ -62,6 +63,7 @@ func New(
|
|||||||
adminDashboardHandler *handler.AdminDashboardHandler,
|
adminDashboardHandler *handler.AdminDashboardHandler,
|
||||||
adminLogHandler *handler.AdminLogHandler,
|
adminLogHandler *handler.AdminLogHandler,
|
||||||
qrcodeHandler *handler.QRCodeHandler,
|
qrcodeHandler *handler.QRCodeHandler,
|
||||||
|
materialHandler *handler.MaterialHandler,
|
||||||
logService *service.LogService,
|
logService *service.LogService,
|
||||||
activityService service.UserActivityService,
|
activityService service.UserActivityService,
|
||||||
casbinService service.CasbinService,
|
casbinService service.CasbinService,
|
||||||
@@ -94,6 +96,7 @@ func New(
|
|||||||
adminDashboardHandler: adminDashboardHandler,
|
adminDashboardHandler: adminDashboardHandler,
|
||||||
adminLogHandler: adminLogHandler,
|
adminLogHandler: adminLogHandler,
|
||||||
qrcodeHandler: qrcodeHandler,
|
qrcodeHandler: qrcodeHandler,
|
||||||
|
materialHandler: materialHandler,
|
||||||
logService: logService,
|
logService: logService,
|
||||||
jwtService: jwtService,
|
jwtService: jwtService,
|
||||||
casbinService: casbinService,
|
casbinService: casbinService,
|
||||||
@@ -262,6 +265,21 @@ func (r *Router) setupRoutes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 学习资料路由(公开)
|
||||||
|
if r.materialHandler != nil {
|
||||||
|
materials := v1.Group("/materials")
|
||||||
|
{
|
||||||
|
// 学科相关
|
||||||
|
materials.GET("/subjects", r.materialHandler.ListSubjects)
|
||||||
|
materials.GET("/subjects/:id", r.materialHandler.GetSubject)
|
||||||
|
// 资料相关
|
||||||
|
materials.GET("", r.materialHandler.ListMaterials)
|
||||||
|
materials.GET("/search", r.materialHandler.SearchMaterials)
|
||||||
|
materials.GET("/:id", r.materialHandler.GetMaterial)
|
||||||
|
materials.GET("/:id/download", r.materialHandler.DownloadMaterial)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 投票选项路由
|
// 投票选项路由
|
||||||
voteOptions := v1.Group("/vote-options")
|
voteOptions := v1.Group("/vote-options")
|
||||||
voteOptions.Use(authMiddleware)
|
voteOptions.Use(authMiddleware)
|
||||||
@@ -508,6 +526,21 @@ func (r *Router) setupRoutes() {
|
|||||||
logs.GET("/export", r.adminLogHandler.ExportLogs)
|
logs.GET("/export", r.adminLogHandler.ExportLogs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 学习资料管理
|
||||||
|
if r.materialHandler != nil {
|
||||||
|
// 学科管理
|
||||||
|
admin.GET("/materials/subjects", r.materialHandler.AdminListSubjects)
|
||||||
|
admin.POST("/materials/subjects", r.materialHandler.AdminCreateSubject)
|
||||||
|
admin.PUT("/materials/subjects/:id", r.materialHandler.AdminUpdateSubject)
|
||||||
|
admin.DELETE("/materials/subjects/:id", r.materialHandler.AdminDeleteSubject)
|
||||||
|
// 资料管理
|
||||||
|
admin.GET("/materials", r.materialHandler.AdminListMaterials)
|
||||||
|
admin.GET("/materials/:id", r.materialHandler.AdminGetMaterial)
|
||||||
|
admin.POST("/materials", r.materialHandler.AdminCreateMaterial)
|
||||||
|
admin.PUT("/materials/:id", r.materialHandler.AdminUpdateMaterial)
|
||||||
|
admin.DELETE("/materials/:id", r.materialHandler.AdminDeleteMaterial)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
172
internal/service/material_service.go
Normal file
172
internal/service/material_service.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"carrot_bbs/internal/model"
|
||||||
|
"carrot_bbs/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrSubjectHasMaterials 学科下存在资料,无法删除
|
||||||
|
var ErrSubjectHasMaterials = errors.New("学科下存在资料,无法删除")
|
||||||
|
|
||||||
|
// MaterialService 学习资料服务接口
|
||||||
|
type MaterialService interface {
|
||||||
|
// 学科相关
|
||||||
|
ListActiveSubjects() ([]*model.MaterialSubject, error)
|
||||||
|
ListAllSubjects() ([]*model.MaterialSubject, error)
|
||||||
|
GetSubjectByID(id string) (*model.MaterialSubject, error)
|
||||||
|
GetSubjectByIDWithMaterialCount(id string) (*model.MaterialSubject, int64, error)
|
||||||
|
CreateSubject(name, icon, color, description string, sortOrder int, isActive bool) (*model.MaterialSubject, error)
|
||||||
|
UpdateSubject(id, name, icon, color, description string, sortOrder int, isActive bool) (*model.MaterialSubject, error)
|
||||||
|
DeleteSubject(id string) error
|
||||||
|
|
||||||
|
// 资料相关
|
||||||
|
ListMaterials(params repository.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error)
|
||||||
|
GetMaterialByID(id string) (*model.MaterialFile, error)
|
||||||
|
GetMaterialByIDWithSubject(id string) (*model.MaterialFile, error)
|
||||||
|
GetMaterialsBySubject(subjectID string, page, pageSize int) ([]*model.MaterialFile, int64, error)
|
||||||
|
SearchMaterials(keyword string, page, pageSize int) ([]*model.MaterialFile, int64, error)
|
||||||
|
GetHotMaterials(limit int) ([]*model.MaterialFile, error)
|
||||||
|
GetLatestMaterials(limit int) ([]*model.MaterialFile, error)
|
||||||
|
CreateMaterial(file *model.MaterialFile) error
|
||||||
|
UpdateMaterial(file *model.MaterialFile) error
|
||||||
|
DeleteMaterial(id string) error
|
||||||
|
IncrementDownloadCount(id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// materialServiceImpl 学习资料服务实现
|
||||||
|
type materialServiceImpl struct {
|
||||||
|
subjectRepo *repository.MaterialSubjectRepository
|
||||||
|
fileRepo *repository.MaterialFileRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMaterialService 创建学习资料服务
|
||||||
|
func NewMaterialService(
|
||||||
|
subjectRepo *repository.MaterialSubjectRepository,
|
||||||
|
fileRepo *repository.MaterialFileRepository,
|
||||||
|
) MaterialService {
|
||||||
|
return &materialServiceImpl{
|
||||||
|
subjectRepo: subjectRepo,
|
||||||
|
fileRepo: fileRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// 学科相关方法
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) ListActiveSubjects() ([]*model.MaterialSubject, error) {
|
||||||
|
return s.subjectRepo.ListActive()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) ListAllSubjects() ([]*model.MaterialSubject, error) {
|
||||||
|
return s.subjectRepo.ListAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) GetSubjectByID(id string) (*model.MaterialSubject, error) {
|
||||||
|
return s.subjectRepo.GetByID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) GetSubjectByIDWithMaterialCount(id string) (*model.MaterialSubject, int64, error) {
|
||||||
|
subject, err := s.subjectRepo.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
count, err := s.subjectRepo.CountMaterials(id)
|
||||||
|
if err != nil {
|
||||||
|
return subject, 0, nil
|
||||||
|
}
|
||||||
|
return subject, count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) CreateSubject(name, icon, color, description string, sortOrder int, isActive bool) (*model.MaterialSubject, error) {
|
||||||
|
subject := &model.MaterialSubject{
|
||||||
|
Name: name,
|
||||||
|
Icon: icon,
|
||||||
|
Color: color,
|
||||||
|
Description: description,
|
||||||
|
SortOrder: sortOrder,
|
||||||
|
IsActive: isActive,
|
||||||
|
}
|
||||||
|
if err := s.subjectRepo.Create(subject); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return subject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) UpdateSubject(id, name, icon, color, description string, sortOrder int, isActive bool) (*model.MaterialSubject, error) {
|
||||||
|
subject, err := s.subjectRepo.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
subject.Name = name
|
||||||
|
subject.Icon = icon
|
||||||
|
subject.Color = color
|
||||||
|
subject.Description = description
|
||||||
|
subject.SortOrder = sortOrder
|
||||||
|
subject.IsActive = isActive
|
||||||
|
if err := s.subjectRepo.Update(subject); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return subject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) DeleteSubject(id string) error {
|
||||||
|
// 检查学科下是否有资料
|
||||||
|
count, err := s.subjectRepo.CountMaterials(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return ErrSubjectHasMaterials
|
||||||
|
}
|
||||||
|
return s.subjectRepo.Delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================
|
||||||
|
// 资料相关方法
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) ListMaterials(params repository.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error) {
|
||||||
|
return s.fileRepo.List(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) GetMaterialByID(id string) (*model.MaterialFile, error) {
|
||||||
|
return s.fileRepo.GetByID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) GetMaterialByIDWithSubject(id string) (*model.MaterialFile, error) {
|
||||||
|
return s.fileRepo.GetByIDWithSubject(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) GetMaterialsBySubject(subjectID string, page, pageSize int) ([]*model.MaterialFile, int64, error) {
|
||||||
|
return s.fileRepo.GetBySubjectID(subjectID, page, pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) SearchMaterials(keyword string, page, pageSize int) ([]*model.MaterialFile, int64, error) {
|
||||||
|
return s.fileRepo.Search(keyword, page, pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) GetHotMaterials(limit int) ([]*model.MaterialFile, error) {
|
||||||
|
return s.fileRepo.GetHotMaterials(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) GetLatestMaterials(limit int) ([]*model.MaterialFile, error) {
|
||||||
|
return s.fileRepo.GetLatestMaterials(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) CreateMaterial(file *model.MaterialFile) error {
|
||||||
|
return s.fileRepo.Create(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) UpdateMaterial(file *model.MaterialFile) error {
|
||||||
|
return s.fileRepo.Update(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) DeleteMaterial(id string) error {
|
||||||
|
return s.fileRepo.Delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *materialServiceImpl) IncrementDownloadCount(id string) error {
|
||||||
|
return s.fileRepo.IncrementDownloadCount(id)
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ var HandlerSet = wire.NewSet(
|
|||||||
handler.NewAdminDashboardHandler,
|
handler.NewAdminDashboardHandler,
|
||||||
handler.NewAdminLogHandler,
|
handler.NewAdminLogHandler,
|
||||||
handler.NewQRCodeHandler,
|
handler.NewQRCodeHandler,
|
||||||
|
handler.NewMaterialHandler,
|
||||||
|
|
||||||
// 需要特殊处理的 Handler
|
// 需要特殊处理的 Handler
|
||||||
ProvideUserHandler,
|
ProvideUserHandler,
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ var RepositorySet = wire.NewSet(
|
|||||||
repository.NewVoteRepository,
|
repository.NewVoteRepository,
|
||||||
repository.NewChannelRepository,
|
repository.NewChannelRepository,
|
||||||
repository.NewScheduleRepository,
|
repository.NewScheduleRepository,
|
||||||
|
repository.NewMaterialSubjectRepository,
|
||||||
|
repository.NewMaterialFileRepository,
|
||||||
ProvideUserActivityRepository,
|
ProvideUserActivityRepository,
|
||||||
|
|
||||||
// 日志相关仓储
|
// 日志相关仓储
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ var ServiceSet = wire.NewSet(
|
|||||||
ProvideAdminDashboardService,
|
ProvideAdminDashboardService,
|
||||||
ProvideQRCodeLoginService,
|
ProvideQRCodeLoginService,
|
||||||
ProvideHotRankWorker,
|
ProvideHotRankWorker,
|
||||||
|
ProvideMaterialService,
|
||||||
|
|
||||||
// 日志服务
|
// 日志服务
|
||||||
ProvideAsyncLogManager,
|
ProvideAsyncLogManager,
|
||||||
@@ -369,6 +370,14 @@ func ProvideQRCodeLoginService(
|
|||||||
return service.NewQRCodeLoginService(redisClient, sseHub, jwtService, userService, activityService, logService)
|
return service.NewQRCodeLoginService(redisClient, sseHub, jwtService, userService, activityService, logService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProvideMaterialService 提供学习资料服务
|
||||||
|
func ProvideMaterialService(
|
||||||
|
subjectRepo *repository.MaterialSubjectRepository,
|
||||||
|
fileRepo *repository.MaterialFileRepository,
|
||||||
|
) service.MaterialService {
|
||||||
|
return service.NewMaterialService(subjectRepo, fileRepo)
|
||||||
|
}
|
||||||
|
|
||||||
// parseDuration 解析时长字符串
|
// parseDuration 解析时长字符串
|
||||||
func parseDuration(s string) (time.Duration, error) {
|
func parseDuration(s string) (time.Duration, error) {
|
||||||
return time.ParseDuration(s)
|
return time.ParseDuration(s)
|
||||||
|
|||||||
Reference in New Issue
Block a user