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