feat(地理围栏): 添加地理围栏功能并优化打卡记录处理

- 添加@turf/turf依赖用于地理围栏计算
- 实现地理围栏检查,拒绝围栏外的打卡并记录失败事件
- 过滤掉失败事件在工人历史记录中显示
- 修复地图链接中的错误语法
- 移除开发提示和HTTPS相关代码
- 优化视图按钮样式和事件类型颜色标识
This commit is contained in:
sudomarcma
2025-06-30 16:03:12 +08:00
parent 57f2d5f6c5
commit d322404007
7 changed files with 2305 additions and 10 deletions
+45 -4
View File
@@ -1,5 +1,3 @@
import https from 'https'
import fs from 'fs'
import express from 'express'
import cors from 'cors'
import { Parser } from 'json2csv'
@@ -8,6 +6,11 @@ import mysql from 'mysql2/promise'
import dotenv from 'dotenv'
import bcrypt from 'bcrypt'
import jwt from 'jsonwebtoken'
// --- FIX START ---
// Import only the required functions from turf
import { point, polygon, booleanPointInPolygon, pointToLineDistance } from '@turf/turf'
// --- FIX END ---
// Main function to start the server
async function startServer() {
@@ -38,6 +41,22 @@ async function startServer() {
process.exit(1)
}
// --- FIX START ---
// Define the geofence polygon by calling the 'polygon' function directly
const geofence = polygon([
[
[101.80827335908509, 2.8350045747358337],
[101.80822799653066, 2.8340134829130363],
[101.80827902940462, 2.8335264317641418],
[101.80941309326164, 2.8332772427247335],
[101.81144873788423, 2.834596811345506],
[101.81166988033686, 2.8345911479647157],
[101.81199875885511, 2.83593336858695],
[101.80827335908509, 2.8350045747358337],
],
])
// --- FIX END ---
app.use(cors())
app.use(express.json())
@@ -96,6 +115,28 @@ async function startServer() {
try {
const { userId, eventType, qrCodeValue, latitude, longitude } = req.body
// Geofencing check using the directly imported functions
const userLocation = point([longitude, latitude]);
const isWithinGeofence = booleanPointInPolygon(userLocation, geofence);
if (!isWithinGeofence) {
// User is outside the geofence, log a 'failed' attempt
// Calculate the distance from the geofence
const distance = pointToLineDistance(userLocation, geofence.geometry.coordinates[0], { units: 'meters' });
// Create a descriptive note
const notes = `Clock-in outside of the zone: ${distance.toFixed(2)} meters.`;
// Insert the failed attempt into the database
await db.execute(
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes, distance_meters) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[userId, 'failed', new Date(), qrCodeValue, latitude, longitude, notes, distance]
);
// Return an error to the user
return res.status(403).json({ message: `You are not within the allowed work area.` });
// --- MODIFICATION END ---
}
const [qrRows] = await db.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [
qrCodeValue,
])
@@ -536,7 +577,7 @@ async function startServer() {
const placeholders = idsArray.map(() => '?').join(',')
// MODIFIED: Use LEFT JOIN and COALESCE to handle manual entries, and select `notes`
let query = `SELECT cr.id, w.full_name, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName, cr.latitude, cr.longitude, cr.notes FROM clock_records cr LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id JOIN workers w ON cr.worker_id = w.id WHERE cr.worker_id IN (${placeholders})`
let query = `SELECT cr.id, w.full_name, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName, cr.latitude, cr.longitude, cr.notes, cr.distance_meters FROM clock_records cr LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id JOIN workers w ON cr.worker_id = w.id WHERE cr.worker_id IN (${placeholders})`;
const params = [...idsArray]
if (startDate && endDate) {
@@ -657,7 +698,7 @@ async function startServer() {
// }
app.listen(port, () => {
console.log(`Server is running on https://localhost:${port}`)
console.log(`Server is running on http://localhost:${port}`)
})
}
+2253 -1
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -16,6 +16,7 @@
"@capacitor/cli": "^7.4.0",
"@capacitor/core": "^7.4.0",
"@primeuix/themes": "^1.1.2",
"@turf/turf": "^7.2.0",
"bcrypt": "^6.0.0",
"body-parser": "^2.2.0",
"cors": "^2.8.5",
+1 -1
View File
@@ -230,7 +230,7 @@
</button>
<button
@click="viewRecords(worker.id)"
class="bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-white px-3 py-1 rounded-md text-sm font-medium transition-colors duration-200"
class="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded-md text-sm font-medium transition-colors duration-200"
>
View Records
</button>
+2 -1
View File
@@ -135,6 +135,7 @@
:class="{
'bg-green-500': record.event_type === 'clock_in',
'bg-red-500': record.event_type === 'clock_out',
'bg-yellow-500': record.event_type === 'failed',
}"
>{{ record.event_type.replace('_', ' ') }}</span
>
@@ -146,7 +147,7 @@
<td class="px-4 py-3">
<a
v-if="record.latitude && record.longitude"
:href="`https://maps.google.com/?q=$${record.latitude},${record.longitude}`"
:href="`https://maps.google.com/?q=${record.latitude},${record.longitude}`"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 hover:text-blue-800 underline font-medium"
+2 -2
View File
@@ -31,9 +31,9 @@
required
/>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 text-center mb-6">
<!-- <p class="text-xs text-gray-500 dark:text-gray-400 text-center mb-6">
Hint: worker/password or manager/password
</p>
</p> -->
<button
type="submit"
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-md text-lg transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
+1 -1
View File
@@ -56,7 +56,7 @@ onMounted(async () => {
try {
const data = await apiFetch(`/api/worker/clock-history/${userId}`)
if (data) {
clockHistory.value = data
clockHistory.value = data.filter(event => event.event_type !== 'failed');
}
} catch (error) {
console.error('Failed to fetch clock history:', error)