|
1 | | -// src/app/api/admin/checkins/pending/route.ts |
| 1 | +// src/app/api/admin/checkins/route.ts |
2 | 2 | import { NextRequest, NextResponse } from 'next/server'; |
3 | 3 | import { PrismaClient } from '@prisma/client'; |
| 4 | +import { getServerSession } from 'next-auth'; |
| 5 | +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; |
4 | 6 |
|
5 | 7 | const prisma = new PrismaClient(); |
6 | 8 |
|
7 | | -// 管理员 Token(实际项目中应使用 JWT 或数据库验证) |
8 | | -const ADMIN_TOKEN = process.env.ADMIN_TOKEN || 'your-super-secret-admin-token'; |
9 | | - |
10 | | -// 辅助函数:计算两点间距离(Haversine 公式),单位:米 |
11 | | -function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { |
12 | | - const R = 6371e3; // 地球半径(米) |
13 | | - const φ1 = (lat1 * Math.PI) / 180; |
14 | | - const φ2 = (lat2 * Math.PI) / 180; |
15 | | - const Δφ = ((lat2 - lat1) * Math.PI) / 180; |
16 | | - const Δλ = ((lon2 - lon1) * Math.PI) / 180; |
17 | | - |
18 | | - const a = |
19 | | - Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + |
20 | | - Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); |
21 | | - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); |
22 | | - |
23 | | - return R * c; |
| 9 | +async function requireAdmin() { |
| 10 | + const session = await getServerSession(authOptions); |
| 11 | + if (!session || session.user?.role !== 'admin') { |
| 12 | + return NextResponse.json( |
| 13 | + { success: false, message: '未授权访问' }, |
| 14 | + { status: 401 } |
| 15 | + ); |
| 16 | + } |
| 17 | + return null; |
24 | 18 | } |
25 | 19 |
|
26 | | -// GET /api/admin/checkins/pending |
27 | | -export async function GET(request: NextRequest) { |
| 20 | +// GET /api/admin/checkins |
| 21 | +// 返回所有打卡记录,含用户昵称与打卡点名称 |
| 22 | +export async function GET(_request: NextRequest) { |
| 23 | + const guard = await requireAdmin(); |
| 24 | + if (guard) return guard; |
28 | 25 | try { |
29 | | - // 1. 验证管理员权限 |
30 | | - const authHeader = request.headers.get('authorization'); |
31 | | - if (!authHeader || !authHeader.startsWith('Bearer ')) { |
32 | | - return NextResponse.json( |
33 | | - { success: false, message: '未授权访问' }, |
34 | | - { status: 401 } |
35 | | - ); |
36 | | - } |
37 | | - |
38 | | - const token = authHeader.split(' ')[1]; |
39 | | - if (token !== ADMIN_TOKEN) { |
40 | | - return NextResponse.json( |
41 | | - { success: false, message: '无效的管理员令牌' }, |
42 | | - { status: 403 } |
43 | | - ); |
44 | | - } |
45 | | - |
46 | | - // 2. 查询所有待审核的打卡记录(status = 'pending') |
47 | | - const pendingCheckins = await prisma.checkin.findMany({ |
48 | | - where: { |
49 | | - status: 'pending', |
50 | | - }, |
51 | | - orderBy: { |
52 | | - createdAt: 'desc', |
53 | | - }, |
| 26 | + const checkins = await prisma.checkin.findMany({ |
| 27 | + orderBy: { createdAt: 'desc' }, |
54 | 28 | include: { |
55 | | - user: { |
56 | | - select: { |
57 | | - walletAddress: true, |
58 | | - id: true, |
59 | | - }, |
60 | | - }, |
61 | | - poi: { |
62 | | - select: { |
63 | | - name: true, |
64 | | - latitude: true, |
65 | | - longitude: true, |
66 | | - }, |
67 | | - }, |
68 | | - route: true, |
69 | | - photos: { |
70 | | - select: { |
71 | | - url: true, |
72 | | - }, |
73 | | - take: 1, |
74 | | - }, |
| 29 | + user: { select: { nickname: true } }, |
| 30 | + poi: { select: { name: true } }, |
75 | 31 | }, |
76 | 32 | }); |
77 | 33 |
|
78 | | - // 3. 获取每个用户的过往打卡次数(用于显示经验) |
79 | | - const userIds = pendingCheckins.map((c) => c.user.id); |
80 | | - const userCheckinCounts = await prisma.checkin.groupBy({ |
81 | | - by: ['userId'], |
82 | | - where: { |
83 | | - userId: { in: userIds }, |
84 | | - status: 'approved', |
85 | | - }, |
86 | | - _count: { |
87 | | - id: true, |
88 | | - }, |
89 | | - }); |
90 | | - |
91 | | - const userCountMap = userCheckinCounts.reduce((map, item) => { |
92 | | - map[item.userId] = item._count.id; |
93 | | - return map; |
94 | | - }, {} as Record<string, number>); |
95 | | - |
96 | | - // 4. 格式化响应数据 |
97 | | - const formattedCheckins = pendingCheckins.map((checkin) => { |
98 | | - // 模拟用户提交的位置(实际应来自前端传入的 latitude/longitude) |
99 | | - // 假设你在 Checkin 表中增加了 latitude 和 longitude 字段 |
100 | | - const submittedLat = checkin.latitude || checkin.poi.latitude + (Math.random() - 0.5) * 0.001; |
101 | | - const submittedLon = checkin.longitude || checkin.poi.longitude + (Math.random() - 0.5) * 0.001; |
102 | | - const accuracy = checkin.accuracy || 25.0; // 米 |
103 | | - |
104 | | - const distance = calculateDistance( |
105 | | - checkin.poi.latitude, |
106 | | - checkin.poi.longitude, |
107 | | - submittedLat, |
108 | | - submittedLon |
109 | | - ); |
110 | | - |
111 | | - // 系统标记原因(示例) |
112 | | - const flaggedReasons: string[] = []; |
113 | | - if (distance > 50) { |
114 | | - flaggedReasons.push('location_discrepancy'); |
115 | | - } |
116 | | - if (accuracy > 30) { |
117 | | - flaggedReasons.push('low_gps_accuracy'); |
118 | | - } |
119 | | - |
120 | | - // 解析 taskData |
121 | | - let taskData: any = null; |
122 | | - try { |
123 | | - taskData = checkin.taskData ? JSON.parse(checkin.taskData) : {}; |
124 | | - } catch (e) { |
125 | | - taskData = { raw: checkin.taskData }; |
126 | | - } |
127 | | - |
128 | | - // 如果是照片任务,补充 photoUrl |
129 | | - if (checkin.photos.length > 0) { |
130 | | - taskData.photoUrl = checkin.photos[0].url; |
131 | | - } |
132 | | - |
133 | | - return { |
134 | | - id: checkin.id, |
135 | | - user: { |
136 | | - walletAddress: checkin.user.walletAddress, |
137 | | - previousCheckins: userCountMap[checkin.user.id] || 0, |
138 | | - }, |
139 | | - poi: { |
140 | | - name: checkin.poi.name, |
141 | | - expectedLocation: { |
142 | | - latitude: checkin.poi.latitude, |
143 | | - longitude: checkin.poi.longitude, |
144 | | - }, |
145 | | - }, |
146 | | - submittedLocation: { |
147 | | - latitude: submittedLat, |
148 | | - longitude: submittedLon, |
149 | | - distance: parseFloat(distance.toFixed(2)), // 米 |
150 | | - accuracy, |
151 | | - }, |
152 | | - taskData, |
153 | | - flaggedReasons, |
154 | | - submittedAt: checkin.createdAt.toISOString(), |
155 | | - }; |
156 | | - }); |
| 34 | + const formatted = checkins.map((c) => ({ |
| 35 | + id: c.id, |
| 36 | + userId: c.userId, |
| 37 | + routeId: c.routeId, |
| 38 | + poiId: c.poiId, |
| 39 | + status: c.status, |
| 40 | + createdAt: c.createdAt, |
| 41 | + user: { nickname: (c as any).user?.nickname || '' }, |
| 42 | + poi: { name: (c as any).poi?.name || '' }, |
| 43 | + })); |
157 | 44 |
|
158 | 45 | return NextResponse.json( |
159 | | - { |
160 | | - success: true, |
161 | | - data: { |
162 | | - checkins: formattedCheckins, |
163 | | - }, |
164 | | - timestamp: new Date().toISOString(), |
165 | | - }, |
| 46 | + { success: true, data: { checkins: formatted }, timestamp: new Date().toISOString() }, |
166 | 47 | { status: 200 } |
167 | 48 | ); |
168 | 49 | } catch (error: any) { |
169 | | - console.error('获取待审核打卡失败:', error); |
| 50 | + console.error('获取打卡记录失败:', error); |
170 | 51 | return NextResponse.json( |
171 | | - { |
172 | | - success: false, |
173 | | - message: '服务器内部错误', |
174 | | - error: error.message, |
175 | | - }, |
| 52 | + { success: false, message: '服务器内部错误', error: error.message }, |
176 | 53 | { status: 500 } |
177 | 54 | ); |
178 | 55 | } |
|
0 commit comments