feat(material): add material handling and routing functionality
Some checks failed
Build Backend / build (push) Failing after 12m1s
Build Backend / build-docker (push) Has been skipped

- 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:
lafay
2026-03-25 20:44:12 +08:00
parent 28a45caad3
commit 0ef8e5a7e1
10 changed files with 1122 additions and 22 deletions

View File

@@ -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
View 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"
}