From 6484c72be4230e89ab45dc928250de9d5f3326ca Mon Sep 17 00:00:00 2001
From: wangzhibo <wangzhibo@shsening.com>
Date: 星期三, 19 八月 2026 10:01:05 +0800
Subject: [PATCH] add maps

---
 src/modules/maps/controller/admin/live.ts |   64 +++++++++
 src/modules/maps/config.ts                |   14 ++
 src/modules/maps/service/live.ts          |  293 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 371 insertions(+), 0 deletions(-)

diff --git a/src/modules/maps/config.ts b/src/modules/maps/config.ts
new file mode 100644
index 0000000..d5e8932
--- /dev/null
+++ b/src/modules/maps/config.ts
@@ -0,0 +1,14 @@
+import { ModuleConfig } from '@cool-midway/core';
+
+/**
+ * 鍦板浘鐩戞帶妯″潡
+ */
+export default () => {
+  return {
+    name: '鍦板浘鐩戞帶',
+    description: '绔欑偣銆佸垎鎷e憳銆佽溅杈嗗疄鏃朵綅缃笌杞ㄨ抗',
+    middlewares: [],
+    globalMiddlewares: [],
+    order: 0,
+  } as ModuleConfig;
+};
diff --git a/src/modules/maps/controller/admin/live.ts b/src/modules/maps/controller/admin/live.ts
new file mode 100644
index 0000000..a631c41
--- /dev/null
+++ b/src/modules/maps/controller/admin/live.ts
@@ -0,0 +1,64 @@
+import { Body, Inject, Post } from '@midwayjs/core';
+import { CoolController, BaseController } from '@cool-midway/core';
+import { MapsLiveService } from '../../service/live';
+
+/**
+ * 鍦板浘瀹炴椂浣嶇疆涓庤建杩�
+ */
+@CoolController()
+export class AdminMapsLiveController extends BaseController {
+  @Inject()
+  mapsLiveService: MapsLiveService;
+
+  @Post('/sites', { summary: '绔欑偣鍦板浘鐐逛綅' })
+  async sites() {
+    return this.ok(await this.mapsLiveService.sites());
+  }
+
+  @Post('/siteWeight', { summary: '绔欑偣绉伴噸鍘嗗彶' })
+  async siteWeight(
+    @Body('businessId') businessId: string,
+    @Body('startTime') startTime: string,
+    @Body('endTime') endTime: string
+  ) {
+    return this.ok(await this.mapsLiveService.siteWeight(businessId, startTime, endTime));
+  }
+
+  @Post('/sorterOnline', { summary: '鍦ㄧ嚎鍒嗘嫞鍛樹綅缃�' })
+  async sorterOnline() {
+    return this.ok(await this.mapsLiveService.sorterOnline());
+  }
+
+  @Post('/sorterTrack', { summary: '鍒嗘嫞鍛樺巻鍙茶建杩�' })
+  async sorterTrack(
+    @Body('idCard') idCard: string,
+    @Body('startTime') startTime: string,
+    @Body('endTime') endTime: string
+  ) {
+    return this.ok(await this.mapsLiveService.sorterTrack(idCard, startTime, endTime));
+  }
+
+  @Post('/sorterHealth', { summary: '鍒嗘嫞鍛樺仴搴蜂笌鍛婅' })
+  async sorterHealth(@Body('idCard') idCard: string) {
+    return this.ok(await this.mapsLiveService.sorterHealth(idCard));
+  }
+
+  @Post('/vehicleOnline', { summary: '鍦ㄧ嚎杞﹁締浣嶇疆' })
+  async vehicleOnline() {
+    return this.ok(await this.mapsLiveService.vehicleOnline());
+  }
+
+  @Post('/vehicleTrack', { summary: '杞﹁締鍘嗗彶杞ㄨ抗' })
+  async vehicleTrack(
+    @Body('vehicleNo') vehicleNo: string,
+    @Body('startTime') startTime: string,
+    @Body('endTime') endTime: string
+  ) {
+    return this.ok(await this.mapsLiveService.vehicleTrack(vehicleNo, startTime, endTime));
+  }
+
+  @Post('/vehicleEvents', { summary: '杞﹁締鍙告満琛屼负娑堟伅' })
+  async vehicleEvents(@Body('vehicleNo') vehicleNo: string) {
+    return this.ok(await this.mapsLiveService.vehicleEvents(vehicleNo));
+  }
+}
diff --git a/src/modules/maps/service/live.ts b/src/modules/maps/service/live.ts
new file mode 100644
index 0000000..2aeddc3
--- /dev/null
+++ b/src/modules/maps/service/live.ts
@@ -0,0 +1,293 @@
+import { Inject, Provide } from '@midwayjs/core';
+import { BaseService } from '@cool-midway/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository, SelectQueryBuilder } from 'typeorm';
+import { Context } from '@midwayjs/koa';
+import { BasicdataSiteEntity } from '../../basicdata/entity/site';
+import { BasicdataSorterEntity } from '../../basicdata/entity/sorter';
+import { BasicdataVehicleEntity } from '../../basicdata/entity/vehicle';
+import { BasicdataWastetypeEntity } from '../../basicdata/entity/wastetype';
+import { BasicdataIotEntity } from '../../basicdata/entity/iot';
+import { PushSorterGpsEntity } from '../../push/entity/sortergps';
+import { PushSorterHealthEntity } from '../../push/entity/sorterhealth';
+import { PushSorterAlarmEntity } from '../../push/entity/sorteralarm';
+import { PushVehicleGpsEntity } from '../../push/entity/vehiclegps';
+import { PushSiteweightEntity } from '../../push/entity/siteweight';
+import { PushDevicealarmEntity } from '../../push/entity/devicealarm';
+
+function n(v: any, d = 0) {
+  const x = Number(v);
+  return Number.isFinite(x) ? x : d;
+}
+
+@Provide()
+export class MapsLiveService extends BaseService {
+  @Inject()
+  ctx: Context;
+
+  @InjectEntityModel(BasicdataSiteEntity)
+  siteRepo: Repository<BasicdataSiteEntity>;
+
+  @InjectEntityModel(BasicdataSorterEntity)
+  sorterRepo: Repository<BasicdataSorterEntity>;
+
+  @InjectEntityModel(BasicdataVehicleEntity)
+  vehicleRepo: Repository<BasicdataVehicleEntity>;
+
+  @InjectEntityModel(PushSorterGpsEntity)
+  sorterGps: Repository<PushSorterGpsEntity>;
+
+  @InjectEntityModel(PushSorterHealthEntity)
+  sorterHealthRepo: Repository<PushSorterHealthEntity>;
+
+  @InjectEntityModel(PushSorterAlarmEntity)
+  sorterAlarm: Repository<PushSorterAlarmEntity>;
+
+  @InjectEntityModel(PushVehicleGpsEntity)
+  vehicleGps: Repository<PushVehicleGpsEntity>;
+
+  @InjectEntityModel(PushSiteweightEntity)
+  siteWeightRepo: Repository<PushSiteweightEntity>;
+
+  @InjectEntityModel(PushDevicealarmEntity)
+  deviceAlarm: Repository<PushDevicealarmEntity>;
+
+  @InjectEntityModel(BasicdataIotEntity)
+  iotRepo: Repository<BasicdataIotEntity>;
+
+  private deptIds(): number[] | null {
+    const admin = this.ctx.admin;
+    if (!admin || admin.username === 'admin') return null;
+    return Array.isArray(admin.departmentIds) ? admin.departmentIds.map(Number) : [];
+  }
+
+  private applyDept(qb: SelectQueryBuilder<any>, alias: string) {
+    const ids = this.deptIds();
+    if (ids === null) return qb;
+    if (!ids.length) {
+      qb.andWhere('1 = 0');
+      return qb;
+    }
+    qb.andWhere(`${alias}.departmentId IN (:...deptIds)`, { deptIds: ids });
+    return qb;
+  }
+
+  async sites() {
+    const qb = this.siteRepo
+      .createQueryBuilder('a')
+      .select([
+        'a.id',
+        'a.businessId',
+        'a.businessName',
+        'a.departmentId',
+        'a.siteType',
+        'a.address',
+        'a.leader',
+        'a.longitude',
+        'a.latitude',
+        'a.photo',
+        'a.status',
+        'a.remark',
+      ])
+      .where('a.status = :status', { status: 1 })
+      .andWhere('a.longitude IS NOT NULL')
+      .andWhere('a.latitude IS NOT NULL');
+    this.applyDept(qb, 'a');
+    const rows = await qb.getMany();
+    return rows.filter(e => n(e.longitude) && n(e.latitude));
+  }
+
+  async siteWeight(businessId: string, startTime: string, endTime: string) {
+    if (!businessId) return [];
+    const qb = this.siteWeightRepo
+      .createQueryBuilder('a')
+      .leftJoin(BasicdataWastetypeEntity, 'c', 'a.garbageType = c.code')
+      .select('a.id', 'id')
+      .addSelect('a.businessId', 'businessId')
+      .addSelect('a.deviceNo', 'deviceNo')
+      .addSelect('a.uploadTime', 'uploadTime')
+      .addSelect('a.garbageType', 'garbageType')
+      .addSelect('c.name', 'garbageName')
+      .addSelect('a.weight', 'weight')
+      .where('a.businessId = :businessId', { businessId })
+      .andWhere('a.uploadTime BETWEEN :startTime AND :endTime', { startTime, endTime })
+      .orderBy('a.uploadTime', 'DESC')
+      .limit(200);
+    this.applyDept(qb, 'a');
+    return qb.getRawMany();
+  }
+
+  async sorterOnline() {
+    const since = new Date(Date.now() - 15 * 60 * 1000);
+    const qb = this.sorterGps
+      .createQueryBuilder('a')
+      .where('a.uploadTime >= :since', { since })
+      .orderBy('a.uploadTime', 'DESC')
+      .take(4000);
+    this.applyDept(qb, 'a');
+    const rows = await qb.getMany();
+    const latest = new Map<string, PushSorterGpsEntity>();
+    rows.forEach(r => {
+      if (r.idCard && !latest.has(r.idCard)) latest.set(r.idCard, r);
+    });
+    const cards = [...latest.keys()];
+    const sorters = cards.length
+      ? await this.sorterRepo
+          .createQueryBuilder('s')
+          .where('s.businessId IN (:...cards)', { cards })
+          .getMany()
+      : [];
+    const sorterMap = new Map(sorters.map(s => [s.businessId, s]));
+    return [...latest.values()].map(g => {
+      const s = sorterMap.get(g.idCard);
+      return {
+        idCard: g.idCard,
+        name: s?.businessName || g.name,
+        phone: s?.phone || '',
+        sex: s?.sex,
+        workSiteId: s?.workSiteId,
+        departmentId: s?.departmentId || g.departmentId,
+        departmentName: g.departmentName,
+        longitude: n(g.longitude),
+        latitude: n(g.latitude),
+        speed: n(g.speed),
+        uploadTime: g.uploadTime,
+        status: s?.status,
+      };
+    });
+  }
+
+  async sorterTrack(idCard: string, startTime: string, endTime: string) {
+    if (!idCard) return [];
+    const qb = this.sorterGps
+      .createQueryBuilder('a')
+      .where('a.idCard = :idCard', { idCard })
+      .andWhere('a.uploadTime BETWEEN :startTime AND :endTime', { startTime, endTime })
+      .andWhere('a.longitude IS NOT NULL')
+      .andWhere('a.latitude IS NOT NULL')
+      .orderBy('a.uploadTime', 'ASC')
+      .take(5000);
+    this.applyDept(qb, 'a');
+    const rows = await qb.getMany();
+    return rows.map(r => ({
+      longitude: n(r.longitude),
+      latitude: n(r.latitude),
+      speed: n(r.speed),
+      time: r.uploadTime,
+    }));
+  }
+
+  async sorterHealth(idCard: string) {
+    if (!idCard) return { recent: [], abnormal: [], alarms: [] };
+    const recentQb = this.sorterHealthRepo
+      .createQueryBuilder('a')
+      .where('a.idCard = :idCard', { idCard })
+      .orderBy('a.uploadTime', 'DESC')
+      .take(20);
+    this.applyDept(recentQb, 'a');
+    const recent = await recentQb.getMany();
+
+    const abQb = this.sorterHealthRepo
+      .createQueryBuilder('a')
+      .where('a.idCard = :idCard', { idCard })
+      .andWhere("a.dataType = '寮傚父'")
+      .orderBy('a.uploadTime', 'DESC')
+      .take(20);
+    this.applyDept(abQb, 'a');
+    const abnormal = await abQb.getMany();
+
+    const alarmQb = this.sorterAlarm
+      .createQueryBuilder('a')
+      .where('a.idCard = :idCard', { idCard })
+      .orderBy('a.uploadTime', 'DESC')
+      .take(20);
+    this.applyDept(alarmQb, 'a');
+    const alarms = await alarmQb.getMany();
+
+    return { recent, abnormal, alarms };
+  }
+
+  async vehicleOnline() {
+    const since = new Date(Date.now() - 15 * 60 * 1000);
+    const qb = this.vehicleGps
+      .createQueryBuilder('a')
+      .where('a.gpsTime >= :since', { since })
+      .andWhere("a.vehicleNo IS NOT NULL AND a.vehicleNo <> ''")
+      .orderBy('a.gpsTime', 'DESC')
+      .take(4000);
+    this.applyDept(qb, 'a');
+    const rows = await qb.getMany();
+    const latest = new Map<string, PushVehicleGpsEntity>();
+    rows.forEach(r => {
+      if (r.vehicleNo && !latest.has(r.vehicleNo)) latest.set(r.vehicleNo, r);
+    });
+    const plates = [...latest.keys()];
+    const vehicles = plates.length
+      ? await this.vehicleRepo
+          .createQueryBuilder('v')
+          .where('v.businessId IN (:...plates)', { plates })
+          .getMany()
+      : [];
+    const vehicleMap = new Map(vehicles.map(v => [v.businessId, v]));
+    return [...latest.values()].map(g => {
+      const v = vehicleMap.get(g.vehicleNo);
+      return {
+        vehicleNo: g.vehicleNo,
+        sn: g.sn,
+        vehicleType: v?.vehicleType,
+        capacityTon: v?.capacityTon,
+        departmentId: v?.departmentId || g.departmentId,
+        departmentName: g.departmentName,
+        longitude: n(g.longitude),
+        latitude: n(g.latitude),
+        speed: n(g.speed),
+        direction: g.direction,
+        gpsTime: g.gpsTime,
+        status: v?.status,
+        gpsStatus: g.status,
+      };
+    });
+  }
+
+  async vehicleTrack(vehicleNo: string, startTime: string, endTime: string) {
+    if (!vehicleNo) return [];
+    const qb = this.vehicleGps
+      .createQueryBuilder('a')
+      .where('a.vehicleNo = :vehicleNo', { vehicleNo })
+      .andWhere('a.gpsTime BETWEEN :startTime AND :endTime', { startTime, endTime })
+      .andWhere('a.longitude IS NOT NULL')
+      .andWhere('a.latitude IS NOT NULL')
+      .orderBy('a.gpsTime', 'ASC')
+      .take(5000);
+    this.applyDept(qb, 'a');
+    const rows = await qb.getMany();
+    return rows.map(r => ({
+      longitude: n(r.longitude),
+      latitude: n(r.latitude),
+      speed: n(r.speed),
+      direction: r.direction,
+      time: r.gpsTime,
+    }));
+  }
+
+  async vehicleEvents(vehicleNo: string) {
+    if (!vehicleNo) return [];
+    const iots = await this.iotRepo.find({
+      where: { businessId: vehicleNo },
+      select: ['iotCode', 'iotName', 'iotTypeCode'],
+    });
+    const codes = iots.map(e => e.iotCode).filter(Boolean);
+    if (!codes.length) return [];
+    const nameMap = new Map(iots.map(e => [e.iotCode, e.iotName]));
+    const qb = this.deviceAlarm
+      .createQueryBuilder('a')
+      .where('a.devSerial IN (:...codes)', { codes })
+      .orderBy('a.createTime', 'DESC')
+      .take(30);
+    const rows = await qb.getMany();
+    return rows.map(r => ({
+      ...r,
+      deviceName: nameMap.get(r.devSerial) || r.devSerial,
+    }));
+  }
+}

--
Gitblit v1.9.1