mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 05:56:29 +00:00
feat: add support for pagination responses
also fix check-in error messages not displaying
This commit is contained in:
+25
-9
@@ -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,
|
||||
|
||||
@@ -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="每页记录数")
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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 || '获取打卡记录失败');
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
|
||||
@@ -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 || '启动打卡任务失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user