diff --git a/internal/handler/routes_admin.go b/internal/handler/routes_admin.go index fa6c6b0..ae806c8 100644 --- a/internal/handler/routes_admin.go +++ b/internal/handler/routes_admin.go @@ -43,6 +43,8 @@ func registerAdminStrmRoutes(admin *gin.RouterGroup, svc *service.Container) { admin.POST("/strm/paths/:id/sync", startStrmSyncHandler(svc)) admin.POST("/strm/paths/:id/cancel", cancelStrmSyncHandler(svc)) admin.GET("/strm/records", listStrmSyncRecordsHandler(svc)) + admin.DELETE("/strm/records/:id", deleteStrmSyncRecordHandler(svc)) + admin.DELETE("/strm/records", clearStrmSyncRecordsHandler(svc)) admin.GET("/strm/local-dirs", listStrmLocalDirsHandler(svc)) admin.GET("/strm/downloads", downloadQueueHandler(svc)) diff --git a/internal/handler/strm.go b/internal/handler/strm.go index a786f6f..09c3da1 100644 --- a/internal/handler/strm.go +++ b/internal/handler/strm.go @@ -300,6 +300,31 @@ func listStrmSyncRecordsHandler(svc *service.Container) gin.HandlerFunc { } } +func deleteStrmSyncRecordHandler(svc *service.Container) gin.HandlerFunc { + return func(c *gin.Context) { + if c.Param("id") == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "缺少记录 ID"}) + return + } + if err := svc.Strm.DeleteSyncRecord(c.Request.Context(), c.Param("id")); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"ok": true}) + } +} + +func clearStrmSyncRecordsHandler(svc *service.Container) gin.HandlerFunc { + return func(c *gin.Context) { + deleted, err := svc.Strm.ClearSyncRecords(c.Request.Context(), c.Query("path_id")) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"ok": true, "deleted": deleted}) + } +} + // ─── 下载/上传队列 ───────────────────────────────────────────────────────────── func downloadQueueHandler(svc *service.Container) gin.HandlerFunc { diff --git a/internal/repository/strm_repository.go b/internal/repository/strm_repository.go index c90b5dd..838dd62 100644 --- a/internal/repository/strm_repository.go +++ b/internal/repository/strm_repository.go @@ -170,6 +170,24 @@ func (r *StrmSyncRecordRepository) List(ctx context.Context, syncPathID string, return rows, err } +// Delete 删除单条同步记录(物理删除)。 +func (r *StrmSyncRecordRepository) Delete(ctx context.Context, id string) error { + return withSQLiteBusyRetry(ctx, func() error { + return r.db.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.StrmSyncRecord{}).Error + }) +} + +// DeleteBySyncPathID 删除某同步目录下的全部同步记录(删除同步目录时级联清理)。 +func (r *StrmSyncRecordRepository) DeleteBySyncPathID(ctx context.Context, syncPathID string) (int64, error) { + var count int64 + err := withSQLiteBusyRetry(ctx, func() error { + res := r.db.WithContext(ctx).Unscoped().Where("sync_path_id = ?", syncPathID).Delete(&model.StrmSyncRecord{}) + count = res.RowsAffected + return res.Error + }) + return count, err +} + // ─── StrmDownloadTask ────────────────────────────────────────────────────────── // StrmDownloadTaskRepository persists model.StrmDownloadTask. diff --git a/internal/service/strm_service.go b/internal/service/strm_service.go index 2d46af7..1857baa 100644 --- a/internal/service/strm_service.go +++ b/internal/service/strm_service.go @@ -449,6 +449,38 @@ func (s *StrmService) ListSyncRecords(ctx context.Context, pathID string, limit return s.repo.StrmSyncRecord.List(ctx, pathID, limit) } +// DeleteSyncRecord 删除单条同步记录。 +func (s *StrmService) DeleteSyncRecord(ctx context.Context, id string) error { + if err := s.repo.StrmSyncRecord.Delete(ctx, id); err != nil { + return err + } + return nil +} + +// ClearSyncRecords 清空某同步目录(pathID 为空则全部)的同步记录,返回删除条数。 +func (s *StrmService) ClearSyncRecords(ctx context.Context, pathID string) (int64, error) { + if pathID != "" { + return s.repo.StrmSyncRecord.DeleteBySyncPathID(ctx, pathID) + } + var total int64 + // 全量清空:分页拉取物理删除所有记录 + for { + rows, err := s.repo.StrmSyncRecord.List(ctx, "", 200) + if err != nil { + return total, err + } + if len(rows) == 0 { + return total, nil + } + for _, rec := range rows { + if err := s.repo.StrmSyncRecord.Delete(ctx, rec.ID); err != nil { + return total, err + } + } + total += int64(len(rows)) + } +} + // CreateSyncPath 校验并创建同步目录。 func (s *StrmService) CreateSyncPath(ctx context.Context, p *model.StrmSyncPath) (*model.StrmSyncPath, error) { if err := s.validateSyncPath(ctx, p); err != nil { diff --git a/internal/service/strm_sync.go b/internal/service/strm_sync.go index 8038fd3..7197618 100644 --- a/internal/service/strm_sync.go +++ b/internal/service/strm_sync.go @@ -235,8 +235,8 @@ func (s *StrmService) finishSync(p *model.StrmSyncPath, rec *model.StrmSyncRecor if rec.SyncType == model.StrmSyncTypeFull { syncTypeLabel = "全量" } - p.LastSyncMessage = fmt.Sprintf("[%s] 完成:新增/更新 %d 个 strm,跳过 %d 个,下载 %d 个元数据,清理 %d 个文件", - syncTypeLabel, rec.NewStrm, rec.Skipped, rec.NewMeta, rec.Pruned) + p.LastSyncMessage = fmt.Sprintf("[%s] 完成:新增/更新 %d 个 strm,跳过 %d 个,下载 %d 个元数据,上传 %d 个元数据,清理 %d 个文件", + syncTypeLabel, rec.NewStrm, rec.Skipped, rec.NewMeta, rec.Uploaded, rec.Pruned) } if err := s.repo.StrmSyncPath.Update(context.Background(), p); err != nil { s.log.Warn("update strm sync path failed", zap.Error(err)) @@ -244,7 +244,7 @@ func (s *StrmService) finishSync(p *model.StrmSyncPath, rec *model.StrmSyncRecor s.log.Info("strm sync finished", zap.String("path_id", p.ID), zap.String("sync_type", rec.SyncType), zap.String("status", status), zap.Int64("new_strm", rec.NewStrm), zap.Int64("skipped", rec.Skipped), zap.Int64("new_meta", rec.NewMeta), - zap.Int64("pruned", rec.Pruned), zap.String("message", message)) + zap.Int64("uploaded", rec.Uploaded), zap.Int64("pruned", rec.Pruned), zap.String("message", message)) } func (st *strmSyncState) run() error { diff --git a/web/src/api/strm.ts b/web/src/api/strm.ts index aaf7362..9310d23 100644 --- a/web/src/api/strm.ts +++ b/web/src/api/strm.ts @@ -117,6 +117,13 @@ export const strmAPI = { .get('/admin/strm/records', { params: pathId ? { path_id: pathId } : {} }) .then((r) => r.data), + deleteRecord: (id: string) => api.delete(`/admin/strm/records/${id}`).then((r) => r.data), + + clearRecords: (pathId?: string) => + api + .delete<{ deleted: number }>('/admin/strm/records', { params: pathId ? { path_id: pathId } : {} }) + .then((r) => r.data), + // ── 本地目录浏览(同步目录选择器) ──────────────────────── listLocalDirs: (path?: string) => api diff --git a/web/src/pages/StrmManagePage.tsx b/web/src/pages/StrmManagePage.tsx index c6768f0..8e2b334 100644 --- a/web/src/pages/StrmManagePage.tsx +++ b/web/src/pages/StrmManagePage.tsx @@ -211,7 +211,7 @@ export function StrmManagePage() { onCancel={cancelSync} /> - + )} @@ -453,13 +453,48 @@ function SyncPathSection({ // ─── 同步记录 ──────────────────────────────────────────────────────────────── -function RecordSection({ records }: { records: StrmSyncRecord[] }) { +function RecordSection({ records, onDeleted }: { records: StrmSyncRecord[]; onDeleted: () => void }) { + const [deletingId, setDeletingId] = useState(null) + + const deleteRecord = async (record: StrmSyncRecord) => { + const ok = await confirmAction({ message: '确定删除这条同步记录?', confirmText: '删除' }) + if (!ok) return + setDeletingId(record.id) + try { + await strmAPI.deleteRecord(record.id) + toast.success('已删除同步记录') + onDeleted() + } catch (err) { + toast.error(apiErrorMessage(err)) + } finally { + setDeletingId(null) + } + } + + const clearRecords = async () => { + const ok = await confirmAction({ message: '确定清空全部同步记录?此操作不可恢复。', confirmText: '清空' }) + if (!ok) return + try { + const res = await strmAPI.clearRecords() + toast.success(`已清空 ${res.deleted} 条同步记录`) + onDeleted() + } catch (err) { + toast.error(apiErrorMessage(err)) + } + } + return (

同步记录

{records.length} + {records.length > 0 && ( + + )}
{records.length === 0 ? (

还没有同步记录

@@ -475,8 +510,10 @@ function RecordSection({ records }: { records: StrmSyncRecord[] }) { 新增/更新 跳过 下载元数据 + 上传元数据 清理 说明 + @@ -502,8 +539,20 @@ function RecordSection({ records }: { records: StrmSyncRecord[] }) { {record.new_strm} {record.skipped} {record.new_meta} + {record.uploaded ?? 0} {record.pruned} {record.message} + + + ) })}