Files
hr_data_analyzer/controllers/lesson_plan.go
T
2026-04-28 19:38:02 +08:00

355 lines
9.9 KiB
Go

package controllers
import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"hr_receiver/config"
"hr_receiver/models"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
const (
lessonPlanMaxSize = 10 * 1024 * 1024
lessonPlanStorageDir = "storage/lesson_plans"
lessonPlanFieldName = "file"
lessonPlanCleanupDays = 30
)
type LessonPlanController struct {
DB *gorm.DB
}
type lessonPlanPaginationParams struct {
PageNum int `form:"pageNum,default=1"`
PageSize int `form:"pageSize,default=10"`
}
func NewLessonPlanController() *LessonPlanController {
return &LessonPlanController{DB: config.DB}
}
func (lc *LessonPlanController) Upload(c *gin.Context) {
fileHeader, err := c.FormFile(lessonPlanFieldName)
if err != nil {
writeError(c, http.StatusBadRequest, "missing file")
return
}
if err := validateLessonPlanFileHeader(fileHeader); err != nil {
writeError(c, http.StatusBadRequest, err.Error())
return
}
uploaderID, uploaderName, ok := currentUser(c)
if !ok {
writeError(c, http.StatusUnauthorized, "invalid user context")
return
}
src, err := fileHeader.Open()
if err != nil {
writeError(c, http.StatusInternalServerError, "failed to open upload")
return
}
defer src.Close()
if err := os.MkdirAll(lessonPlanStorageDir, 0o755); err != nil {
writeError(c, http.StatusInternalServerError, "failed to initialize storage")
return
}
tempFile, err := os.CreateTemp(lessonPlanStorageDir, "upload-*.docx")
if err != nil {
writeError(c, http.StatusInternalServerError, "failed to create temp file")
return
}
tempPath := tempFile.Name()
defer func() {
_ = tempFile.Close()
if _, statErr := os.Stat(tempPath); statErr == nil {
_ = os.Remove(tempPath)
}
}()
hasher := md5.New()
size, err := io.Copy(io.MultiWriter(tempFile, hasher), src)
if err != nil {
writeError(c, http.StatusInternalServerError, "failed to save upload")
return
}
if size > lessonPlanMaxSize {
writeError(c, http.StatusBadRequest, "file exceeds 10MB limit")
return
}
md5Value := hex.EncodeToString(hasher.Sum(nil))
var existing models.AppFile
if err := lc.DB.Where("md5 = ? AND file_type = ?", md5Value, models.AppFileTypeLessonPlan).First(&existing).Error; err == nil {
writeSuccess(c, http.StatusConflict, "duplicate file", existing)
return
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
writeError(c, http.StatusInternalServerError, "failed to check duplicate file")
return
}
storedFilename := buildStoredLessonPlanFilename(md5Value, fileHeader.Filename)
finalPath := filepath.Join(lessonPlanStorageDir, storedFilename)
if err := os.Rename(tempPath, finalPath); err != nil {
writeError(c, http.StatusInternalServerError, "failed to finalize upload")
return
}
record := models.AppFile{
FileType: models.AppFileTypeLessonPlan,
OriginalFilename: fileHeader.Filename,
StoredFilename: storedFilename,
FilePath: finalPath,
ContentType: normalizeLessonPlanContentType(fileHeader.Header.Get("Content-Type")),
FileSize: size,
MD5: md5Value,
UploaderID: uploaderID,
UploaderName: uploaderName,
}
if err := lc.DB.Create(&record).Error; err != nil {
_ = os.Remove(finalPath)
writeError(c, http.StatusInternalServerError, "failed to persist file metadata")
return
}
writeSuccess(c, http.StatusCreated, "upload success", record)
}
func (lc *LessonPlanController) List(c *gin.Context) {
var records []models.AppFile
if err := lc.DB.Where("file_type = ?", models.AppFileTypeLessonPlan).Order("created_at DESC").Find(&records).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to list lesson plans")
return
}
writeSuccess(c, http.StatusOK, "query success", records)
}
func (lc *LessonPlanController) Page(c *gin.Context) {
var params lessonPlanPaginationParams
if err := c.ShouldBindQuery(&params); err != nil {
writeError(c, http.StatusBadRequest, err.Error())
return
}
if params.PageNum < 1 {
params.PageNum = 1
}
if params.PageSize < 1 || params.PageSize > 100 {
params.PageSize = 10
}
offset := (params.PageNum - 1) * params.PageSize
var total int64
var records []models.AppFile
query := lc.DB.Model(&models.AppFile{}).Where("file_type = ?", models.AppFileTypeLessonPlan)
if err := query.Count(&total).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to count lesson plans")
return
}
if err := query.Order("created_at DESC").Offset(offset).Limit(params.PageSize).Find(&records).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to query lesson plans")
return
}
writeSuccess(c, http.StatusOK, "query success", gin.H{
"list": records,
"pagination": gin.H{
"currentPage": params.PageNum,
"pageSize": params.PageSize,
"totalList": total,
"totalPage": int((total + int64(params.PageSize) - 1) / int64(params.PageSize)),
},
})
}
func (lc *LessonPlanController) Download(c *gin.Context) {
record, err := lc.findLessonPlan(c.Param("id"))
if err != nil {
respondLessonPlanLookupError(c, err)
return
}
userID, _, ok := currentUser(c)
if !ok {
writeError(c, http.StatusUnauthorized, "invalid user context")
return
}
role := currentUserRole(c)
if !canAccessLessonPlan(record, userID, role) {
writeError(c, http.StatusForbidden, "permission denied")
return
}
now := time.Now().UnixMilli()
if err := lc.DB.Model(&models.AppFile{}).
Where("id = ?", record.ID).
Updates(map[string]interface{}{
"download_count": gorm.Expr("download_count + 1"),
"last_download_at": now,
}).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to update download stats")
return
}
c.FileAttachment(record.FilePath, record.OriginalFilename)
}
func (lc *LessonPlanController) Delete(c *gin.Context) {
record, err := lc.findLessonPlan(c.Param("id"))
if err != nil {
respondLessonPlanLookupError(c, err)
return
}
userID, _, ok := currentUser(c)
if !ok {
writeError(c, http.StatusUnauthorized, "invalid user context")
return
}
role := currentUserRole(c)
if !canAccessLessonPlan(record, userID, role) {
writeError(c, http.StatusForbidden, "permission denied")
return
}
if err := deleteLessonPlanRecord(lc.DB, &record); err != nil {
writeError(c, http.StatusInternalServerError, err.Error())
return
}
writeSuccess(c, http.StatusOK, "delete success", nil)
}
func (lc *LessonPlanController) CleanupExpiredFiles() error {
cutoffMillis := time.Now().AddDate(0, 0, -lessonPlanCleanupDays).UnixMilli()
cutoffTime := time.Now().AddDate(0, 0, -lessonPlanCleanupDays)
var records []models.AppFile
if err := lc.DB.
Where("file_type = ?", models.AppFileTypeLessonPlan).
Where("(last_download_at IS NOT NULL AND last_download_at < ?) OR (last_download_at IS NULL AND created_at < ?)", cutoffMillis, cutoffTime).
Find(&records).Error; err != nil {
return err
}
for i := range records {
if err := deleteLessonPlanRecord(lc.DB, &records[i]); err != nil {
return err
}
}
return nil
}
func StartLessonPlanCleanupJob(db *gorm.DB) {
controller := &LessonPlanController{DB: db}
go func() {
runCleanup := func() {
if err := controller.CleanupExpiredFiles(); err != nil {
fmt.Printf("lesson plan cleanup failed: %v\n", err)
}
}
runCleanup()
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for range ticker.C {
runCleanup()
}
}()
}
func validateLessonPlanFileHeader(fileHeader *multipart.FileHeader) error {
if fileHeader.Size > lessonPlanMaxSize {
return fmt.Errorf("file exceeds 10MB limit")
}
if !strings.EqualFold(filepath.Ext(fileHeader.Filename), ".docx") {
return fmt.Errorf("only .docx files are allowed")
}
contentType := strings.ToLower(strings.TrimSpace(fileHeader.Header.Get("Content-Type")))
if contentType != "" && contentType != "application/vnd.openxmlformats-officedocument.wordprocessingml.document" {
return fmt.Errorf("content-type must be .docx")
}
return nil
}
func buildStoredLessonPlanFilename(md5Value, originalName string) string {
return fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), md5Value, strings.ToLower(filepath.Ext(originalName)))
}
func normalizeLessonPlanContentType(contentType string) string {
if strings.TrimSpace(contentType) == "" {
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}
return contentType
}
func currentUser(c *gin.Context) (uint, string, bool) {
userIDValue, idExists := c.Get("userID")
usernameValue, usernameExists := c.Get("username")
if !idExists || !usernameExists {
return 0, "", false
}
userID, ok := userIDValue.(uint)
if !ok {
return 0, "", false
}
username, ok := usernameValue.(string)
if !ok {
return 0, "", false
}
return userID, username, true
}
func currentUserRole(c *gin.Context) models.UserRole {
roleValue, exists := c.Get("role")
if !exists {
return ""
}
role, _ := roleValue.(models.UserRole)
return role
}
func canAccessLessonPlan(record models.AppFile, userID uint, role models.UserRole) bool {
if record.UploaderID == userID {
return true
}
return role == models.UserRoleSuperAdmin || role == models.UserRoleRegionAdmin
}
func deleteLessonPlanRecord(db *gorm.DB, record *models.AppFile) error {
if err := os.Remove(record.FilePath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove file: %w", err)
}
if err := db.Unscoped().Delete(&models.AppFile{}, record.ID).Error; err != nil {
return fmt.Errorf("failed to delete metadata: %w", err)
}
return nil
}
func (lc *LessonPlanController) findLessonPlan(id string) (models.AppFile, error) {
var record models.AppFile
if err := lc.DB.Where("id = ? AND file_type = ?", id, models.AppFileTypeLessonPlan).First(&record).Error; err != nil {
return record, err
}
return record, nil
}
func respondLessonPlanLookupError(c *gin.Context, err error) {
if errors.Is(err, gorm.ErrRecordNotFound) {
writeError(c, http.StatusNotFound, "lesson plan not found")
return
}
writeError(c, http.StatusInternalServerError, "failed to query lesson plan")
}