From 2626477b0d8e9a3ea40832ca7ee594cea13a2358 Mon Sep 17 00:00:00 2001 From: Cccc_ Date: Tue, 6 Jan 2026 21:30:19 +0800 Subject: [PATCH] feat: add support for pagination responses also fix check-in error messages not displaying --- backend/api/check_in.py | 34 ++++++++--- backend/schemas/check_in.py | 12 +++- backend/services/check_in_service.py | 36 ++++++++--- frontend/src/stores/checkIn.js | 6 +- frontend/src/views/DashboardView.vue | 6 +- frontend/src/views/TaskRecordsView.vue | 83 ++++++++++++++++++++------ frontend/src/views/TasksView.vue | 6 +- 7 files changed, 142 insertions(+), 41 deletions(-) diff --git a/backend/api/check_in.py b/backend/api/check_in.py index 963d402..fac50f7 100644 --- a/backend/api/check_in.py +++ b/backend/api/check_in.py @@ -7,6 +7,7 @@ from backend.schemas.check_in import ( ManualCheckInRequest, CheckInRecordResponse, CheckInResultResponse, + PaginatedResponse, ) from backend.services.check_in_service import CheckInService from backend.services.task_service import TaskService @@ -91,7 +92,7 @@ async def get_check_in_record_status( } -@router.get("/task/{task_id}/records", response_model=List[CheckInRecordResponse], summary="查看任务的打卡记录") +@router.get("/task/{task_id}/records", response_model=PaginatedResponse[CheckInRecordResponse], summary="查看任务的打卡记录") async def get_task_check_in_records( task_id: int, skip: int = Query(0, ge=0, description="跳过记录数"), @@ -120,10 +121,15 @@ async def get_task_check_in_records( ) try: - records = CheckInService.get_task_records( + records, total = CheckInService.get_task_records( task_id, db, skip, limit, status_filter, trigger_type ) - return records + return PaginatedResponse( + records=records, + total=total, + skip=skip, + limit=limit + ) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -131,7 +137,7 @@ async def get_task_check_in_records( ) -@router.get("/my-records", response_model=List[CheckInRecordResponse], summary="查看当前用户的所有打卡记录") +@router.get("/my-records", response_model=PaginatedResponse[CheckInRecordResponse], summary="查看当前用户的所有打卡记录") async def get_my_check_in_records( skip: int = Query(0, ge=0, description="跳过记录数"), limit: int = Query(100, ge=1, le=500, description="限制记录数"), @@ -149,10 +155,15 @@ async def get_my_check_in_records( - **trigger_type**: 过滤触发类型 (scheduler/manual) """ try: - records = CheckInService.get_user_records( + records, total = CheckInService.get_user_records( current_user.id, db, skip, limit, status_filter, trigger_type ) - return records + return PaginatedResponse( + records=records, + total=total, + skip=skip, + limit=limit + ) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -161,7 +172,7 @@ async def get_my_check_in_records( -@router.get("/records", response_model=List[CheckInRecordResponse], summary="查看所有打卡记录(管理员)") +@router.get("/records", response_model=PaginatedResponse[CheckInRecordResponse], summary="查看所有打卡记录(管理员)") async def get_all_check_in_records( skip: int = Query(0, ge=0, description="跳过记录数"), limit: int = Query(100, ge=1, le=500, description="限制记录数"), @@ -179,10 +190,15 @@ async def get_all_check_in_records( - **status**: 过滤指定状态的记录 """ try: - records = CheckInService.get_all_records(db, skip, limit, task_id, status_filter) + records, total = CheckInService.get_all_records(db, skip, limit, task_id, status_filter) # 为每条记录添加用户和任务信息 enriched_records = [CheckInService.enrich_record_with_user_task_info(record, db) for record in records] - return enriched_records + return PaginatedResponse( + records=enriched_records, + total=total, + skip=skip, + limit=limit + ) except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/backend/schemas/check_in.py b/backend/schemas/check_in.py index 162a92c..29aa3cb 100644 --- a/backend/schemas/check_in.py +++ b/backend/schemas/check_in.py @@ -1,7 +1,9 @@ from datetime import datetime -from typing import Optional +from typing import Optional, List, Generic, TypeVar from pydantic import BaseModel, Field, ConfigDict +T = TypeVar('T') + class ManualCheckInRequest(BaseModel): """手动打卡请求 Schema(已废弃,现在使用路径参数 task_id)""" @@ -46,3 +48,11 @@ class CheckInResultResponse(BaseModel): message: str record_id: Optional[int] = None error: Optional[str] = None + + +class PaginatedResponse(BaseModel, Generic[T]): + """分页响应 Schema""" + records: List[T] = Field(..., description="记录列表") + total: int = Field(..., description="总记录数") + skip: int = Field(..., description="跳过的记录数") + limit: int = Field(..., description="每页记录数") diff --git a/backend/services/check_in_service.py b/backend/services/check_in_service.py index 7eddbfa..7af0e75 100644 --- a/backend/services/check_in_service.py +++ b/backend/services/check_in_service.py @@ -432,7 +432,7 @@ class CheckInService: limit: int = 100, status: Optional[str] = None, trigger_type: Optional[str] = None - ) -> List[CheckInRecord]: + ) -> tuple[List[CheckInRecord], int]: """ 获取任务的打卡记录 @@ -445,7 +445,7 @@ class CheckInService: trigger_type: 过滤触发类型 (scheduler/manual) Returns: - 打卡记录列表 + (打卡记录列表, 总记录数) """ query = db.query(CheckInRecord).filter(CheckInRecord.task_id == task_id) @@ -455,10 +455,16 @@ class CheckInService: if trigger_type: query = query.filter(CheckInRecord.trigger_type == trigger_type) - return query.order_by( + # 获取总数 + total = query.count() + + # 获取分页数据 + records = query.order_by( CheckInRecord.check_in_time.desc() ).offset(skip).limit(limit).all() + return records, total + @staticmethod def get_user_records( user_id: int, @@ -467,7 +473,7 @@ class CheckInService: limit: int = 100, status: Optional[str] = None, trigger_type: Optional[str] = None - ) -> List[CheckInRecord]: + ) -> tuple[List[CheckInRecord], int]: """ 获取用户的所有打卡记录 @@ -480,7 +486,7 @@ class CheckInService: trigger_type: 过滤触发类型 (scheduler/manual) Returns: - 打卡记录列表 + (打卡记录列表, 总记录数) """ # 获取用户的所有任务ID user_task_ids = db.query(CheckInTask.id).filter(CheckInTask.user_id == user_id).all() @@ -495,10 +501,16 @@ class CheckInService: if trigger_type: query = query.filter(CheckInRecord.trigger_type == trigger_type) - return query.order_by( + # 获取总数 + total = query.count() + + # 获取分页数据 + records = query.order_by( CheckInRecord.check_in_time.desc() ).offset(skip).limit(limit).all() + return records, total + @staticmethod def get_all_records( db: Session, @@ -506,7 +518,7 @@ class CheckInService: limit: int = 100, task_id: Optional[int] = None, status: Optional[str] = None - ) -> List[CheckInRecord]: + ) -> tuple[List[CheckInRecord], int]: """ 获取所有打卡记录(管理员)- 使用联表查询优化性能 @@ -518,7 +530,7 @@ class CheckInService: status: 过滤状态 Returns: - 打卡记录列表 + (打卡记录列表, 总记录数) """ from sqlalchemy.orm import joinedload @@ -533,10 +545,16 @@ class CheckInService: if status: query = query.filter(CheckInRecord.status == status) - return query.order_by( + # 获取总数 + total = query.count() + + # 获取分页数据 + records = query.order_by( CheckInRecord.check_in_time.desc() ).offset(skip).limit(limit).all() + return records, total + @staticmethod def enrich_record_with_user_task_info(record: CheckInRecord, db: Session) -> dict: """ diff --git a/frontend/src/stores/checkIn.js b/frontend/src/stores/checkIn.js index 8172048..b573220 100644 --- a/frontend/src/stores/checkIn.js +++ b/frontend/src/stores/checkIn.js @@ -52,8 +52,9 @@ export const useCheckInStore = defineStore('checkIn', { limit: this.pageSize, ...params, }); + // 后端现在返回 { records, total, skip, limit } this.myRecords = data.records || data; - this.total = data.total || this.myRecords.length; + this.total = data.total || 0; return data; } catch (error) { throw new Error(error.message || '获取打卡记录失败'); @@ -71,8 +72,9 @@ export const useCheckInStore = defineStore('checkIn', { limit: this.pageSize, ...params, }); + // 后端现在返回 { records, total, skip, limit } this.allRecords = data.records || data; - this.total = data.total || this.allRecords.length; + this.total = data.total || 0; return data; } catch (error) { throw new Error(error.message || '获取打卡记录失败'); diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index a8fbc99..0baf8e3 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -418,7 +418,11 @@ const handleCheckIn = async () => { }, onFailure: statusData => { checkInLoading.value = false; - const errorMsg = statusData.error_message || statusData.response_text || '打卡失败'; + // 优先使用 error_message,如果为空则使用 response_text,都为空则使用默认消息 + const errorMsg = + (statusData.error_message && statusData.error_message.trim()) || + (statusData.response_text && statusData.response_text.trim()) || + '打卡失败'; message.error(errorMsg); checkInStore.fetchMyRecords({ limit: 1 }); }, diff --git a/frontend/src/views/TaskRecordsView.vue b/frontend/src/views/TaskRecordsView.vue index 65fb437..3648aec 100644 --- a/frontend/src/views/TaskRecordsView.vue +++ b/frontend/src/views/TaskRecordsView.vue @@ -216,10 +216,12 @@ import { import Layout from '@/components/Layout.vue'; import { useTaskStore } from '@/stores/task'; import { formatDateTime } from '@/utils/helpers'; +import { usePolling } from '@/composables/usePolling'; const route = useRoute(); const router = useRouter(); const taskStore = useTaskStore(); +const { startPolling } = usePolling(); const taskId = computed(() => parseInt(route.params.taskId)); const currentTask = ref(null); @@ -297,13 +299,14 @@ const fetchRecords = async () => { const response = await taskStore.fetchTaskRecords(taskId.value, params); - // API 可能返回数组或对象 - if (Array.isArray(response)) { + // 后端现在返回 { records, total, skip, limit } + if (response.records) { + records.value = response.records; + total.value = response.total || 0; + } else if (Array.isArray(response)) { + // 兼容旧格式 records.value = response; total.value = response.length; - } else if (response.items) { - records.value = response.items; - total.value = response.total || response.items.length; } else { records.value = []; total.value = 0; @@ -319,25 +322,69 @@ const fetchRecords = async () => { const handleManualCheckIn = async () => { checkInLoading.value = true; - // 显示持久化通知 - const hide = message.loading('正在打卡中,请稍候... 您可以继续浏览其他页面', 0); - try { + // 调用异步打卡接口,立即返回 record_id const result = await taskStore.checkInTask(taskId.value); - hide(); - if (result.success) { - message.success('打卡成功'); - // 刷新记录列表 - await fetchRecords(); - } else { - message.warning(result.message || '打卡失败'); + // 获取 record_id + const recordId = result.record_id; + if (!recordId) { + message.error('打卡请求失败:未获取到记录ID'); + checkInLoading.value = false; + return; } + + // 如果初始状态就是失败,显示错误并刷新记录列表 + if (result.status === 'failure') { + const errorMsg = + (result.error_message && result.error_message.trim()) || + (result.response_text && result.response_text.trim()) || + '打卡失败'; + message.error(errorMsg); + checkInLoading.value = false; + await fetchRecords(); + return; + } + + // 显示提示消息 + message.info('打卡任务已启动,正在后台处理...'); + + // 使用轮询 composable 检查打卡状态 + startPolling( + async () => { + const status = await taskStore.getCheckInRecordStatus(recordId); + return { + completed: status.status !== 'pending', + success: status.status === 'success', + data: status, + }; + }, + { + onSuccess: async () => { + checkInLoading.value = false; + message.success('打卡成功!'); + await fetchRecords(); + }, + onFailure: async statusData => { + checkInLoading.value = false; + // 优先使用 error_message,如果为空则使用 response_text,都为空则使用默认消息 + const errorMsg = + (statusData.error_message && statusData.error_message.trim()) || + (statusData.response_text && statusData.response_text.trim()) || + '打卡失败'; + message.error(errorMsg); + await fetchRecords(); + }, + onTimeout: () => { + checkInLoading.value = false; + message.warning('打卡处理时间较长,请稍后查看打卡记录'); + }, + } + ); } catch (error) { - hide(); - message.error(error.message || '打卡失败'); - } finally { + console.error('启动打卡失败:', error); checkInLoading.value = false; + message.error(error.message || '启动打卡任务失败'); } }; diff --git a/frontend/src/views/TasksView.vue b/frontend/src/views/TasksView.vue index 1a2b406..4897165 100644 --- a/frontend/src/views/TasksView.vue +++ b/frontend/src/views/TasksView.vue @@ -721,7 +721,11 @@ const handleCheckIn = async taskId => { }, onFailure: async statusData => { checkInLoading.value[taskId] = false; - const errorMsg = statusData.error_message || statusData.response_text || '打卡失败'; + // 优先使用 error_message,如果为空则使用 response_text,都为空则使用默认消息 + const errorMsg = + (statusData.error_message && statusData.error_message.trim()) || + (statusData.response_text && statusData.response_text.trim()) || + '打卡失败'; message.error(errorMsg); await fetchTasks(); },