Skip to content

Commit 2d6769e

Browse files
Merge pull request #53 from easyshellworld/dev
Dev
2 parents 9681f2e + 2328bd6 commit 2d6769e

21 files changed

Lines changed: 1538 additions & 185 deletions

File tree

contracts/hardhat-arrowtower/hardhat.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { HardhatUserConfig } from "hardhat/config"
22
import "@nomicfoundation/hardhat-toolbox"
33
import "@parity/hardhat-polkadot"
4+
import { config as dotenvConfig } from "dotenv"
5+
dotenvConfig()
6+
47

58
const PRIVATE_KEY_LOCAL = process.env.PRIVATE_KEY_LOCAL || ""
69
const PRIVATE_KEY_PA = process.env.PRIVATE_KEY_PA || ""

contracts/hardhat-arrowtower/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"devDependencies": {
1313
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
1414
"@parity/hardhat-polkadot": "^0.1.9",
15-
"solc": "0.8.26"
15+
"solc": "0.8.28"
16+
},
17+
"dependencies": {
18+
"@openzeppelin/contracts-upgradeable": "^5.4.0",
19+
"dotenv": "^17.2.3"
1620
}
1721
}

data/arrowtower.db

0 Bytes
Binary file not shown.

data/arrowtower_test.db

0 Bytes
Binary file not shown.

prisma/data/arrowtower.db

104 KB
Binary file not shown.

prisma/initdb.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
import { PrismaClient } from '@prisma/client'
2+
import { config } from 'dotenv'
3+
import { existsSync } from 'fs'
4+
5+
// 按Next.js优先级加载环境变量
6+
if (existsSync('.env.local')) {
7+
config({ path: '.env.local' })
8+
} else {
9+
config({ path: '.env' })
10+
}
211

312
const prisma = new PrismaClient()
413

514
async function main() {
6-
console.log('🌱 清空原来数据库...')
7-
8-
/* // 清空现有数据
9-
10-
15+
/* console.log('🌱 清空原来数据库...')
1116
17+
// 清空现有数据
1218
await prisma.voucher.deleteMany()
1319
await prisma.checkin.deleteMany()
1420
await prisma.pOI.deleteMany()
1521
await prisma.checkinPhoto.deleteMany()
1622
await prisma.route.deleteMany()
17-
await prisma.user.deleteMany() */
18-
23+
await prisma.user.deleteMany()
24+
*/
1925

2026

2127

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// src/app/api/admin/checkins/[id]/route.ts
2+
import { NextRequest, NextResponse } from 'next/server';
3+
import { PrismaClient } from '@prisma/client';
4+
import { getServerSession } from 'next-auth';
5+
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
6+
7+
const prisma = new PrismaClient();
8+
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;
18+
}
19+
20+
// PUT /api/admin/checkins/[id]
21+
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
22+
const guard = await requireAdmin();
23+
if (guard) return guard;
24+
const { id } = await params;
25+
try {
26+
const body = await request.json();
27+
const allowed = ['pending', 'approved', 'rejected', 'flagged'];
28+
const status = String(body.status);
29+
if (!allowed.includes(status)) {
30+
return NextResponse.json(
31+
{ success: false, message: '状态值无效' },
32+
{ status: 400 }
33+
);
34+
}
35+
36+
const checkin = await prisma.checkin.update({ where: { id }, data: { status } });
37+
38+
return NextResponse.json(
39+
{ success: true, data: { checkin }, timestamp: new Date().toISOString() },
40+
{ status: 200 }
41+
);
42+
} catch (error: any) {
43+
console.error('更新打卡记录失败:', error);
44+
return NextResponse.json(
45+
{ success: false, message: '无效的请求数据', error: error.message },
46+
{ status: 400 }
47+
);
48+
}
49+
}
50+
51+
// DELETE /api/admin/checkins/[id]
52+
export async function DELETE(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
53+
const guard = await requireAdmin();
54+
if (guard) return guard;
55+
const { id } = await params;
56+
try {
57+
const exists = await prisma.checkin.findUnique({ where: { id } });
58+
if (!exists) {
59+
return NextResponse.json(
60+
{ success: false, message: '打卡记录不存在' },
61+
{ status: 404 }
62+
);
63+
}
64+
65+
await prisma.checkinPhoto.deleteMany({ where: { checkinId: id } });
66+
await prisma.checkin.delete({ where: { id } });
67+
68+
return NextResponse.json(
69+
{ success: true, data: { id }, timestamp: new Date().toISOString() },
70+
{ status: 200 }
71+
);
72+
} catch (error: any) {
73+
console.error('删除打卡记录失败:', error);
74+
return NextResponse.json(
75+
{ success: false, message: '服务器内部错误', error: error.message },
76+
{ status: 500 }
77+
);
78+
}
79+
}

src/app/api/admin/checkins/route.ts

Lines changed: 34 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,178 +1,55 @@
1-
// src/app/api/admin/checkins/pending/route.ts
1+
// src/app/api/admin/checkins/route.ts
22
import { NextRequest, NextResponse } from 'next/server';
33
import { PrismaClient } from '@prisma/client';
4+
import { getServerSession } from 'next-auth';
5+
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
46

57
const prisma = new PrismaClient();
68

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;
2418
}
2519

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;
2825
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' },
5428
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 } },
7531
},
7632
});
7733

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+
}));
15744

15845
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() },
16647
{ status: 200 }
16748
);
16849
} catch (error: any) {
169-
console.error('获取待审核打卡失败:', error);
50+
console.error('获取打卡记录失败:', error);
17051
return NextResponse.json(
171-
{
172-
success: false,
173-
message: '服务器内部错误',
174-
error: error.message,
175-
},
52+
{ success: false, message: '服务器内部错误', error: error.message },
17653
{ status: 500 }
17754
);
17855
}

0 commit comments

Comments
 (0)