feat: share code.

This commit is contained in:
2026-04-28 21:47:51 +08:00
parent 641703ca69
commit ea44ea0153
4 changed files with 172 additions and 0 deletions
+141
View File
@@ -2,6 +2,7 @@ package controllers
import (
"crypto/md5"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
@@ -24,6 +25,8 @@ const (
lessonPlanStorageDir = "storage/lesson_plans"
lessonPlanFieldName = "file"
lessonPlanCleanupDays = 30
shareCodeExpiry = 5 * time.Minute
shareCodeDigits = 6
)
type LessonPlanController struct {
@@ -35,6 +38,12 @@ type lessonPlanPaginationParams struct {
PageSize int `form:"pageSize,default=10"`
}
type lessonPlanShareCodeResponse struct {
Code string `json:"code"`
ExpiresAt int64 `json:"expiresAt"`
FileID uint `json:"fileId"`
}
func NewLessonPlanController() *LessonPlanController {
return &LessonPlanController{DB: config.DB}
}
@@ -216,6 +225,101 @@ func (lc *LessonPlanController) Download(c *gin.Context) {
c.FileAttachment(record.FilePath, record.OriginalFilename)
}
func (lc *LessonPlanController) GenerateShareCode(c *gin.Context) {
record, err := lc.findLessonPlan(c.Param("id"))
if err != nil {
respondLessonPlanLookupError(c, err)
return
}
userID, username, 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()
expiresAt := now.Add(shareCodeExpiry).UnixMilli()
var created models.AppFileShareCode
if err := lc.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&models.AppFileShareCode{}).
Where("app_file_id = ? AND is_active = ?", record.ID, true).
Update("is_active", false).Error; err != nil {
return err
}
shareCode, err := generateUniqueShareCode(tx, now.UnixMilli())
if err != nil {
return err
}
created = models.AppFileShareCode{
AppFileID: record.ID,
ShareCode: shareCode,
GeneratedByID: userID,
GeneratedByName: username,
ExpiresAt: expiresAt,
IsActive: true,
}
return tx.Create(&created).Error
}); err != nil {
writeError(c, http.StatusInternalServerError, "failed to generate share code")
return
}
writeSuccess(c, http.StatusOK, "share code generated", lessonPlanShareCodeResponse{
Code: created.ShareCode,
ExpiresAt: created.ExpiresAt,
FileID: record.ID,
})
}
func (lc *LessonPlanController) DownloadByShareCode(c *gin.Context) {
shareCode := strings.TrimSpace(c.Param("code"))
if len(shareCode) != shareCodeDigits {
writeError(c, http.StatusBadRequest, "invalid share code")
return
}
now := time.Now().UnixMilli()
var share models.AppFileShareCode
if err := lc.DB.
Where("share_code = ? AND is_active = ? AND expires_at > ?", shareCode, true, now).
Order("id DESC").
First(&share).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
writeError(c, http.StatusNotFound, "share code expired or not found")
return
}
writeError(c, http.StatusInternalServerError, "failed to query share code")
return
}
var record models.AppFile
if err := lc.DB.Where("id = ? AND file_type = ?", share.AppFileID, models.AppFileTypeLessonPlan).First(&record).Error; err != nil {
respondLessonPlanLookupError(c, err)
return
}
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 {
@@ -336,6 +440,9 @@ func canAccessLessonPlan(record models.AppFile, userID uint, role models.UserRol
}
func deleteLessonPlanRecord(db *gorm.DB, record *models.AppFile) error {
if err := db.Where("app_file_id = ?", record.ID).Delete(&models.AppFileShareCode{}).Error; err != nil {
return fmt.Errorf("failed to delete share codes: %w", err)
}
if err := os.Remove(record.FilePath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove file: %w", err)
}
@@ -360,3 +467,37 @@ func respondLessonPlanLookupError(c *gin.Context, err error) {
}
writeError(c, http.StatusInternalServerError, "failed to query lesson plan")
}
func generateUniqueShareCode(db *gorm.DB, nowMillis int64) (string, error) {
for i := 0; i < 10; i++ {
shareCode, err := randomNumericCode(shareCodeDigits)
if err != nil {
return "", err
}
var count int64
if err := db.Model(&models.AppFileShareCode{}).
Where("share_code = ? AND is_active = ? AND expires_at > ?", shareCode, true, nowMillis).
Count(&count).Error; err != nil {
return "", err
}
if count == 0 {
return shareCode, nil
}
}
return "", fmt.Errorf("failed to allocate unique share code")
}
func randomNumericCode(length int) (string, error) {
if length <= 0 {
return "", fmt.Errorf("invalid code length")
}
buffer := make([]byte, length)
if _, err := rand.Read(buffer); err != nil {
return "", err
}
digits := make([]byte, length)
for i, value := range buffer {
digits[i] = byte('0' + (value % 10))
}
return string(digits), nil
}