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,
|
||||
adminLogHandler *handler.AdminLogHandler,
|
||||
qrcodeHandler *handler.QRCodeHandler,
|
||||
materialHandler *handler.MaterialHandler,
|
||||
logService *service.LogService,
|
||||
activityService service.UserActivityService,
|
||||
casbinService service.CasbinService,
|
||||
@@ -73,6 +74,7 @@ func ProvideRouter(
|
||||
adminDashboardHandler,
|
||||
adminLogHandler,
|
||||
qrcodeHandler,
|
||||
materialHandler,
|
||||
logService,
|
||||
activityService,
|
||||
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{},
|
||||
&UserRole{},
|
||||
&CasbinRule{},
|
||||
|
||||
// 学习资料相关
|
||||
&MaterialSubject{},
|
||||
&MaterialFile{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -184,6 +188,11 @@ func autoMigrate(db *gorm.DB) error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -321,8 +330,18 @@ func seedPermissions(db *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedChannels 初始化频道种子数据(按 slug 幂等)
|
||||
// seedChannels 初始化频道种子数据(仅在表为空时)
|
||||
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{
|
||||
{Name: "跳蚤市场", Slug: "market", Description: "二手交易与闲置发布", SortOrder: 10, 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},
|
||||
}
|
||||
|
||||
for _, item := range defaultChannels {
|
||||
var existing Channel
|
||||
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
|
||||
}
|
||||
if err := db.Create(&defaultChannels).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zap.L().Info("Seeded channels successfully", zap.Int("count", len(defaultChannels)))
|
||||
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 关闭数据库连接
|
||||
func CloseDB(db *gorm.DB) {
|
||||
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
|
||||
adminLogHandler *handler.AdminLogHandler
|
||||
qrcodeHandler *handler.QRCodeHandler
|
||||
materialHandler *handler.MaterialHandler
|
||||
logService *service.LogService
|
||||
jwtService *service.JWTService
|
||||
casbinService service.CasbinService
|
||||
@@ -62,6 +63,7 @@ func New(
|
||||
adminDashboardHandler *handler.AdminDashboardHandler,
|
||||
adminLogHandler *handler.AdminLogHandler,
|
||||
qrcodeHandler *handler.QRCodeHandler,
|
||||
materialHandler *handler.MaterialHandler,
|
||||
logService *service.LogService,
|
||||
activityService service.UserActivityService,
|
||||
casbinService service.CasbinService,
|
||||
@@ -94,6 +96,7 @@ func New(
|
||||
adminDashboardHandler: adminDashboardHandler,
|
||||
adminLogHandler: adminLogHandler,
|
||||
qrcodeHandler: qrcodeHandler,
|
||||
materialHandler: materialHandler,
|
||||
logService: logService,
|
||||
jwtService: jwtService,
|
||||
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.Use(authMiddleware)
|
||||
@@ -508,6 +526,21 @@ func (r *Router) setupRoutes() {
|
||||
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.NewAdminLogHandler,
|
||||
handler.NewQRCodeHandler,
|
||||
handler.NewMaterialHandler,
|
||||
|
||||
// 需要特殊处理的 Handler
|
||||
ProvideUserHandler,
|
||||
|
||||
@@ -23,6 +23,8 @@ var RepositorySet = wire.NewSet(
|
||||
repository.NewVoteRepository,
|
||||
repository.NewChannelRepository,
|
||||
repository.NewScheduleRepository,
|
||||
repository.NewMaterialSubjectRepository,
|
||||
repository.NewMaterialFileRepository,
|
||||
ProvideUserActivityRepository,
|
||||
|
||||
// 日志相关仓储
|
||||
|
||||
@@ -54,6 +54,7 @@ var ServiceSet = wire.NewSet(
|
||||
ProvideAdminDashboardService,
|
||||
ProvideQRCodeLoginService,
|
||||
ProvideHotRankWorker,
|
||||
ProvideMaterialService,
|
||||
|
||||
// 日志服务
|
||||
ProvideAsyncLogManager,
|
||||
@@ -369,6 +370,14 @@ func ProvideQRCodeLoginService(
|
||||
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 解析时长字符串
|
||||
func parseDuration(s string) (time.Duration, error) {
|
||||
return time.ParseDuration(s)
|
||||
|
||||
Reference in New Issue
Block a user