wangzhibo
4 天以前 c19f1f7e21c93d1f13b4f44a8a615f65af577559
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
import { Provide, Inject } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { ReportDailyVehicleEntity } from '../entity/dailyvehicle';
import { PushVehicleGpsEntity } from '../../push/entity/vehiclegps';
import { BaseSysParamService } from '../../base/service/sys/param';
 
 
 
@Provide()
export class TaskServiceVehicle extends BaseService {
    @InjectEntityModel(ReportDailyVehicleEntity)
    reportDailyVehicleEntity: Repository<ReportDailyVehicleEntity>;
 
    @InjectEntityModel(PushVehicleGpsEntity)
    pushVehicleGpsEntity: Repository<PushVehicleGpsEntity>;
 
    @Inject()
    baseSysParamService: BaseSysParamService;
 
    async aggregateDailyData(targetDate: string) {
        const startTime = `${targetDate} 00:00:00`;
        const endTime = `${targetDate} 23:59:59`;
 
        let HOUR_STARTWORK = parseInt(await this.baseSysParamService.dataByKey('vehicle.hour.startwork')); //'6';
        let HOUR_OFFWORK = parseInt(await this.baseSysParamService.dataByKey('vehicle.hour.offwork')); //'22';
        let THRESHOLD_MOVING = parseInt(await this.baseSysParamService.dataByKey('vehicle.speed.threshold.moving')); //'2.0 km/h';
        let THRESHOLD_SPEEDING = parseInt(await this.baseSysParamService.dataByKey('vehicle.speed.threshold.speeding')); //'30 km/h';
 
        HOUR_STARTWORK = Number.isNaN(HOUR_STARTWORK) ? 6 : HOUR_STARTWORK;
        HOUR_OFFWORK = Number.isNaN(HOUR_OFFWORK) ? 22 : HOUR_OFFWORK;
        THRESHOLD_MOVING = Number.isNaN(THRESHOLD_MOVING) ? 2 : THRESHOLD_MOVING;
        THRESHOLD_SPEEDING = Number.isNaN(THRESHOLD_SPEEDING) ? 30 : THRESHOLD_SPEEDING;
 
 
 
        const rawGpsList = await this.pushVehicleGpsEntity.createQueryBuilder('gps')
            .select([
                'gps.vehicleNo AS vehicleNo',             // 拿最新的自增ID作为每日表的 siteId
                'gps.departmentId AS departmentId', // 永远拿当前后台最新的站点名称,防止设备传旧名称
                'gps.gpsTime AS gpsTime',
                'gps.longitude AS longitude',
                'gps.latitude AS latitude',
                'gps.speed AS speed',
                'gps.direction AS direction',
                'gps.elevation AS elevation'
            ])
            .where('gps.gpsTime BETWEEN :start AND :end', { start: startTime, end: endTime })
            .orderBy({
                'gps.vehicleNo': 'ASC',
                'gps.gpsTime': 'ASC',
            })
            .getRawMany();
 
        if (!rawGpsList || rawGpsList.length === 0) return;
 
        // 2. 按车牌号 vehicleNo 分组
        const groupedMap = new Map<string, PushVehicleGpsEntity[]>();
        for (const point of rawGpsList) {
            if (!groupedMap.has(point.vehicleNo)) {
                groupedMap.set(point.vehicleNo, []);
            }
            groupedMap.get(point.vehicleNo)?.push(point);
        }
 
        // 3. 计算截至当前(或全天)经历的总分钟数,用于倒推计算 offlineMinutes
        const startMs = new Date(`${targetDate}T00:00:00`).getTime();
        const endMs = new Date(`${targetDate}T23:59:59`).getTime();
        const nowMs = new Date().getTime();
 
        // 如果 targetDate 是今天,计算到当前时间;如果是过去/未来,上限算到 23:59:59
        const effectiveEndMs = nowMs < endMs ? nowMs : endMs;
        const totalElapsedMinutes = Math.max(1, Math.floor((effectiveEndMs - startMs) / (1000 * 60)));
 
        const summariesToUpsert: Partial<ReportDailyVehicleEntity>[] = [];
 
        // 4. 逐车按严格顺序计算各项指标
        for (const [vehicleNo, points] of groupedMap.entries()) {
            const summary = this.calculateVehicleMetrics(
                targetDate,
                vehicleNo,
                points,
                totalElapsedMinutes,
                HOUR_STARTWORK,
                HOUR_OFFWORK,
                THRESHOLD_MOVING,
                THRESHOLD_SPEEDING
            );
            summariesToUpsert.push(summary);
        }
 
        // 5. 执行 UPSERT 覆盖更新
        await this.upsertDailyReports(summariesToUpsert);
 
 
        return summariesToUpsert.length;
    }
 
 
    /**
       * 核心单车计算引擎(严格按照指标依赖逻辑顺序执行)
       */
    private calculateVehicleMetrics(
        targetDate: string,
        vehicleNo: string,
        points: PushVehicleGpsEntity[],
        totalElapsedMinutes: number,
        HOUR_STARTWORK: number,
        HOUR_OFFWORK: number,
        HHRESHOLD_MOVING: number,
        THRESHOLD_SPEEDING: number
    ): Partial<ReportDailyVehicleEntity> {
        const departmentId = points[0]?.departmentId ?? null;
 
        // 基础统计量
        let totalMeter = 0; // 累计行驶距离(米)
        let secondsMoving = 0; // 纯行驶时长(秒)
        let secondsParked = 0; // 静止停靠时长(秒)
        let secondsAbnormalIdle = 0; // 异常怠速时长(秒)
 
        // 安全驾驶统计量
        let overspeedCount = 0; // 超速次数
        let overspeedDurationSec = 0; // 超速总秒数
        let harshBrakeCount = 0; // 急刹车次数
        let harshAccelCount = 0; // 急加速次数
        let harshTurnCount = 0; // 急转弯次数
        let offHoursMoveCount = 0; // 非工作时段异动次数
 
        let isOverspeeding = false; // 超速状态标记
 
        for (let i = 0; i < points.length; i++) {
            const curr = points[i];
            const speed = Number(curr.speed) || 0;
            const currDate = new Date(curr.gpsTime);
 
            // ====================================================
            // 顺序 1:单点状态判断(只依赖当前点的数据)
            // ====================================================
 
            // A. 判断非工作时段异动 (23:00 - 05:00 且速度 > 2 km/h)
            const hour = currDate.getHours();
            if ((hour >= HOUR_OFFWORK || hour < HOUR_STARTWORK) && speed > HHRESHOLD_MOVING) {
                offHoursMoveCount++;
            }
 
            // B. 判断超速告警 (限速 30 km/h)
            if (speed > THRESHOLD_SPEEDING) {
                overspeedDurationSec += 1; // 按 1 秒/采样点粗略累加
                if (!isOverspeeding) {
                    overspeedCount++; // 刚跨过 20 时记录一次新告警
                    isOverspeeding = true;
                }
            } else {
                isOverspeeding = false;
            }
 
            // ====================================================
            // 顺序 2:相邻点微分与物理量计算(必须依赖前一点 i-1)
            // ====================================================
            if (i > 0) {
                const prev = points[i - 1];
                const prevDate = new Date(prev.gpsTime);
 
                // 计算相邻两点时间差(秒)
                const timeDiffSec = Math.floor((currDate.getTime() - prevDate.getTime()) / 1000);
 
                // 过滤网络断连/关机(时间差 > 120 秒 视为掉线,不参与物理微分计算)
                if (timeDiffSec > 0 && timeDiffSec <= 120) {
 
                    // ================================================
                    // 顺序 3:基于速度划分为【行驶】或【静止停靠】
                    // ================================================
                    if (speed > HHRESHOLD_MOVING) {
                        // 3.1 累加纯行驶时长
                        secondsMoving += timeDiffSec;
 
                        // 3.2 累加球面真实位移距离
                        totalMeter += this.getHaversineDistance(
                            Number(prev.latitude),
                            Number(prev.longitude),
                            Number(curr.latitude),
                            Number(curr.longitude),
                        );
                    } else {
                        // 3.3 累加静止停靠时长
                        secondsParked += timeDiffSec;
 
                        // 3.4 静止停靠超过 15 分钟(900秒)算作异常怠速
                        if (timeDiffSec > 900) {
                            secondsAbnormalIdle += timeDiffSec;
                        }
                    }
 
                    // ================================================
                    // 顺序 4:安全驾驶行为(加速度与转弯角微分计算)
                    // ================================================
 
                    // 4.1 加速度计算: a = (v2 - v1) / t (需要转换为 m/s)
                    const v1 = (Number(prev.speed) || 0) / 3.6;
                    const v2 = speed / 3.6;
                    const accel = (v2 - v1) / timeDiffSec;
 
                    if (accel > 3.0) {
                        harshAccelCount++; // 急加速 a > 3.0 m/s²
                    } else if (accel < -3.5) {
                        harshBrakeCount++; // 急刹车 a < -3.5 m/s²
                    }
 
                    // 4.2 急转弯计算: 2秒内航向角差 > 60° 且车速 > 15 km/h
                    const prevDir = Number(prev.direction) || 0;
                    const currDir = Number(curr.direction) || 0;
                    let dirDiff = Math.abs(currDir - prevDir);
                    if (dirDiff > 180) dirDiff = 360 - dirDiff; // 跨 0/360 度修正
 
                    if (timeDiffSec <= 2 && dirDiff > 60 && speed > 15) {
                        harshTurnCount++;
                    }
                }
            }
        }
 
        // ====================================================
        // 顺序 5:衍生指标倒推计算(必须放在最后)
        // ====================================================
 
        // 5.1 秒转化为分钟
        const minutesMoving = Math.round(secondsMoving / 60);
        const minutesParked = Math.round(secondsParked / 60);
        const minutesAbnormalIdle = Math.round(secondsAbnormalIdle / 60);
        const totalKm = Number((totalMeter / 1000).toFixed(2));
 
        // 5.2 倒推【未启动/离线时长】 = 当天已过总时长 - 行驶时长 - 停靠时长
        const minutesOffline = Math.max(
            0,
            totalElapsedMinutes - minutesMoving - minutesParked,
        );
 
        // 5.3 计算【平均行驶车速】 = 总行驶公里数 / 纯行驶小时数
        const hoursMoving = secondsMoving / 3600;
        const avgSpeed =
            hoursMoving > 0 ? Number((totalKm / hoursMoving).toFixed(1)) : 0.0;
 
        return {
            date: targetDate,
            vehicleNo,
            departmentId,
            totalKm,
            minutesMoving,
            minutesParked,
            minutesOffline,
            minutesAbnormalIdle,
            avgSpeed,
            overspeedCount,
            overspeedDurationSec,
            harshBrakeCount,
            harshAccelCount,
            harshTurnCount,
            offHoursMoveCount,
        };
    }
 
    /**
     * 数据库批量覆盖更新 (ON DUPLICATE KEY UPDATE)
     */
    private async upsertDailyReports(finalRecords: Partial<ReportDailyVehicleEntity>[]) {
        if (finalRecords.length === 0) return;
 
        // 1. 动态抓取所有的键,将主键、联合唯一键彻底从“待更新字段”中排除
        // 这样能确保 ON DUPLICATE KEY UPDATE 后面的赋值语句绝对干净
        const allUpdateFields = Object.keys(finalRecords[0]).filter(
            k => k !== 'date' && k !== 'vehicleNo' && k !== 'id'
        );
 
        // 2. 依然推荐采用 Chunk 分批,这是应对海量数据/多子字段最安全、执行效率最高的做法
        const chunkSize = 50;
        for (let i = 0; i < finalRecords.length; i += chunkSize) {
            const chunk = finalRecords.slice(i, i + chunkSize);
 
            await this.reportDailyVehicleEntity.createQueryBuilder()
                .insert()
                .values(chunk)
                // 💡 显式声明:当 ['date', 'siteId'] 发生冲突时,强行把 allUpdateFields 里的字段全部更新一遍
                .orUpdate(allUpdateFields, ['date', 'vehicleNo'])
                .execute();
        }
 
    }
 
    /**
     * 纯 JS 原生计算两点之间的球面距离 (单位: 米)
     */
    private getHaversineDistance(
        lat1: number,
        lon1: number,
        lat2: number,
        lon2: number,
    ): number {
        if (lat1 === lat2 && lon1 === lon2) return 0;
        const R = 6371000;
        const rad = Math.PI / 180;
        const dLat = (lat2 - lat1) * rad;
        const dLon = (lon2 - lon1) * rad;
        const a =
            Math.sin(dLat / 2) * Math.sin(dLat / 2) +
            Math.cos(lat1 * rad) *
            Math.cos(lat2 * rad) *
            Math.sin(dLon / 2) *
            Math.sin(dLon / 2);
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return R * c;
    }
}