diff --git a/admin-frontend-vue/src/pages/records/RecordsHistoryPage.vue b/admin-frontend-vue/src/pages/records/RecordsHistoryPage.vue
index d90c0ecc..37b642f0 100644
--- a/admin-frontend-vue/src/pages/records/RecordsHistoryPage.vue
+++ b/admin-frontend-vue/src/pages/records/RecordsHistoryPage.vue
@@ -2,11 +2,12 @@
/** 历史生成记录页(module 13 task 259 对齐 admin panel-history):指定用户下拉 + 起止时间筛选、类型中文、≤3 缩略图、大图预览、空态。 */
import { formatDateTime } from '@/utils/datetime'
import { computed, onMounted, reactive, ref } from 'vue'
-import { fetchHistoryList, fetchHistoryUserOptions } from './history-api.ts'
+import { fetchHistoryList, fetchHistoryUserOptions, clearHistory } from './history-api.ts'
import type { HistoryUserOption } from './history-user-option.ts'
import type { HistoryRecordItem } from './history-dto.ts'
import { historyEmptyText, historyErrorText } from './history-feedback.ts'
import { historyPanelTypeLabel } from './history-type.ts'
+import { ElMessage, ElMessageBox } from 'element-plus'
const loading = ref(false)
const userLoading = ref(false)
@@ -98,6 +99,32 @@ function openPreview(item: HistoryRecordItem) {
previewVisible.value = true
}
+const clearing = ref(false)
+
+/** 清空全部生成记录:二次确认后调后端 DELETE /api/admin/history。 */
+async function clearAll() {
+ try {
+ await ElMessageBox.confirm('确定清空全部生成记录吗?该操作会删除所有历史数据,无法恢复。', '清理生成记录', {
+ confirmButtonText: '确认清空',
+ cancelButtonText: '取消',
+ type: 'warning',
+ })
+ } catch {
+ return
+ }
+ clearing.value = true
+ try {
+ const removed = await clearHistory()
+ ElMessage.success(`已清空 ${removed} 条生成记录`)
+ page.value = 1
+ await load()
+ } catch (error) {
+ ElMessage.error(error instanceof Error ? error.message : '清理失败')
+ } finally {
+ clearing.value = false
+ }
+}
+
onMounted(() => {
load()
loadUserOptions()
@@ -111,6 +138,9 @@ onMounted(() => {
生成记录
查看各用户在后台生成的历史记录与结果图。
+
+ 清理数据
+
diff --git a/admin-frontend-vue/src/pages/records/history-api.ts b/admin-frontend-vue/src/pages/records/history-api.ts
index e28f29a7..ce6a99d6 100644
--- a/admin-frontend-vue/src/pages/records/history-api.ts
+++ b/admin-frontend-vue/src/pages/records/history-api.ts
@@ -17,3 +17,11 @@ export async function fetchHistoryUserOptions(): Promise {
const { data } = await http.get('/api/admin/users', { params: { page: 1, page_size: 999 } })
return parseHistoryUserOptions(data)
}
+
+/** 清空全部生成记录(验收反馈:生成记录数据要可清理)。返回清理条数。 */
+export async function clearHistory(): Promise {
+ const { data } = await http.delete(HISTORY_ENDPOINT)
+ const record = data && typeof data === 'object' ? (data as Record) : null
+ const removed = record?.data
+ return typeof removed === 'number' ? removed : 0
+}
diff --git a/admin-frontend-vue/tests/align-history-clear.test.ts b/admin-frontend-vue/tests/align-history-clear.test.ts
new file mode 100644
index 00000000..f2086f2f
--- /dev/null
+++ b/admin-frontend-vue/tests/align-history-clear.test.ts
@@ -0,0 +1,34 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readSource } from './helpers.ts'
+
+// 验收反馈:生成记录的数据要可清理。
+// 契约:页面有「清理数据」入口(二次确认)、前端 DELETE /api/admin/history、后端清空端点与日志。
+
+test('align_history_clear_normal_primary_path', () => {
+ const page = readSource('src/pages/records/RecordsHistoryPage.vue')
+ assert.match(page, /清理数据/, '页头有清理数据按钮')
+ assert.match(page, /确定清空全部生成记录吗/, '清理前需二次确认')
+ assert.match(page, /clearHistory\(\)/, '页面调用清理 API')
+})
+
+test('align_history_clear_normal_variant_input', () => {
+ const api = readSource('src/pages/records/history-api.ts')
+ assert.match(api, /http\.delete\(HISTORY_ENDPOINT\)/, '清理走 DELETE /api/admin/history')
+ assert.match(api, /clearHistory/, '暴露 clearHistory 适配函数')
+})
+
+test('align_history_clear_boundary_empty_input', () => {
+ const page = readSource('src/pages/records/RecordsHistoryPage.vue')
+ assert.match(page, /已清空 \$\{removed\} 条生成记录/, '成功提示含清理条数')
+})
+
+test('align_history_clear_dependency_failure_returns_actionable_message', () => {
+ // 后端契约:DELETE /history 端点 + service 清空 + 中文排查日志。
+ const ctrl = readSource('../backend-java/src/main/java/com/nanri/aiimage/modules/imagehistory/controller/ImageHistoryController.java')
+ assert.match(ctrl, /@DeleteMapping\("\/history"\)/, '后端有 DELETE /api/admin/history 端点')
+ assert.match(ctrl, /admin_history/, '清理端点沿用生成记录模块权限')
+ const service = readSource('../backend-java/src/main/java/com/nanri/aiimage/modules/imagehistory/service/ImageHistoryService.java')
+ assert.match(service, /clearAllHistory/, 'service 有清空方法')
+ assert.match(service, /清空生成记录,共清理/, '清空操作留中文排查日志')
+})
diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/imagehistory/controller/ImageHistoryController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/imagehistory/controller/ImageHistoryController.java
index 84eb878a..c99360f6 100644
--- a/backend-java/src/main/java/com/nanri/aiimage/modules/imagehistory/controller/ImageHistoryController.java
+++ b/backend-java/src/main/java/com/nanri/aiimage/modules/imagehistory/controller/ImageHistoryController.java
@@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -43,4 +44,15 @@ public class ImageHistoryController {
ImageHistoryListVo vo = imageHistoryService.listHistory(page, pageSize, userId, timeStart, timeEnd);
return ApiResponse.success(vo);
}
+
+ @DeleteMapping("/history")
+ @Operation(summary = "清空全部生成记录")
+ public ApiResponse clearHistory(HttpServletRequest request) {
+ AdminUserEntity operator = adminAuthSupport.requireAdminOrInternal(request);
+ if (!permissionMenuService.hasAnyAdminMenu(operator, List.of("admin_history"))) {
+ throw new BusinessException(403, "无权访问生成记录模块");
+ }
+ long removed = imageHistoryService.clearAllHistory(operator.getUsername());
+ return ApiResponse.success(removed);
+ }
}
diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/imagehistory/service/ImageHistoryService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/imagehistory/service/ImageHistoryService.java
index f2a34ca5..36c7e644 100644
--- a/backend-java/src/main/java/com/nanri/aiimage/modules/imagehistory/service/ImageHistoryService.java
+++ b/backend-java/src/main/java/com/nanri/aiimage/modules/imagehistory/service/ImageHistoryService.java
@@ -49,24 +49,21 @@ public class ImageHistoryService {
if (safeSize < 10) safeSize = 10;
if (safeSize > 50) safeSize = 50;
- LambdaQueryWrapper query = new LambdaQueryWrapper<>();
- if (userId != null && userId > 0) {
- query.eq(ImageHistoryEntity::getUserId, userId);
- }
LocalDateTime startDt = parseDateTime(timeStart, false);
- if (startDt != null) {
- query.ge(ImageHistoryEntity::getCreatedAt, startDt);
- }
LocalDateTime endDt = parseDateTime(timeEnd, true);
- if (endDt != null) {
- query.le(ImageHistoryEntity::getCreatedAt, endDt);
- }
- Long total = imageHistoryMapper.selectCount(query);
- query.orderByDesc(ImageHistoryEntity::getCreatedAt);
+ Long total = imageHistoryMapper.selectCount(filterOf(userId, startDt, endDt));
+
+ // 先只取本页 id(仅排 id/时间小列,避免对大文本 filesort 撑爆 sort buffer),再按 id 取行。
+ LambdaQueryWrapper idPage = filterOf(userId, startDt, endDt)
+ .select(ImageHistoryEntity::getId)
+ .orderByDesc(ImageHistoryEntity::getCreatedAt);
int offset = (safePage - 1) * safeSize;
- query.last("LIMIT " + safeSize + " OFFSET " + offset);
- List rows = imageHistoryMapper.selectList(query);
+ idPage.last("LIMIT " + safeSize + " OFFSET " + offset);
+ List pageIds = imageHistoryMapper.selectList(idPage).stream()
+ .map(ImageHistoryEntity::getId)
+ .toList();
+ List rows = rowsByIds(pageIds);
Map usernameMap = loadUsernameMap(rows);
@@ -83,6 +80,50 @@ public class ImageHistoryService {
return vo;
}
+ /** 清空全部生成记录(验收反馈:生成记录数据要可清理)。返回清理条数。 */
+ public long clearAllHistory(String operatorName) {
+ Long count = imageHistoryMapper.selectCount(new LambdaQueryWrapper<>());
+ long removed = count == null ? 0L : count;
+ imageHistoryMapper.delete(new LambdaQueryWrapper<>());
+ log.info("[image-history] 管理员 {} 清空生成记录,共清理 {} 条", operatorName == null ? "unknown" : operatorName, removed);
+ return removed;
+ }
+
+ /** 按用户/时间范围构造筛选条件(不排序、不分页)。 */
+ private LambdaQueryWrapper filterOf(Long userId, LocalDateTime startDt, LocalDateTime endDt) {
+ LambdaQueryWrapper filter = new LambdaQueryWrapper<>();
+ if (userId != null && userId > 0) {
+ filter.eq(ImageHistoryEntity::getUserId, userId);
+ }
+ if (startDt != null) {
+ filter.ge(ImageHistoryEntity::getCreatedAt, startDt);
+ }
+ if (endDt != null) {
+ filter.le(ImageHistoryEntity::getCreatedAt, endDt);
+ }
+ return filter;
+ }
+
+ /** 按本页 id 顺序回表取完整行(id 集合小,避免对全量大文本字段排序)。 */
+ private List rowsByIds(List ids) {
+ if (ids == null || ids.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List fetched = imageHistoryMapper.selectBatchIds(ids);
+ Map byId = new HashMap<>(fetched.size() * 2);
+ for (ImageHistoryEntity row : fetched) {
+ byId.put(row.getId(), row);
+ }
+ List ordered = new ArrayList<>(ids.size());
+ for (Long id : ids) {
+ ImageHistoryEntity row = byId.get(id);
+ if (row != null) {
+ ordered.add(row);
+ }
+ }
+ return ordered;
+ }
+
private Map loadUsernameMap(List rows) {
Set userIds = new LinkedHashSet<>();
for (ImageHistoryEntity r : rows) {