mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
优化 strm 同步记录:支持删除记录并展示上传数量统计
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -117,6 +117,13 @@ export const strmAPI = {
|
||||
.get<StrmSyncRecord[]>('/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
|
||||
|
||||
@@ -211,7 +211,7 @@ export function StrmManagePage() {
|
||||
onCancel={cancelSync}
|
||||
/>
|
||||
|
||||
<RecordSection records={records} />
|
||||
<RecordSection records={records} onDeleted={refresh} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -453,13 +453,48 @@ function SyncPathSection({
|
||||
|
||||
// ─── 同步记录 ────────────────────────────────────────────────────────────────
|
||||
|
||||
function RecordSection({ records }: { records: StrmSyncRecord[] }) {
|
||||
function RecordSection({ records, onDeleted }: { records: StrmSyncRecord[]; onDeleted: () => void }) {
|
||||
const [deletingId, setDeletingId] = useState<string | null>(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 (
|
||||
<section className="glass-panel space-y-3 p-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<History size={18} className="text-brand-500" />
|
||||
<h2 className="font-display text-lg font-semibold text-ink-600">同步记录</h2>
|
||||
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-[11px] text-sand-500">{records.length}</span>
|
||||
{records.length > 0 && (
|
||||
<button type="button" onClick={clearRecords} className={iconButtonCls + ' ml-auto'}>
|
||||
<Trash2 size={14} />
|
||||
清空
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{records.length === 0 ? (
|
||||
<p className="rounded-xl bg-gray-50 px-4 py-6 text-center text-sm text-sand-500">还没有同步记录</p>
|
||||
@@ -475,8 +510,10 @@ function RecordSection({ records }: { records: StrmSyncRecord[] }) {
|
||||
<th className="px-3 py-2 text-right">新增/更新</th>
|
||||
<th className="px-3 py-2 text-right">跳过</th>
|
||||
<th className="px-3 py-2 text-right">下载元数据</th>
|
||||
<th className="px-3 py-2 text-right">上传元数据</th>
|
||||
<th className="px-3 py-2 text-right">清理</th>
|
||||
<th className="px-3 py-2">说明</th>
|
||||
<th className="px-3 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -502,8 +539,20 @@ function RecordSection({ records }: { records: StrmSyncRecord[] }) {
|
||||
<td className="px-3 py-2 text-right text-brand-500">{record.new_strm}</td>
|
||||
<td className="px-3 py-2 text-right text-gray-500">{record.skipped}</td>
|
||||
<td className="px-3 py-2 text-right">{record.new_meta}</td>
|
||||
<td className="px-3 py-2 text-right">{record.uploaded ?? 0}</td>
|
||||
<td className="px-3 py-2 text-right">{record.pruned}</td>
|
||||
<td className="max-w-[260px] truncate px-3 py-2 text-xs text-sand-500">{record.message}</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deleteRecord(record)}
|
||||
disabled={deletingId === record.id}
|
||||
title="删除记录"
|
||||
className="rounded-md p-1 text-sand-400 transition hover:bg-rose-50 hover:text-rose-500 disabled:opacity-40"
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user