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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions backend/internal/handler/daily_recommendation.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ func (h *DailyRecommendationHandler) Create(c *gin.Context) {
pkg.Unauthorized(c)
return
}
userID := middleware.GetUserID(c)

input := &service.CreateDailyRecInput{
SongName: c.PostForm("songName"),
Artist: c.PostForm("artist"),
Expand All @@ -51,7 +49,7 @@ func (h *DailyRecommendationHandler) Create(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), longTimeout)
defer cancel()

result, err := h.svc.CreateRecommendation(ctx, userID, submitterInfo, input)
result, err := h.svc.CreateRecommendation(ctx, user.Name, submitterInfo, input)
if err != nil {
logrus.WithError(err).Warn("create daily recommendation failed")
pkg.BadRequest(c, err.Error())
Expand All @@ -63,7 +61,7 @@ func (h *DailyRecommendationHandler) Create(c *gin.Context) {

// UploadTemp POST /api/v1/daily-recommendations/upload-temp
func (h *DailyRecommendationHandler) UploadTemp(c *gin.Context) {
if middleware.GetUserID(c) == "" {
if middleware.GetUserName(c) == "" {
pkg.Unauthorized(c)
return
}
Expand Down Expand Up @@ -101,7 +99,7 @@ func (h *DailyRecommendationHandler) UploadTemp(c *gin.Context) {
// UploadTempFromURL POST /api/v1/daily-recommendations/upload-temp-from-url
// 服务端代理下载图片 URL 并转存到临时区
func (h *DailyRecommendationHandler) UploadTempFromURL(c *gin.Context) {
if middleware.GetUserID(c) == "" {
if middleware.GetUserName(c) == "" {
pkg.Unauthorized(c)
return
}
Expand Down Expand Up @@ -131,7 +129,7 @@ func (h *DailyRecommendationHandler) UploadTempFromURL(c *gin.Context) {

// CheckDate GET /api/v1/daily-recommendations/check-date?date=YYYY-MM-DD
func (h *DailyRecommendationHandler) CheckDate(c *gin.Context) {
if middleware.GetUserID(c) == "" {
if middleware.GetUserName(c) == "" {
pkg.Unauthorized(c)
return
}
Expand Down Expand Up @@ -198,16 +196,16 @@ func (h *DailyRecommendationHandler) GetByDate(c *gin.Context) {

// ListMine GET /api/v1/daily-recommendations/submissions
func (h *DailyRecommendationHandler) ListMine(c *gin.Context) {
userID := middleware.GetUserID(c)
if userID == "" {
username := middleware.GetUserName(c)
if username == "" {
pkg.Unauthorized(c)
return
}

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

result, err := h.svc.ListBySubmitter(ctx, userID)
result, err := h.svc.ListBySubmitter(ctx, username)
if err != nil {
pkg.InternalError(c, "查询列表失败")
return
Expand Down
12 changes: 5 additions & 7 deletions backend/internal/handler/search_ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ func (h *SearchIPHandler) Create(c *gin.Context) {
return
}

userID := middleware.GetUserID(c)

// 读取 title 字段
title := c.PostForm("title")

Expand Down Expand Up @@ -74,7 +72,7 @@ func (h *SearchIPHandler) Create(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), longTimeout)
defer cancel()

result, err := h.svc.CreateSubmission(ctx, userID, submitterInfo, &service.CreateSearchIPInput{
result, err := h.svc.CreateSubmission(ctx, user.Name, submitterInfo, &service.CreateSearchIPInput{
Title: title,
Data: data,
TempKeys: tempKeys,
Expand All @@ -91,7 +89,7 @@ func (h *SearchIPHandler) Create(c *gin.Context) {
// UploadTemp POST /api/v1/search-ip/upload-temp
// 上传单张图片到临时区,返回 { tempKey }
func (h *SearchIPHandler) UploadTemp(c *gin.Context) {
if middleware.GetUserID(c) == "" {
if middleware.GetUserName(c) == "" {
pkg.Unauthorized(c)
return
}
Expand Down Expand Up @@ -133,16 +131,16 @@ type MatchRequest struct {

// List GET /api/v1/search-ip/submissions
func (h *SearchIPHandler) List(c *gin.Context) {
userID := middleware.GetUserID(c)
if userID == "" {
username := middleware.GetUserName(c)
if username == "" {
pkg.Unauthorized(c)
return
}

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

result, err := h.svc.ListBySubmitter(ctx, userID)
result, err := h.svc.ListBySubmitter(ctx, username)
if err != nil {
logrus.WithError(err).Error("list search ip submissions failed")
pkg.InternalError(c, "查询列表失败")
Expand Down
2 changes: 2 additions & 0 deletions backend/internal/handler/submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ func writeSubmissionErr(c *gin.Context, err error) {
pkg.Fail(c, http.StatusForbidden, http.StatusForbidden, "无权操作该投稿")
case errors.Is(err, service.ErrMissingUnrearrangedReason):
pkg.BadRequest(c, "勾选未重排歌词时必须填写原因")
case errors.Is(err, service.ErrMissingRevisionTarget):
pkg.BadRequest(c, "修正已有歌词时必须选择要修正的原歌词")
case errors.Is(err, service.ErrInvalidInput):
pkg.BadRequest(c, "请求参数非法")
case errors.Is(err, service.ErrUpstreamUnavailable):
Expand Down
18 changes: 17 additions & 1 deletion backend/internal/model/submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ const (
StatusRevised = "revised"
)

// 投稿提交类型枚举
const (
// SubmissionTypeNew 新歌词提交
SubmissionTypeNew = "new"
// SubmissionTypeRevision 修正已有歌词(对比曲库现有歌词)
SubmissionTypeRevision = "revision"
)

// SubmissionLanguage 投稿语言枚举
const (
LangOthers = "others"
Expand Down Expand Up @@ -126,7 +134,15 @@ type Submission struct {
// IsUnrearranged 为 true 表示上传的是未经重排的原始歌词文件
IsUnrearranged bool `gorm:"column:is_unrearranged;not null;default:false" json:"isUnrearranged"`
// UnrearrangedReason 投稿者填写的「为什么用未重排版」说明,详情页展示
UnrearrangedReason string `gorm:"column:unrearranged_reason;type:varchar(500);not null;default:''" json:"unrearrangedReason"`
UnrearrangedReason string `gorm:"column:unrearranged_reason;type:varchar(500);not null;default:''" json:"unrearrangedReason"`
// SubmissionType 投稿类型:new=新歌词提交,revision=修正已有歌词
SubmissionType string `gorm:"column:submission_type;type:varchar(20);not null;default:'new'" json:"submissionType"`
// TargetSongID 修正投稿指向的曲库歌曲 id(songs.id)
TargetSongID *int64 `gorm:"column:target_song_id" json:"targetSongId,omitempty"`
// TargetRawLyricFile 修正投稿指向的原歌词文件名(raw-lyrics 目录)
TargetRawLyricFile string `gorm:"column:target_raw_lyric_file;type:varchar(255);not null;default:''" json:"targetRawLyricFile,omitempty"`
// DiffSnapshot 修正投稿的差异快照(前端内置 diff 工具生成)
DiffSnapshot JSONObject `gorm:"column:diff_snapshot;type:jsonb;not null;default:'{}'" json:"diffSnapshot,omitempty"`
Status string `gorm:"type:varchar(20);not null;default:'pending'" json:"status"`
IsFirstSubmission bool `gorm:"column:is_first_submission;not null;default:false" json:"isFirstSubmission"` // 首通过前提交的投稿
Submitter string `gorm:"type:varchar(100);not null" json:"submitter"`
Expand Down
1 change: 1 addition & 0 deletions backend/internal/repository/submission_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ func (r *SubmissionRepo) List(ctx context.Context, q ListQuery) (*ListResult, er
order = "updated_at DESC"
}
if err := db.
Omit("DiffSnapshot").
Order(order).
Offset((q.Page - 1) * q.Limit).
Limit(q.Limit).
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/service/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ var ErrForbidden = errors.New("forbidden")
// ErrMissingUnrearrangedReason 勾选了「未重排歌词」但没填原因(原因强制必填)
var ErrMissingUnrearrangedReason = errors.New("missing unrearranged reason")

// ErrMissingRevisionTarget 修正投稿未选择要修正的原歌词
var ErrMissingRevisionTarget = errors.New("missing revision target")

// 歌词/索引模块

// ErrLyricNotFound 歌词未找到
Expand Down
82 changes: 63 additions & 19 deletions backend/internal/service/submission_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,16 @@ type CreateSubmissionInput struct {
IsUnrearranged bool `json:"isUnrearranged"`
// UnrearrangedReason 未重排的原因说明(仅 IsUnrearranged 为 true 时有意义)
UnrearrangedReason string `json:"unrearrangedReason"`
Type string `json:"type"`
Status string `json:"status"`
// SubmissionType 投稿类型
SubmissionType string `json:"submissionType"`
// TargetSongID 修正目标曲库歌曲 id
TargetSongID *int64 `json:"targetSongId"`
// TargetRawLyricFile 修正目标原歌词文件名
TargetRawLyricFile string `json:"targetRawLyricFile"`
// DiffSnapshot 修正差异快照
DiffSnapshot map[string]any `json:"diffSnapshot"`
Type string `json:"type"`
Status string `json:"status"`
} // Submission 投稿 DTO
type Submission struct {
ID int64 `json:"id"`
Expand All @@ -58,6 +66,10 @@ type Submission struct {
Languages []string `json:"languages"`
IsUnrearranged bool `json:"isUnrearranged"`
UnrearrangedReason string `json:"unrearrangedReason"`
SubmissionType string `json:"submissionType"`
TargetSongID *int64 `json:"targetSongId,omitempty"`
TargetRawLyricFile string `json:"targetRawLyricFile,omitempty"`
DiffSnapshot map[string]any `json:"diffSnapshot,omitempty"`
Status string `json:"status"`
IsFirstSubmission bool `json:"isFirstSubmission"`
Submitter string `json:"submitter"`
Expand Down Expand Up @@ -254,24 +266,39 @@ func (s *SubmissionService) Create(ctx context.Context, user *SubmissionUser, in
return 0, ErrMissingUnrearrangedReason
}

// 提交类型:默认新歌词提交;修正投稿必须指明要修正的原歌词
submissionType := model.SubmissionTypeNew
targetFile := sanitize(in.TargetRawLyricFile, 255)
if in.SubmissionType == model.SubmissionTypeRevision {
submissionType = model.SubmissionTypeRevision
if targetFile == "" {
return 0, ErrMissingRevisionTarget
}
// 文件名只允许裸文件名,防止路径穿越到其他目录
if strings.Contains(targetFile, "..") || strings.ContainsAny(targetFile, `/\`) {
return 0, ErrInvalidInput
}
}

sub := &model.Submission{
Title: title,
Artist: strings.Join(artists, ", "),
Album: strings.Join(album, ", "),
NcmID: extractPlatformId(in.Metadata, "ncm_music_id"),
QqID: extractPlatformId(in.Metadata, "qq_music_id"),
AmID: extractPlatformId(in.Metadata, "apple_music_id"),
SpotifyID: extractPlatformId(in.Metadata, "spotify_id"),
FileName: in.FileName,
Notes: notes,
Tags: normalizeTags(in.Tags),
Metadata: normalizeMetadata(in.Metadata),
Language: primaryLang,
Languages: languages,
Status: status,
Submitter: user.Name,
SubmitterInfo: model.UserInfo{Username: user.Name, DisplayName: user.DisplayName, Avatar: user.Avatar},
Provider: "casdoor",
Title: title,
Artist: strings.Join(artists, ", "),
Album: strings.Join(album, ", "),
NcmID: extractPlatformId(in.Metadata, "ncm_music_id"),
QqID: extractPlatformId(in.Metadata, "qq_music_id"),
AmID: extractPlatformId(in.Metadata, "apple_music_id"),
SpotifyID: extractPlatformId(in.Metadata, "spotify_id"),
FileName: in.FileName,
Notes: notes,
Tags: normalizeTags(in.Tags),
Metadata: normalizeMetadata(in.Metadata),
Language: primaryLang,
Languages: languages,
SubmissionType: submissionType,
Status: status,
Submitter: user.Name,
SubmitterInfo: model.UserInfo{Username: user.Name, DisplayName: user.DisplayName, Avatar: user.Avatar},
Provider: "casdoor",
}

// 未重排投稿:记录标记与原因说明
Expand All @@ -280,6 +307,17 @@ func (s *SubmissionService) Create(ctx context.Context, user *SubmissionUser, in
sub.UnrearrangedReason = sanitize(in.UnrearrangedReason, 500)
}

// 修正投稿:记录目标歌词与差异快照,详情页直接读快照渲染
if submissionType == model.SubmissionTypeRevision {
if in.TargetSongID != nil && *in.TargetSongID > 0 {
sub.TargetSongID = in.TargetSongID
}
sub.TargetRawLyricFile = targetFile
if in.DiffSnapshot != nil {
sub.DiffSnapshot = model.JSONObject(in.DiffSnapshot)
}
}

// 事务写入
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.subRepo.Create(ctx, tx, sub); err != nil {
Expand Down Expand Up @@ -320,6 +358,8 @@ func (s *SubmissionService) List(ctx context.Context, user *SubmissionUser, q Su
items := make([]Submission, len(res.Items))
for i, m := range res.Items {
items[i] = convertSubmission(m)
// 列表不下发差异明细,避免单页响应过大;详情接口才返回完整快照
items[i].DiffSnapshot = nil
}
return &SubmissionListResult{Total: res.Total, Items: items}, nil
}
Expand Down Expand Up @@ -1141,6 +1181,10 @@ func convertSubmission(m model.Submission) Submission {
Languages: ensureStringSlice(m.Languages),
IsUnrearranged: m.IsUnrearranged,
UnrearrangedReason: m.UnrearrangedReason,
SubmissionType: m.SubmissionType,
TargetSongID: m.TargetSongID,
TargetRawLyricFile: m.TargetRawLyricFile,
DiffSnapshot: map[string]any(m.DiffSnapshot),
Status: m.Status,
IsFirstSubmission: m.IsFirstSubmission,
Submitter: m.Submitter,
Expand Down
18 changes: 18 additions & 0 deletions backend/migrations/001_init.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,15 @@ CREATE TABLE submissions (
-- 不以格式校验作为提交门槛;unrearranged_reason 强制必填。
is_unrearranged BOOLEAN NOT NULL DEFAULT FALSE,
unrearranged_reason VARCHAR(500) NOT NULL DEFAULT '',
-- submission_type:投稿类型。new=新歌词提交;revision=修正已有歌词
-- (以曲库现有歌词为基线,提交时生成差异快照)。
submission_type VARCHAR(20) NOT NULL DEFAULT 'new',
-- target_song_id / target_raw_lyric_file:修正投稿指向的曲库歌曲与原歌词文件,仅 revision 有值。
target_song_id BIGINT,
target_raw_lyric_file VARCHAR(255) NOT NULL DEFAULT '',
-- diff_snapshot:修正投稿提交时由前端内置 diff 工具生成的差异快照
-- (统计/行级明细/元数据变更/目标歌曲),详情页直接读取渲染。
diff_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
-- is_first_submission:提交时投稿人名下还没有任何 approved 投稿则置 TRUE,
-- 即「首篇通过之前提交的每一篇都算首次」(可同时存在多篇)。
Expand Down Expand Up @@ -244,10 +253,19 @@ COMMENT ON COLUMN submissions.reviewer_info IS
'审核员信息(username/displayName/avatar),审核占用或审核结论时写入';
COMMENT ON COLUMN submissions.is_first_submission IS
'是否为「首篇通过审核之前」提交的投稿;提交时判定,事后不回改,历史数据不回填';
COMMENT ON COLUMN submissions.submission_type IS
'投稿类型:new=新歌词提交,revision=修正已有歌词';
COMMENT ON COLUMN submissions.target_song_id IS
'修正投稿指向的曲库歌曲 id(songs.id),仅 revision 有值';
COMMENT ON COLUMN submissions.target_raw_lyric_file IS
'修正投稿指向的原歌词文件名(raw-lyrics 目录),仅 revision 有值';
COMMENT ON COLUMN submissions.diff_snapshot IS
'修正投稿提交时的差异快照(统计/行级明细/元数据变更/目标歌曲),由前端内置 diff 工具生成';

CREATE INDEX idx_submissions_submitter ON submissions(submitter);
CREATE INDEX idx_submissions_status ON submissions(status);
CREATE INDEX idx_submissions_language ON submissions(language);
CREATE INDEX idx_submissions_submission_type ON submissions(submission_type);
-- 多选语言筛选用 GIN 索引
CREATE INDEX idx_submissions_languages ON submissions USING GIN (languages);
CREATE INDEX idx_submissions_created ON submissions(created_at DESC);
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/components/RecommendCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import QRCode from 'qrcode';
import { domToPng } from 'modern-screenshot';
import type { DailyRecommendation } from '@/lib/types';
import { parseMarkupText } from '@/lib/markup';
import { usernameOf } from '@/lib/username';
import { api } from '@/lib/api';
import { queryKeys } from '@/lib/query';
import { resolvedThemeAtom } from '@/atoms/theme';
Expand Down Expand Up @@ -211,10 +212,11 @@ export function RecommendCard({
[recommendation.comment]
);

const submitterName = usernameOf(recommendation.submitter);
const username =
(recommendation.submitterInfo?.displayName as string) ||
(recommendation.submitterInfo?.name as string) ||
recommendation.submitter ||
submitterName ||
'匿名用户';

const ncmId = recommendation.ncmId;
Expand Down Expand Up @@ -393,9 +395,9 @@ export function RecommendCard({
/>
<div className="mt-2.5 text-right text-[13px] font-light text-ink-2">
--来自{' '}
{recommendation.submitter ? (
{submitterName ? (
<Link
to={`/user/${encodeURIComponent(recommendation.submitter)}`}
to={`/user/${encodeURIComponent(submitterName)}`}
className="text-inherit underline-offset-2 hover:text-primary hover:underline"
>
@{username}
Expand Down
Loading
Loading