Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions backend/cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func Run() {
historyRepo := repository.NewReviewHistoryRepo(db)
fileHistoryRepo := repository.NewFileHistoryRepo(db)
commentRepo := repository.NewCommentRepo(db)
reportRepo := repository.NewReviewReportRepo(db)
reviewerRepo := repository.NewReviewerRepo(db)
adminRepo := repository.NewAdminRepo(db)
notifRepo := repository.NewNotificationRepo(db)
Expand Down Expand Up @@ -123,6 +124,10 @@ func Run() {
viewerSvc := service.NewViewerService(nil, subRepo) // hub 后面注入
submissionSvc := service.NewSubmissionService(subRepo, audioRepo, historyRepo, fileHistoryRepo, commentRepo, fileSvc, db)
reviewSvc := service.NewReviewService(subRepo, historyRepo, fileSvc, githubSvc, viewerSvc, db)
revisionSvc := service.NewRevisionService(
subRepo, historyRepo, reportRepo, commentRepo, audioRepo,
fileSvc, githubSvc, viewerSvc, db,
)
batchSvc := service.NewBatchService(songRepo)

// 消息中心 service
Expand Down Expand Up @@ -192,6 +197,7 @@ func Run() {
// 注入通知服务(T1~T3 触发点)
submissionSvc.SetNotifier(notifSvc)
reviewSvc.SetNotifier(notifSvc)
revisionSvc.SetNotifier(notifSvc)
// 90 天历史消息清理
notifSvc.StartCleanupTask(appCtx)

Expand Down Expand Up @@ -222,6 +228,7 @@ func Run() {
// 投稿 handler
submissionH := handler.NewSubmissionHandler(submissionSvc, reviewerCache)
reviewH := handler.NewReviewHandler(reviewSvc)
revisionH := handler.NewRevisionHandler(revisionSvc)
commentH := handler.NewCommentHandler(submissionSvc)
uploadH := handler.NewUploadHandler(fileSvc, submissionSvc)
searchIpH := handler.NewSearchIPHandler(searchIpSvc)
Expand All @@ -247,6 +254,7 @@ func Run() {
Auth: authH,
Submission: submissionH,
Review: reviewH,
Revision: revisionH,
Comment: commentH,
Upload: uploadH,
SearchIP: searchIpH,
Expand Down
160 changes: 160 additions & 0 deletions backend/internal/handler/revision.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package handler

import (
"context"
"net/http"
"strconv"

"github.com/gin-gonic/gin"

"github.com/amll-dev/amll-hub/backend/internal/pkg"
"github.com/amll-dev/amll-hub/backend/internal/service"
)

// RevisionHandler 审核员修订版 handler
type RevisionHandler struct {
svc *service.RevisionService
}

// NewRevisionHandler 创建审核修订 handler
func NewRevisionHandler(svc *service.RevisionService) *RevisionHandler {
return &RevisionHandler{svc: svc}
}

// GetEditorContext GET /api/v1/submissions/:id/editor-context
// 审核员进入编辑器时拉取:原投稿 TTML + 可选音频列表
func (h *RevisionHandler) GetEditorContext(c *gin.Context) {
user := currentUser(c)
if user == nil {
pkg.Unauthorized(c)
return
}
id, ok := parseIDParam(c)
if !ok {
return
}

ctx, cancel := context.WithTimeout(c.Request.Context(), longTimeout)
defer cancel()

data, err := h.svc.GetEditorContext(ctx, id)
if err != nil {
writeSubmissionErr(c, err)
return
}
pkg.OK(c, data)
}

// Save POST /api/v1/submissions/:id/revision
func (h *RevisionHandler) Save(c *gin.Context) {
user := currentUser(c)
if user == nil {
pkg.Unauthorized(c)
return
}
id, ok := parseIDParam(c)
if !ok {
return
}

var req service.SaveRevisionInput
if err := c.ShouldBindJSON(&req); err != nil {
pkg.BadRequest(c, "参数错误")
return
}

ctx, cancel := context.WithTimeout(c.Request.Context(), longTimeout)
defer cancel()

if err := h.svc.SaveRevision(ctx, user, id, &req); err != nil {
writeSubmissionErr(c, err)
return
}
pkg.OKWithMsg(c, nil, "已保存")
}

// GetRevisionContent GET /api/v1/submissions/:id/revision/ttml
// 投稿者与审核员查看修订版 TTML 原文
func (h *RevisionHandler) GetRevisionContent(c *gin.Context) {
user := currentUser(c)
if user == nil {
pkg.Unauthorized(c)
return
}
id, ok := parseIDParam(c)
if !ok {
return
}

ctx, cancel := context.WithTimeout(c.Request.Context(), defaultTimeout)
defer cancel()

content, err := h.svc.GetRevisionContent(ctx, user, id)
if err != nil {
writeSubmissionErr(c, err)
return
}
c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(content))
}

// Adopt POST /api/v1/submissions/:id/revision/adopt
// 投稿者确认采用审核员修订版
func (h *RevisionHandler) Adopt(c *gin.Context) {
user := currentUser(c)
if user == nil {
pkg.Unauthorized(c)
return
}
id, ok := parseIDParam(c)
if !ok {
return
}

ctx, cancel := context.WithTimeout(c.Request.Context(), longTimeout)
defer cancel()

if err := h.svc.AdoptRevision(ctx, user, id); err != nil {
writeSubmissionErr(c, err)
return
}
pkg.OKWithMsg(c, nil, "已采用审核员的修订版,投稿通过")
}

// Reject POST /api/v1/submissions/:id/revision/reject
// 投稿者选择保留原版
func (h *RevisionHandler) Reject(c *gin.Context) {
user := currentUser(c)
if user == nil {
pkg.Unauthorized(c)
return
}
id, ok := parseIDParam(c)
if !ok {
return
}

var req struct {
Reason string `json:"reason"`
}
// 允许空 body:reason可省略
_ = c.ShouldBindJSON(&req)

ctx, cancel := context.WithTimeout(c.Request.Context(), longTimeout)
defer cancel()

if err := h.svc.RejectRevision(ctx, user, id, req.Reason); err != nil {
writeSubmissionErr(c, err)
return
}
pkg.OKWithMsg(c, nil, "已保留你的版本,投稿转为需修改")
}

// parseIDParam 解析路径中的 :id,非法时直接写错误响应并返回 false
func parseIDParam(c *gin.Context) (int64, bool) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil || id <= 0 {
pkg.BadRequest(c, "无效的 id")
return 0, false
}
return id, true
}
12 changes: 12 additions & 0 deletions backend/internal/mail/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ var resultStyles = map[string]resultStyle{
BadgeBG: "#fdf3e3",
BadgeFG: "#c97a0e",
},
// revised:审核员已改好,等投稿者确认。
"revised": {
Label: "待确认",
Headline: "审核员已提交修订版歌词",
Lead: "你的投稿已完成审核修订,审核员提交了一版修订歌词。",
Tip: "请在详情页查看修订内容与审核报告,确认后即可直接采用;如需保留原版可以选择不采用。",
Preheader: "歌词修订版已提交,请确认是否采用。",
Button: "查看并确认",
Accent: "#7c4dbe",
BadgeBG: "#f1ebfa",
BadgeFG: "#7c4dbe",
},
"closed": {
Label: "已关闭",
Headline: "你的投稿已超时关闭",
Expand Down
2 changes: 2 additions & 0 deletions backend/internal/model/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const (
NotifyResultNeedRevision = "need_revision"
NotifyResultMissingAudio = "missing_audio"
NotifyResultClosed = "closed"
// NotifyResultRevised 审核员已提交修订版 TTML,等投稿者确认
NotifyResultRevised = "revised"
)

// Notification 站内消息
Expand Down
67 changes: 67 additions & 0 deletions backend/internal/model/submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const (
StatusApproved = "approved"
StatusRejected = "rejected"
StatusClosed = "closed"
// StatusRevised 审核员已提交修订版 TTML,等待投稿者确认。
// 投稿者确认采用后直接转 approved
StatusRevised = "revised"
)

// SubmissionLanguage 投稿语言枚举
Expand Down Expand Up @@ -138,10 +141,57 @@ type Submission struct {
Reviewer string `gorm:"type:varchar(100)" json:"reviewer,omitempty"`
ReviewedAt *time.Time `gorm:"column:reviewed_at;type:timestamptz" json:"reviewedAt,omitempty"`
ReviewComment string `gorm:"column:review_comment;type:text" json:"reviewComment,omitempty"`

// 审核员修订版:编辑器保存时勾选(上传修改后的 TTML)才会写入
RevisionFileKey string `gorm:"column:revision_file_key;type:varchar(500);not null;default:''" json:"revisionFileKey,omitempty"`
RevisionMetadata JSONObject `gorm:"column:revision_metadata;type:jsonb;not null;default:'{}'" json:"revisionMetadata,omitempty"`
RevisionTTML *string `gorm:"column:revision_ttml;type:text" json:"revisionTtml,omitempty"`
RevisionAt *time.Time `gorm:"column:revision_at;type:timestamptz" json:"revisionAt,omitempty"`
RevisionReviewer string `gorm:"column:revision_reviewer;type:varchar(100)" json:"revisionReviewer,omitempty"`
RevisionReviewerInfo *UserInfo `gorm:"column:revision_reviewer_info;type:jsonb" json:"revisionReviewerInfo,omitempty"`
}

func (Submission) TableName() string { return "submissions" }

// SnapshotRevision 返回当前修订版字段的副本
func (s *Submission) SnapshotRevision() *Submission {
cp := &Submission{
RevisionFileKey: s.RevisionFileKey,
RevisionMetadata: s.RevisionMetadata,
RevisionAt: s.RevisionAt,
RevisionReviewer: s.RevisionReviewer,
RevisionReviewerInfo: s.RevisionReviewerInfo,
}
if s.RevisionTTML != nil {
ttml := *s.RevisionTTML
cp.RevisionTTML = &ttml
}
return cp
}

// RestoreRevision 把快照里的修订版字段写回自身,用于回滚
func (s *Submission) RestoreRevision(saved *Submission) {
if saved == nil {
return
}
s.RevisionFileKey = saved.RevisionFileKey
s.RevisionMetadata = saved.RevisionMetadata
s.RevisionTTML = saved.RevisionTTML
s.RevisionAt = saved.RevisionAt
s.RevisionReviewer = saved.RevisionReviewer
s.RevisionReviewerInfo = saved.RevisionReviewerInfo
}

// ClearRevision 清空修订版字段
func (s *Submission) ClearRevision() {
s.RevisionFileKey = ""
s.RevisionMetadata = JSONObject{}
s.RevisionTTML = nil
s.RevisionAt = nil
s.RevisionReviewer = ""
s.RevisionReviewerInfo = nil
}

// SubmissionAudio 音频附件
type SubmissionAudio struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Expand Down Expand Up @@ -184,6 +234,23 @@ type SubmissionFileHistory struct {

func (SubmissionFileHistory) TableName() string { return "submission_file_history" }

// ReviewReport 审核报告
type ReviewReport struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
SubmissionID int64 `gorm:"column:submission_id;not null" json:"submissionId"`
Reviewer string `gorm:"type:varchar(100);not null" json:"reviewer"`
ReviewerInfo UserInfo `gorm:"type:jsonb;not null;default:'{}'" json:"reviewerInfo"`
// Action 本次报告对应的审核动作(revision/approve/reject/missing_audio)
Action string `gorm:"type:varchar(20);not null" json:"action"`
// HasRevision 本次是否上传了修订版 TTML
HasRevision bool `gorm:"column:has_revision;not null;default:false" json:"hasRevision"`
ReportMD string `gorm:"column:report_md;type:text;not null;default:''" json:"reportMd"`
Structured JSONObject `gorm:"type:jsonb;not null;default:'{}'" json:"structured"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"createdAt"`
}

func (ReviewReport) TableName() string { return "review_reports" }

// Comment 普通评论
type Comment struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Expand Down
Loading
Loading