From 920cec41cedfde27f89d7c19b78e03147b926ab5 Mon Sep 17 00:00:00 2001
From: wangzhibo <wangzhibo@shsening.com>
Date: 星期二, 28 七月 2026 10:58:45 +0800
Subject: [PATCH] add pdfjs and traderecord

---
 src/modules/report/service/taskservice.ts             |  121 +------
 src/modules/operation/entity/traderecord.ts           |   55 +++
 src/modules/push/controller/app/open.ts               |    4 
 src/modules/operation/service/traderecord.ts          |   29 +
 src/modules/report/entity/dailyvehicle.ts             |   78 ++++
 src/modules/report/service/dailyvehicle.ts            |   14 
 src/modules/operation/controller/admin/traderecord.ts |   55 +++
 src/modules/report/service/taskservicevehicle.ts      |  313 +++++++++++++++++++
 src/modules/report/service/ExecutorSiteDeptDaily.ts   |  105 ++++++
 src/modules/report/service/ExecutorVehicleDaily.ts    |   84 +++++
 src/modules/report/service/taskservicedepartment.ts   |    2 
 src/modules/report/controller/admin/dailyvehicle.ts   |   34 ++
 src/modules/plugin/hooks/upload/index.ts              |    2 
 src/modules/push/controller/admin/cameragps.ts        |    2 
 src/modules/report/service/taskservicesite.ts         |    2 
 src/modules/basicdata/entity/iot.ts                   |    2 
 16 files changed, 799 insertions(+), 103 deletions(-)

diff --git a/src/modules/basicdata/entity/iot.ts b/src/modules/basicdata/entity/iot.ts
index ecba81d..75c4f75 100644
--- a/src/modules/basicdata/entity/iot.ts
+++ b/src/modules/basicdata/entity/iot.ts
@@ -6,7 +6,7 @@
  */
 @Entity('t_basicdata_iot')
 export class BasicdataIotEntity extends BaseEntity {
-  @Index({ unique: true })
+  @Index({ unique: false })
   @Column({ comment: '缁堢缂栫爜' })
   iotCode: string;
 
diff --git a/src/modules/operation/controller/admin/traderecord.ts b/src/modules/operation/controller/admin/traderecord.ts
new file mode 100644
index 0000000..50311e9
--- /dev/null
+++ b/src/modules/operation/controller/admin/traderecord.ts
@@ -0,0 +1,55 @@
+import { CoolController, BaseController } from '@cool-midway/core';
+import { Body, Inject, Post } from '@midwayjs/core';
+import { OperationTradeRecordEntity } from '../../entity/traderecord';
+import { OperationTradeRecordService } from '../../service/traderecord';
+import { BaseSysDepartmentEntity } from '../../../base/entity/sys/department';
+import { WithDeptFilterWhere } from '../../../base/middleware/with-dept-filter-where'
+
+/**
+ * 浜ゆ槗璁板綍
+ */
+@CoolController({
+  api: ['add', 'delete', 'update', 'info', 'list', 'page'],
+  entity: OperationTradeRecordEntity,
+  service: OperationTradeRecordService,
+  pageQueryOp: {
+    keyWordLikeFields: ['a.goodsName', 'a.opponent'],
+    fieldEq: [
+      'a.transactionType',
+      'a.incomeType',
+      'a.payMethod'
+    ],
+    select: ['a.*', 'b.name AS departmentName'],
+    join: [
+      {
+        entity: BaseSysDepartmentEntity,
+        alias: 'b',
+        condition: 'a.departmentId = b.id',
+        type: 'leftJoin'
+      }
+    ],
+    where: async (ctx) => {
+      const { startTime, endTime } = ctx.request.body;
+      const conditions: any[][] = [];
+
+      if (startTime && endTime) {
+        conditions.push(['a.transactionTime BETWEEN :startTime AND :endTime', { startTime, endTime }]);
+      }
+      
+      conditions.push(...(await WithDeptFilterWhere(ctx, { alias: 'a', field: 'departmentId' })));
+      return conditions;
+    },
+    addOrderBy: {
+      transactionTime: 'desc'
+    }
+  }
+})
+export class AdminOperationTradeRecordController extends BaseController {
+  @Inject()
+  tradeRecordService: OperationTradeRecordService;
+
+  @Post('/import', { summary: '瀵煎叆璐﹀崟' })
+  async importData(@Body('list') list: any[]) {
+    return this.ok(await this.tradeRecordService.importData(list));
+  }
+}
\ No newline at end of file
diff --git a/src/modules/operation/entity/traderecord.ts b/src/modules/operation/entity/traderecord.ts
new file mode 100644
index 0000000..b3d1f22
--- /dev/null
+++ b/src/modules/operation/entity/traderecord.ts
@@ -0,0 +1,55 @@
+import { BaseEntity } from '../../base/entity/base';
+import { Column, Entity, Index } from 'typeorm';
+
+/**
+ * 浜ゆ槗璁板綍
+ */
+@Entity('t_operation_trade_record')
+export class OperationTradeRecordEntity extends BaseEntity {
+  @Index()
+  @Column({ comment: '浜ゆ槗鏃堕棿', nullable: true })
+  transactionTime: Date;
+
+  @Column({ comment: '浜ゆ槗绫诲瀷', nullable: true })
+  transactionType: string;
+
+  @Index()
+  @Column({ comment: '浜ゆ槗瀵规柟', nullable: true })
+  opponent: string;
+
+  @Index()
+  @Column({ comment: '鍟嗗搧鍚嶇О', nullable: true })
+  goodsName: string;
+
+  @Column({ comment: '鏀舵敮绫诲瀷', dict: ['鏀跺叆', '鏀嚭', '鍏朵粬'], default: '鏀跺叆' })
+  incomeType: string;
+
+  @Column({ comment: '閲戦', type: 'decimal', precision: 12, scale: 2, default: 0 })
+  amount: number;
+
+  @Column({ comment: '鏀粯鏂瑰紡', nullable: true })
+  payMethod: string;
+
+  @Column({ comment: '褰撳墠鐘舵��', nullable: true })
+  status: string;
+
+  @Index()
+  @Column({ comment: '浜ゆ槗鍗曞彿', length: 100, nullable: true })
+  transactionNo: string;
+
+  @Index()
+  @Column({ comment: '鍟嗘埛鍗曞彿', length: 100, nullable: true })
+  merchantNo: string;
+
+  @Column({ comment: '澶囨敞', type: 'text', nullable: true })
+  remark: string;
+
+  @Column({ comment: '鎵�灞為儴闂�' })
+  departmentId: number;
+
+  departmentName: string;
+
+  @Column({ comment: '鍘熷鏂囦欢璺緞', length: 500 })
+  filepath: string;
+  
+}
\ No newline at end of file
diff --git a/src/modules/operation/service/traderecord.ts b/src/modules/operation/service/traderecord.ts
new file mode 100644
index 0000000..5b585e8
--- /dev/null
+++ b/src/modules/operation/service/traderecord.ts
@@ -0,0 +1,29 @@
+import { Provide } from '@midwayjs/core';
+import { BaseService, CoolCommException } from '@cool-midway/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { OperationTradeRecordEntity } from '../entity/traderecord';
+
+/**
+ * 浜ゆ槗璁板綍鏈嶅姟
+ */
+@Provide()
+export class OperationTradeRecordService extends BaseService {
+  @InjectEntityModel(OperationTradeRecordEntity)
+  tradeRecordEntity: Repository<OperationTradeRecordEntity>;
+
+
+  /**
+   * 瀵煎叆Excel鏁版嵁
+   * @param fileInfo 鏂囦欢淇℃伅
+   * @param dataList 瑙f瀽鍚庣殑鏁扮粍
+   */
+  async importData(list: any[]) {
+    if (!list || list.length === 0) {
+      throw new Error('瀵煎叆鏁版嵁涓嶈兘涓虹┖');
+    }
+    // 浣跨敤 TypeORM 鐨� save 鏂规硶鎵归噺鎻掑叆
+    await this.tradeRecordEntity.save(list);
+
+  }
+}
\ No newline at end of file
diff --git a/src/modules/plugin/hooks/upload/index.ts b/src/modules/plugin/hooks/upload/index.ts
index 72a9c4c..b5c1d6b 100644
--- a/src/modules/plugin/hooks/upload/index.ts
+++ b/src/modules/plugin/hooks/upload/index.ts
@@ -156,7 +156,7 @@
   async upload(ctx: any) {
     const { domain } = this.pluginInfo.config;
     try {
-      const { key } = ctx.fields;
+      const key = ctx.fields?.key;
       const basePath = pUploadPath();
       const dateDir = moment().format('YYYYMMDD');
 
diff --git a/src/modules/push/controller/admin/cameragps.ts b/src/modules/push/controller/admin/cameragps.ts
index f05ed50..66cf769 100644
--- a/src/modules/push/controller/admin/cameragps.ts
+++ b/src/modules/push/controller/admin/cameragps.ts
@@ -15,7 +15,7 @@
     fieldEq: ['a.status'],
     where: async (ctx) => {
       const { createDate, gpsTime } = ctx.request.body;
-      const condition = [];
+      const condition:any[][] = [];
       if (createDate && createDate.length === 2) {
         condition.push(['a.createDate BETWEEN :createStartDate AND :createEndDate', { createStartDate: createDate[0], createEndDate: createDate[1] }]);
       }
diff --git a/src/modules/push/controller/app/open.ts b/src/modules/push/controller/app/open.ts
index 9a89a67..3d70d84 100644
--- a/src/modules/push/controller/app/open.ts
+++ b/src/modules/push/controller/app/open.ts
@@ -27,8 +27,8 @@
     @Body() data: WeightUploadDto,
   ) {
 
-    const CLIENT_ID = String(await this.baseSysParamService.dataByKey('YAOHUA.WEIGHT.CLIENTID')); //'10001';
-    const SECRET = String(await this.baseSysParamService.dataByKey('YAOHUA.WEIGHT.SECRET')); //'9001ac9676b8';
+    const CLIENT_ID = String(await this.baseSysParamService.dataByKey('yaohua.weight.clientid')); //'10001';
+    const SECRET = String(await this.baseSysParamService.dataByKey('yaohua.weight.secret')); //'9001ac9676b8';
 
 
     const clientId = headers['clientid'];
diff --git a/src/modules/report/controller/admin/dailyvehicle.ts b/src/modules/report/controller/admin/dailyvehicle.ts
new file mode 100644
index 0000000..95c5e7c
--- /dev/null
+++ b/src/modules/report/controller/admin/dailyvehicle.ts
@@ -0,0 +1,34 @@
+import { CoolController, BaseController } from '@cool-midway/core';
+import { ReportDailyVehicleEntity } from '../../entity/dailyvehicle';
+import { ReportDailyVehicleService } from '../../service/dailyvehicle';
+import { BaseSysDepartmentEntity } from '../../../base/entity/sys/department'
+import { WithDeptFilterWhere } from '../../../base/middleware/with-dept-filter-where'
+
+/**
+ * 姣忔棩绔欑偣姹囨�绘帶鍒跺櫒
+ */
+@CoolController({
+  api: ['add', 'delete', 'update', 'info', 'list', 'page'],
+  entity: ReportDailyVehicleEntity,
+  service: ReportDailyVehicleService,
+  pageQueryOp: {
+    keyWordLikeFields: ['a.vehicleNo'],
+    fieldEq: ['a.date', 'a.vehicleNo'],
+    select: ['a.*', 'b.name AS departmentName'],
+        join: [
+          {
+            entity: BaseSysDepartmentEntity,
+            alias: 'b',
+            condition: 'a.departmentId = b.id',
+            type: 'leftJoin',
+          },
+        ],
+
+    where: async (ctx) => {
+      const conditions: any[][] = [];
+      conditions.push(...(await WithDeptFilterWhere(ctx, { alias: 'a', field: 'departmentId' })));
+      return conditions;
+    }
+  }
+})
+export class AdminReportDailyVehicleController extends BaseController { }
\ No newline at end of file
diff --git a/src/modules/report/entity/dailyvehicle.ts b/src/modules/report/entity/dailyvehicle.ts
new file mode 100644
index 0000000..dd80675
--- /dev/null
+++ b/src/modules/report/entity/dailyvehicle.ts
@@ -0,0 +1,78 @@
+import { BaseEntity } from '../../base/entity/base';
+import { Column, Entity, Index, Unique } from 'typeorm';
+
+/**
+ * 姣忔棩绔欑偣姹囨��
+ */
+@Entity('t_report_daily_vehicle')
+@Unique('uk_date_vehicle', ['date', 'vehicleNo'])
+export class ReportDailyVehicleEntity extends BaseEntity {
+
+  @Index()
+  @Column({ comment: '鏃ユ湡', type: 'date' })
+  date: string;
+
+  @Index()
+  @Column({ comment: '杞︾墝鍙�', length: 32 })
+  vehicleNo: string;
+
+  @Index()
+  @Column({ comment: '鎵�灞為儴闂�/瀛︽牎/鍚庡嫟缁処D', type: 'int', nullable: true })
+  departmentId: number;
+
+  departmentName: string;
+
+  // ==================== 2. 鍩虹杩愯鎸囨爣 ====================
+
+  @Column({
+    comment: '褰撴棩琛岄┒閲岀▼(km)',
+    type: 'decimal',
+    precision: 10,
+    scale: 2,
+    default: 0.0,
+  })
+  totalKm: number;
+
+  @Column({ comment: '鏈惎鍔ㄦ椂闂�(鍒嗛挓)', type: 'int', default: 0 })
+  minutesOffline: number;
+
+  @Column({ comment: '绾椹舵椂闀�(鍒嗛挓)', type: 'int', default: 0 })
+  minutesMoving: number;
+
+  @Column({ comment: '鍋滆溅鏃堕暱(鍒嗛挓)', type: 'int', default: 0 })
+  minutesParked: number;
+
+  @Column({ comment: '寮傚父鎬犻�熸椂闀�(鍒嗛挓)', type: 'int', default: 0 })
+  minutesAbnormalIdle: number;
+
+  @Column({
+    comment: '骞冲潎琛岄┒杞﹂��(km/h)',
+    type: 'decimal',
+    precision: 5,
+    scale: 1,
+    default: 0.0,
+  })
+  avgSpeed: number;
+
+  // ==================== 3. 瀹夊叏涓庨┚椹惰涓烘寚鏍� ====================
+
+  @Column({ comment: '瓒呴�熷憡璀︽鏁�', type: 'int', default: 0 })
+  overspeedCount: number;
+
+  @Column({ comment: '瓒呴�熺疮璁℃椂闀�(绉�)', type: 'int', default: 0 })
+  overspeedDurationSec: number;
+
+  @Column({ comment: '鎬ュ埞杞︽鏁�', type: 'int', default: 0 })
+  harshBrakeCount: number;
+
+  @Column({ comment: '鎬ュ姞閫熸鏁�', type: 'int', default: 0 })
+  harshAccelCount: number;
+
+  @Column({ comment: '鎬ヨ浆寮鏁�', type: 'int', default: 0 })
+  harshTurnCount: number;
+
+  @Column({ comment: '闈炲伐浣滄椂娈靛紓鍔ㄥ憡璀︽鏁�', type: 'int', default: 0 })
+  offHoursMoveCount: number;
+
+
+}
\ No newline at end of file
diff --git a/src/modules/report/service/ExecutorSiteDeptDaily.ts b/src/modules/report/service/ExecutorSiteDeptDaily.ts
new file mode 100644
index 0000000..83bf27e
--- /dev/null
+++ b/src/modules/report/service/ExecutorSiteDeptDaily.ts
@@ -0,0 +1,105 @@
+import { Provide, Inject } from '@midwayjs/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository, In } from 'typeorm';
+import { ReportJoblogEntity } from '../entity/joblog';
+import { TaskServiceSite } from './taskservicesite'
+import { TaskServiceDepartment } from './taskservicedepartment'
+
+
+
+@Provide()
+export class ExecutorSiteDeptDaily {
+
+    @InjectEntityModel(ReportJoblogEntity)
+    reportJoblogEntity: Repository<ReportJoblogEntity>;
+
+    @Inject()
+    taskServiceSite: TaskServiceSite;
+
+    @Inject()
+    taskServiceDepartment: TaskServiceDepartment;
+
+    async execute() {
+        // 鎹炲嚭鎵�鏈夊緟澶勭悊浠诲姟
+        const pendingSiteJobs = await this.reportJoblogEntity.find({
+            where: {
+                status: In(['INIT', 'FAILED']),
+                jobName: In(['SITE_DAILY', 'DEPT_DAILY']),
+            },
+            order: {
+                dataDate: 'ASC', // 鍏堢畻鍘嗗彶锛屽悗绠椾粖澶�
+                // 纭繚鍦ㄥ悓涓�澶╁唴锛孲ITE_DAILY 鍏堣窇锛孌EPT_DAILY 鍚庤窇
+                // 鍦� JS 鏁扮粍涓垜浠彲浠ヨ繘涓�姝ユ帶鍒舵帓搴忥紝鎴栬�呯敱涓氬姟閫昏緫涓ユ牸鎺у埗渚濊禆
+            },
+        });
+
+        for (const job of pendingSiteJobs) {
+            // 涓ユ牸渚濊禆妫�鏌ワ細濡傛灉鏄儴闂ㄦ棩琛ㄤ换鍔★紝蹇呴』纭繚鍚屽ぉ鐨勭珯鐐规棩琛ㄤ换鍔″凡缁忔槸 SUCCESS
+            if (job.jobName === 'DEPT_DAILY') {
+                const siteJob = await this.reportJoblogEntity.findOneBy({
+                    dataDate: job.dataDate,
+                    jobName: 'SITE_DAILY',
+                });
+                if (!siteJob || siteJob.status !== 'SUCCESS') {
+                    // 婧愬ご鏄庣粏杩樻病绠楀ソ/鎴栬繕鍦ㄨ繍琛岋紝閮ㄩ棬琛ㄤ换鍔″繀椤荤瓑寰咃紝璺宠繃鏈鎵ц
+                    continue;
+                }
+            }
+
+            // 閿佸畾浠诲姟鐘舵�佷负 RUNNING锛堣交閲忕骇涔愯閿侀攣鏈哄埗锛�
+            const lockResult = await this.reportJoblogEntity.update(
+                { id: job.id, status: job.status }, // 甯︽湁鍘熷鐘舵�佹鏌ワ紝闃叉琚埆鐨勭嚎绋嬫姠璺�
+                { status: 'RUNNING', startTime: new Date() }
+            );
+
+            if (lockResult.affected === 0) {
+                continue; // 娌℃姠鍒伴攣锛岃鏄庡埆鐨勫畾鏃跺櫒鍦ㄨ窇瀹冿紝璺宠繃
+            }
+
+            // 鐪熸寮�濮嬭窇鏁版嵁
+            try {
+                let recordsProcessed = 0;
+
+                if (job.jobName === 'SITE_DAILY') {
+                    // 璋冪敤浣犲啓鐨� 绔欑偣鏃ヨ〃 鎻愬彇閫昏緫
+                    recordsProcessed = await this.taskServiceSite.aggregateDailyData(job.dataDate) || 0;
+                } else if (job.jobName === 'DEPT_DAILY') {
+                    // 璋冪敤浣犲啓鐨� 閮ㄩ棬鏍戣桨鐐� 鎻愬彇閫昏緫
+                    recordsProcessed = await this.taskServiceDepartment.aggregateDailyData(job.dataDate) || 0;
+                }
+
+                // 鎴愬姛锛氭洿鏂扮姸鎬�
+                await this.reportJoblogEntity.update(job.id, {
+                    status: 'SUCCESS',
+                    recordsProcessed: recordsProcessed,
+                    endTime: new Date(),
+                    errorMessage: '',
+                });
+
+            } catch (error) {
+                // 澶辫触锛氳褰曞爢鏍堬紝绛夊緟涓嬩竴灏忔椂閲嶈瘯
+
+                var errorMessage = '';
+                if (error instanceof Error) {
+                    errorMessage = error.stack ?? error.message;
+                }
+                if (typeof error === 'string') {
+                    errorMessage = error;
+                }
+                try {
+                    errorMessage = JSON.stringify(error);
+                } catch {
+                    errorMessage = String(error);
+                }
+
+                await this.reportJoblogEntity.update(job.id, {
+                    status: 'FAILED',
+                    endTime: new Date(),
+                    errorMessage: errorMessage,
+                    failedCount: () => 'failedCount + 1',
+                });
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/modules/report/service/ExecutorVehicleDaily.ts b/src/modules/report/service/ExecutorVehicleDaily.ts
new file mode 100644
index 0000000..1bb5e93
--- /dev/null
+++ b/src/modules/report/service/ExecutorVehicleDaily.ts
@@ -0,0 +1,84 @@
+import { Provide, Inject } from '@midwayjs/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository, In } from 'typeorm';
+import { ReportJoblogEntity } from '../entity/joblog';
+import { TaskServiceVehicle } from './taskservicevehicle'
+
+
+
+@Provide()
+export class ExecutorVehicleDaily {
+
+    @InjectEntityModel(ReportJoblogEntity)
+    reportJoblogEntity: Repository<ReportJoblogEntity>;
+
+    @Inject()
+    taskServiceVehicle: TaskServiceVehicle;
+
+    async execute() {
+        // 鎹炲嚭鎵�鏈夊緟澶勭悊浠诲姟
+        const pendingSiteJobs = await this.reportJoblogEntity.find({
+            where: {
+                status: In(['INIT', 'FAILED']),
+                jobName: In(['VEHICLE_DAILY']),
+            },
+            order: {
+                dataDate: 'ASC',
+            },
+        });
+
+        for (const job of pendingSiteJobs) {
+
+            // 閿佸畾浠诲姟鐘舵�佷负 RUNNING锛堣交閲忕骇涔愯閿侀攣鏈哄埗锛�
+            const lockResult = await this.reportJoblogEntity.update(
+                { id: job.id, status: job.status }, // 甯︽湁鍘熷鐘舵�佹鏌ワ紝闃叉琚埆鐨勭嚎绋嬫姠璺�
+                { status: 'RUNNING', startTime: new Date() }
+            );
+
+            if (lockResult.affected === 0) {
+                continue; // 娌℃姠鍒伴攣锛岃鏄庡埆鐨勫畾鏃跺櫒鍦ㄨ窇瀹冿紝璺宠繃
+            }
+
+            // 鐪熸寮�濮嬭窇鏁版嵁
+            try {
+                let recordsProcessed = 0;
+
+
+                recordsProcessed = await this.taskServiceVehicle.aggregateDailyData(job.dataDate) || 0;
+
+
+                // 鎴愬姛锛氭洿鏂扮姸鎬�
+                await this.reportJoblogEntity.update(job.id, {
+                    status: 'SUCCESS',
+                    recordsProcessed: recordsProcessed,
+                    endTime: new Date(),
+                    errorMessage: '',
+                });
+
+            } catch (error) {
+                // 澶辫触锛氳褰曞爢鏍堬紝绛夊緟涓嬩竴灏忔椂閲嶈瘯
+
+                var errorMessage = '';
+                if (error instanceof Error) {
+                    errorMessage = error.stack ?? error.message;
+                }
+                if (typeof error === 'string') {
+                    errorMessage = error;
+                }
+                try {
+                    errorMessage = JSON.stringify(error);
+                } catch {
+                    errorMessage = String(error);
+                }
+
+                await this.reportJoblogEntity.update(job.id, {
+                    status: 'FAILED',
+                    endTime: new Date(),
+                    errorMessage: errorMessage,
+                    failedCount: () => 'failedCount + 1',
+                });
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/modules/report/service/dailyvehicle.ts b/src/modules/report/service/dailyvehicle.ts
new file mode 100644
index 0000000..1fbc755
--- /dev/null
+++ b/src/modules/report/service/dailyvehicle.ts
@@ -0,0 +1,14 @@
+import { Provide } from '@midwayjs/core';
+import { BaseService } from '@cool-midway/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { ReportDailyVehicleEntity } from '../entity/dailyvehicle';
+
+/**
+ * 姣忔棩绔欑偣姹囨�绘湇鍔�
+ */
+@Provide()
+export class ReportDailyVehicleService extends BaseService {
+  @InjectEntityModel(ReportDailyVehicleEntity)
+  reportDailyVehicleEntity: Repository<ReportDailyVehicleEntity>;
+}
\ No newline at end of file
diff --git a/src/modules/report/service/taskservice.ts b/src/modules/report/service/taskservice.ts
index 9536abe..56c3a21 100644
--- a/src/modules/report/service/taskservice.ts
+++ b/src/modules/report/service/taskservice.ts
@@ -3,8 +3,8 @@
 import { InjectEntityModel } from '@midwayjs/typeorm';
 import { Repository, In } from 'typeorm';
 import { ReportJoblogEntity } from '../entity/joblog';
-import { TaskSiteService } from './tasksiteservice'
-import { TaskDepartmentService } from './taskdepartmentservice'
+import { ExecutorSiteDeptDaily } from './ExecutorSiteDeptDaily'
+import { ExecutorVehicleDaily } from './ExecutorVehicleDaily'
 
 /**
  *  瀹氭椂浠诲姟-姹囨�荤珯鐐�/鍚勪釜鍗曚綅鐨勫悇绫诲瀮鍦炬眹鎬绘暟鎹�
@@ -17,12 +17,13 @@
   @InjectEntityModel(ReportJoblogEntity)
   reportJoblogEntity: Repository<ReportJoblogEntity>;
 
-  // 娉ㄥ叆浣犲叿浣撶殑涓氬姟 Service锛岀敤浜庤窇鐪熸鐨勬眹鎬� SQL
   @Inject()
-  taskSiteService: TaskSiteService;
+  executorSiteDeptDaily: ExecutorSiteDeptDaily;
 
   @Inject()
-  departmentCubeService: TaskDepartmentService;
+  executorVehicleDaily: ExecutorVehicleDaily;
+
+
 
   /**
    * 澶栭儴瀹氭椂浠诲姟锛圕rontab锛夋瘡灏忔椂璋冪敤鐨勬�诲叆鍙�
@@ -32,10 +33,17 @@
     // 1. 鑷姩鐪嬮棬鐙楋細纭繚鏄ㄥぉ鍜屼粖澶╃殑浠诲姟瀹炰緥宸茬粡鍦ㄨ〃閲屽垵濮嬪寲浜�
     await this.ensureJobInstancesCreated();
 
-    // 2. 鎵ц寮曟搸锛氭崬鍑洪渶瑕佹墽琛屾垨閲嶈瘯鐨勪换鍔�
-    await this.executePendingJobs();
+    // 2. 涓ょ被浠诲姟鍚屾椂鎵ц
+    // 鍚庣画澧炲姞鏂扮殑 task service 姣斿 鏇存柊 钀ょ煶浜� token锛� 缁熻鍒嗘嫞鍛樿涓虹瓑绛夈��
 
-    return '浠诲姟鎵ц鎴愬姛:ReportTaskService';
+    await Promise.all([
+      this.executorSiteDeptDaily.execute(),
+      this.executorVehicleDaily.execute()
+
+    ]);
+
+
+    return '浠诲姟鎵ц:ReportTaskService';
   }
 
   /**
@@ -56,10 +64,14 @@
 
     // 鎴戜滑闇�瑕佸畧鎶ょ殑鐩爣鏃ユ湡鍜屼换鍔$被鍨嬬粍鍚�
     const targets = [
-      { date: yesterdayStr, type: 'SITE_DAILY',isToday: false},
-      { date: yesterdayStr, type: 'DEPT_DAILY',isToday: false},
-      { date: todayStr, type: 'SITE_DAILY' ,isToday: true},
-      { date: todayStr, type: 'DEPT_DAILY' ,isToday: true},
+      { date: yesterdayStr, type: 'SITE_DAILY', isToday: false },
+      { date: yesterdayStr, type: 'DEPT_DAILY', isToday: false },
+      { date: yesterdayStr, type: 'VEHICLE_DAILY', isToday: false },   // 鏂板
+
+      { date: todayStr, type: 'SITE_DAILY', isToday: true },
+      { date: todayStr, type: 'DEPT_DAILY', isToday: true },
+      { date: todayStr, type: 'VEHICLE_DAILY', isToday: true },         // 鏂板
+
     ];
 
     for (const target of targets) {
@@ -75,7 +87,7 @@
           job.dataDate = target.date;
           job.status = 'INIT';
           await this.reportJoblogEntity.save(job);
-        }else if (target.isToday && exist.status === 'SUCCESS') {
+        } else if (target.isToday && exist.status === 'SUCCESS') {
           // 2. 銆愭牳蹇冧慨澶嶃�戝鏋滄槸浠婂ぉ鐨勪换鍔★紝涓斿凡缁忔槸 SUCCESS 鐘舵�侊紝寮鸿娲楀洖 INIT 鍙備笌鏈皬鏃剁殑閲嶇畻
           await this.reportJoblogEntity.update(exist.id, {
             status: 'INIT'
@@ -87,88 +99,5 @@
     }
   }
 
-  /**
-   * 鎹炲嚭 INIT 鍜� FAILED 鐨勪换鍔★紝骞朵覆琛屼弗鏍兼寜椤哄簭鎵ц
-   */
-  private async executePendingJobs() {
-    // 鎹炲嚭鎵�鏈夊緟澶勭悊浠诲姟
-    const pendingJobs = await this.reportJoblogEntity.find({
-      where: {
-        status: In(['INIT', 'FAILED']),
-      },
-      order: {
-        dataDate: 'ASC', // 鍏堢畻鍘嗗彶锛屽悗绠椾粖澶�
-        // 纭繚鍦ㄥ悓涓�澶╁唴锛孲ITE_DAILY 鍏堣窇锛孌EPT_DAILY 鍚庤窇
-        // 鍦� JS 鏁扮粍涓垜浠彲浠ヨ繘涓�姝ユ帶鍒舵帓搴忥紝鎴栬�呯敱涓氬姟閫昏緫涓ユ牸鎺у埗渚濊禆
-      },
-    });
 
-    for (const job of pendingJobs) {
-      // 涓ユ牸渚濊禆妫�鏌ワ細濡傛灉鏄儴闂ㄦ棩琛ㄤ换鍔★紝蹇呴』纭繚鍚屽ぉ鐨勭珯鐐规棩琛ㄤ换鍔″凡缁忔槸 SUCCESS
-      if (job.jobName === 'DEPT_DAILY') {
-        const siteJob = await this.reportJoblogEntity.findOneBy({
-          dataDate: job.dataDate,
-          jobName: 'SITE_DAILY',
-        });
-        if (!siteJob || siteJob.status !== 'SUCCESS') {
-          // 婧愬ご鏄庣粏杩樻病绠楀ソ/鎴栬繕鍦ㄨ繍琛岋紝閮ㄩ棬琛ㄤ换鍔″繀椤荤瓑寰咃紝璺宠繃鏈鎵ц
-          continue;
-        }
-      }
-
-      // 閿佸畾浠诲姟鐘舵�佷负 RUNNING锛堣交閲忕骇涔愯閿侀攣鏈哄埗锛�
-      const lockResult = await this.reportJoblogEntity.update(
-        { id: job.id, status: job.status }, // 甯︽湁鍘熷鐘舵�佹鏌ワ紝闃叉琚埆鐨勭嚎绋嬫姠璺�
-        { status: 'RUNNING', startTime: new Date() }
-      );
-
-      if (lockResult.affected === 0) {
-        continue; // 娌℃姠鍒伴攣锛岃鏄庡埆鐨勫畾鏃跺櫒鍦ㄨ窇瀹冿紝璺宠繃
-      }
-
-      // 鐪熸寮�濮嬭窇鏁版嵁
-      try {
-        let recordsProcessed = 0 ;
-
-        if (job.jobName === 'SITE_DAILY') {
-          // 璋冪敤浣犲啓鐨� 绔欑偣鏃ヨ〃 鎻愬彇閫昏緫
-          recordsProcessed = await this.taskSiteService.aggregateDailyData(job.dataDate) || 0;
-        } else if (job.jobName === 'DEPT_DAILY') {
-          // 璋冪敤浣犲啓鐨� 閮ㄩ棬鏍戣桨鐐� 鎻愬彇閫昏緫
-          recordsProcessed = await this.departmentCubeService.aggregateDailyData(job.dataDate) || 0;
-        }
-
-        // 鎴愬姛锛氭洿鏂扮姸鎬�
-        await this.reportJoblogEntity.update(job.id, {
-            status: 'SUCCESS',
-            recordsProcessed:recordsProcessed,
-            endTime: new Date(),
-            errorMessage: '',
-          });
-
-      } catch (error) {
-        // 澶辫触锛氳褰曞爢鏍堬紝绛夊緟涓嬩竴灏忔椂閲嶈瘯
-
-        var errorMessage = '';
-        if (error instanceof Error) {
-          errorMessage = error.stack ?? error.message;
-        }
-        if (typeof error === 'string') {
-          errorMessage = error;
-        }
-        try {
-          errorMessage = JSON.stringify(error);
-        } catch {
-          errorMessage = String(error);
-        }
-
-        await this.reportJoblogEntity.update(job.id, {
-          status: 'FAILED',
-          endTime: new Date(),
-          errorMessage: errorMessage,
-          failedCount: () => 'failedCount + 1',
-        });
-      }
-    }
-  }
 }
\ No newline at end of file
diff --git a/src/modules/report/service/taskdepartmentservice.ts b/src/modules/report/service/taskservicedepartment.ts
similarity index 98%
rename from src/modules/report/service/taskdepartmentservice.ts
rename to src/modules/report/service/taskservicedepartment.ts
index f0d920d..5539997 100644
--- a/src/modules/report/service/taskdepartmentservice.ts
+++ b/src/modules/report/service/taskservicedepartment.ts
@@ -12,7 +12,7 @@
  *  瀹氭椂浠诲姟-瀛愪换鍔� 姹囨�婚儴闂ㄧ殑鍚勭被鍨冨溇鏁版嵁
  */
 @Provide()
-export class TaskDepartmentService extends BaseService {
+export class TaskServiceDepartment extends BaseService {
   @InjectEntityModel(ReportDailysiteEntity)
   reportDailysiteEntity: Repository<ReportDailysiteEntity>;
 
diff --git a/src/modules/report/service/tasksiteservice.ts b/src/modules/report/service/taskservicesite.ts
similarity index 98%
rename from src/modules/report/service/tasksiteservice.ts
rename to src/modules/report/service/taskservicesite.ts
index 0e3efe4..60582fb 100644
--- a/src/modules/report/service/tasksiteservice.ts
+++ b/src/modules/report/service/taskservicesite.ts
@@ -7,7 +7,7 @@
 import { BasicdataSiteEntity } from '../../basicdata/entity/site'
 
 @Provide()
-export class TaskSiteService extends BaseService {
+export class TaskServiceSite extends BaseService {
     @InjectEntityModel(ReportDailysiteEntity)
     reportDailysiteEntity: Repository<ReportDailysiteEntity>;
 
diff --git a/src/modules/report/service/taskservicevehicle.ts b/src/modules/report/service/taskservicevehicle.ts
new file mode 100644
index 0000000..22405df
--- /dev/null
+++ b/src/modules/report/service/taskservicevehicle.ts
@@ -0,0 +1,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;
+    }
+}

--
Gitblit v1.9.1