feat: add support for pagination responses

also fix check-in error messages not displaying
This commit is contained in:
2026-01-06 21:30:19 +08:00
parent 63b4935fce
commit 2626477b0d
7 changed files with 142 additions and 41 deletions
+25 -9
View File
@@ -7,6 +7,7 @@ from backend.schemas.check_in import (
ManualCheckInRequest, ManualCheckInRequest,
CheckInRecordResponse, CheckInRecordResponse,
CheckInResultResponse, CheckInResultResponse,
PaginatedResponse,
) )
from backend.services.check_in_service import CheckInService from backend.services.check_in_service import CheckInService
from backend.services.task_service import TaskService 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( async def get_task_check_in_records(
task_id: int, task_id: int,
skip: int = Query(0, ge=0, description="跳过记录数"), skip: int = Query(0, ge=0, description="跳过记录数"),
@@ -120,10 +121,15 @@ async def get_task_check_in_records(
) )
try: try:
records = CheckInService.get_task_records( records, total = CheckInService.get_task_records(
task_id, db, skip, limit, status_filter, trigger_type 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: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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( async def get_my_check_in_records(
skip: int = Query(0, ge=0, description="跳过记录数"), skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=500, 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) - **trigger_type**: 过滤触发类型 (scheduler/manual)
""" """
try: try:
records = CheckInService.get_user_records( records, total = CheckInService.get_user_records(
current_user.id, db, skip, limit, status_filter, trigger_type 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: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 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( async def get_all_check_in_records(
skip: int = Query(0, ge=0, description="跳过记录数"), skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=500, description="限制记录数"), limit: int = Query(100, ge=1, le=500, description="限制记录数"),
@@ -179,10 +190,15 @@ async def get_all_check_in_records(
- **status**: 过滤指定状态的记录 - **status**: 过滤指定状态的记录
""" """
try: 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] 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: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+11 -1
View File
@@ -1,7 +1,9 @@
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, List, Generic, TypeVar
from pydantic import BaseModel, Field, ConfigDict from pydantic import BaseModel, Field, ConfigDict
T = TypeVar('T')
class ManualCheckInRequest(BaseModel): class ManualCheckInRequest(BaseModel):
"""手动打卡请求 Schema(已废弃,现在使用路径参数 task_id)""" """手动打卡请求 Schema(已废弃,现在使用路径参数 task_id)"""
@@ -46,3 +48,11 @@ class CheckInResultResponse(BaseModel):
message: str message: str
record_id: Optional[int] = None record_id: Optional[int] = None
error: Optional[str] = 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="每页记录数")
+27 -9
View File
@@ -432,7 +432,7 @@ class CheckInService:
limit: int = 100, limit: int = 100,
status: Optional[str] = None, status: Optional[str] = None,
trigger_type: Optional[str] = None trigger_type: Optional[str] = None
) -> List[CheckInRecord]: ) -> tuple[List[CheckInRecord], int]:
""" """
获取任务的打卡记录 获取任务的打卡记录
@@ -445,7 +445,7 @@ class CheckInService:
trigger_type: 过滤触发类型 (scheduler/manual) trigger_type: 过滤触发类型 (scheduler/manual)
Returns: Returns:
打卡记录列表 (打卡记录列表, 总记录数)
""" """
query = db.query(CheckInRecord).filter(CheckInRecord.task_id == task_id) query = db.query(CheckInRecord).filter(CheckInRecord.task_id == task_id)
@@ -455,10 +455,16 @@ class CheckInService:
if trigger_type: if trigger_type:
query = query.filter(CheckInRecord.trigger_type == 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() CheckInRecord.check_in_time.desc()
).offset(skip).limit(limit).all() ).offset(skip).limit(limit).all()
return records, total
@staticmethod @staticmethod
def get_user_records( def get_user_records(
user_id: int, user_id: int,
@@ -467,7 +473,7 @@ class CheckInService:
limit: int = 100, limit: int = 100,
status: Optional[str] = None, status: Optional[str] = None,
trigger_type: Optional[str] = None trigger_type: Optional[str] = None
) -> List[CheckInRecord]: ) -> tuple[List[CheckInRecord], int]:
""" """
获取用户的所有打卡记录 获取用户的所有打卡记录
@@ -480,7 +486,7 @@ class CheckInService:
trigger_type: 过滤触发类型 (scheduler/manual) trigger_type: 过滤触发类型 (scheduler/manual)
Returns: Returns:
打卡记录列表 (打卡记录列表, 总记录数)
""" """
# 获取用户的所有任务ID # 获取用户的所有任务ID
user_task_ids = db.query(CheckInTask.id).filter(CheckInTask.user_id == user_id).all() user_task_ids = db.query(CheckInTask.id).filter(CheckInTask.user_id == user_id).all()
@@ -495,10 +501,16 @@ class CheckInService:
if trigger_type: if trigger_type:
query = query.filter(CheckInRecord.trigger_type == 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() CheckInRecord.check_in_time.desc()
).offset(skip).limit(limit).all() ).offset(skip).limit(limit).all()
return records, total
@staticmethod @staticmethod
def get_all_records( def get_all_records(
db: Session, db: Session,
@@ -506,7 +518,7 @@ class CheckInService:
limit: int = 100, limit: int = 100,
task_id: Optional[int] = None, task_id: Optional[int] = None,
status: Optional[str] = None status: Optional[str] = None
) -> List[CheckInRecord]: ) -> tuple[List[CheckInRecord], int]:
""" """
获取所有打卡记录(管理员)- 使用联表查询优化性能 获取所有打卡记录(管理员)- 使用联表查询优化性能
@@ -518,7 +530,7 @@ class CheckInService:
status: 过滤状态 status: 过滤状态
Returns: Returns:
打卡记录列表 (打卡记录列表, 总记录数)
""" """
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
@@ -533,10 +545,16 @@ class CheckInService:
if status: if status:
query = query.filter(CheckInRecord.status == status) query = query.filter(CheckInRecord.status == status)
return query.order_by( # 获取总数
total = query.count()
# 获取分页数据
records = query.order_by(
CheckInRecord.check_in_time.desc() CheckInRecord.check_in_time.desc()
).offset(skip).limit(limit).all() ).offset(skip).limit(limit).all()
return records, total
@staticmethod @staticmethod
def enrich_record_with_user_task_info(record: CheckInRecord, db: Session) -> dict: def enrich_record_with_user_task_info(record: CheckInRecord, db: Session) -> dict:
""" """
+4 -2
View File
@@ -52,8 +52,9 @@ export const useCheckInStore = defineStore('checkIn', {
limit: this.pageSize, limit: this.pageSize,
...params, ...params,
}); });
// 后端现在返回 { records, total, skip, limit }
this.myRecords = data.records || data; this.myRecords = data.records || data;
this.total = data.total || this.myRecords.length; this.total = data.total || 0;
return data; return data;
} catch (error) { } catch (error) {
throw new Error(error.message || '获取打卡记录失败'); throw new Error(error.message || '获取打卡记录失败');
@@ -71,8 +72,9 @@ export const useCheckInStore = defineStore('checkIn', {
limit: this.pageSize, limit: this.pageSize,
...params, ...params,
}); });
// 后端现在返回 { records, total, skip, limit }
this.allRecords = data.records || data; this.allRecords = data.records || data;
this.total = data.total || this.allRecords.length; this.total = data.total || 0;
return data; return data;
} catch (error) { } catch (error) {
throw new Error(error.message || '获取打卡记录失败'); throw new Error(error.message || '获取打卡记录失败');
+5 -1
View File
@@ -418,7 +418,11 @@ const handleCheckIn = async () => {
}, },
onFailure: statusData => { onFailure: statusData => {
checkInLoading.value = false; 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); message.error(errorMsg);
checkInStore.fetchMyRecords({ limit: 1 }); checkInStore.fetchMyRecords({ limit: 1 });
}, },
+65 -18
View File
@@ -216,10 +216,12 @@ import {
import Layout from '@/components/Layout.vue'; import Layout from '@/components/Layout.vue';
import { useTaskStore } from '@/stores/task'; import { useTaskStore } from '@/stores/task';
import { formatDateTime } from '@/utils/helpers'; import { formatDateTime } from '@/utils/helpers';
import { usePolling } from '@/composables/usePolling';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const taskStore = useTaskStore(); const taskStore = useTaskStore();
const { startPolling } = usePolling();
const taskId = computed(() => parseInt(route.params.taskId)); const taskId = computed(() => parseInt(route.params.taskId));
const currentTask = ref(null); const currentTask = ref(null);
@@ -297,13 +299,14 @@ const fetchRecords = async () => {
const response = await taskStore.fetchTaskRecords(taskId.value, params); const response = await taskStore.fetchTaskRecords(taskId.value, params);
// API 可能返回数组或对象 // 后端现在返回 { records, total, skip, limit }
if (Array.isArray(response)) { if (response.records) {
records.value = response.records;
total.value = response.total || 0;
} else if (Array.isArray(response)) {
// 兼容旧格式
records.value = response; records.value = response;
total.value = response.length; total.value = response.length;
} else if (response.items) {
records.value = response.items;
total.value = response.total || response.items.length;
} else { } else {
records.value = []; records.value = [];
total.value = 0; total.value = 0;
@@ -319,25 +322,69 @@ const fetchRecords = async () => {
const handleManualCheckIn = async () => { const handleManualCheckIn = async () => {
checkInLoading.value = true; checkInLoading.value = true;
// 显示持久化通知
const hide = message.loading('正在打卡中,请稍候... 您可以继续浏览其他页面', 0);
try { try {
// 调用异步打卡接口,立即返回 record_id
const result = await taskStore.checkInTask(taskId.value); const result = await taskStore.checkInTask(taskId.value);
hide();
if (result.success) { // 获取 record_id
message.success('打卡成功'); const recordId = result.record_id;
// 刷新记录列表 if (!recordId) {
await fetchRecords(); message.error('打卡请求失败:未获取到记录ID');
} else { checkInLoading.value = false;
message.warning(result.message || '打卡失败'); 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) { } catch (error) {
hide(); console.error('启动打卡失败:', error);
message.error(error.message || '打卡失败');
} finally {
checkInLoading.value = false; checkInLoading.value = false;
message.error(error.message || '启动打卡任务失败');
} }
}; };
+5 -1
View File
@@ -721,7 +721,11 @@ const handleCheckIn = async taskId => {
}, },
onFailure: async statusData => { onFailure: async statusData => {
checkInLoading.value[taskId] = false; 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); message.error(errorMsg);
await fetchTasks(); await fetchTasks();
}, },